diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ae24767 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,88 @@ +# Git +.git +.gitignore +.gitattributes + +# CI +.codeclimate.yml +.travis.yml +.taskcluster.yml + +# Docker +docker-compose.yml +Dockerfile +.docker +.dockerignore + +# Byte-compiled / optimized / DLL files +**/__pycache__/ +**/*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# 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/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Virtual environment +.env +.venv/ +venv/ + +# PyCharm +.idea + +# Python mode for VIM +.ropeproject +**/.ropeproject + +# Vim swap files +**/*.swp + +# VS Code +.vscode/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dc1a4af --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +FROM python:3.12.9-slim-bookworm AS base + +WORKDIR /app + +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONFAULTHANDLER=1 +ENV PATH=/home/pysatluser/.local/bin:$PATH +ENV POETRY_HOME=/opt/poetry + +RUN apt-get update && apt-get install -y curl && \ + curl -sSL https://install.python-poetry.org | python - && \ + cd /usr/local/bin && \ + ln -s /opt/poetry/bin/poetry && \ + poetry config virtualenvs.create true + +# Копируем файлы зависимостей +COPY pyproject.toml ./ + +# Устанавливаем зависимости +RUN poetry install --no-interaction --no-ansi + +# Копируем остальные файлы +COPY . . + +ENTRYPOINT ["poetry", "run", "python", "-m", "stattest.main"] +# Default to experiment mode +CMD [ "experiment", "--config", "../../config_examples/config_example.json" ] diff --git a/config_examples/config_example.json b/config_examples/config_example.json new file mode 100644 index 0000000..0bad525 --- /dev/null +++ b/config_examples/config_example.json @@ -0,0 +1,76 @@ +{ + "generator_configuration": { + "generators": [ + { + "name": "ExponentialGenerator", + "params": { + "lam": 0.5 + } + } + ], + "sizes": [100, 200], + "count": 1000, + "threads": 1, + "skip_if_exists": true, + "clear_before": false, + "skip_step": false, + "show_progress": false, + "listeners": [ + { + "name": "TimeEstimationListener" + } + ] + }, + "test_configuration": { + "tests": [ + { + "name": "KolmogorovSmirnovWeibullGofStatistic" + } + ], + "threads": 8, + "worker": + { + "name": "PowerCalculationWorker", + "params": { + "alpha": 0.05, + "monte_carlo_count": 100000, + "cv_store": { + "name": "CriticalValueDbStore", + "params": { + "db_url": "sqlite:///weibull_experiment.sqlite" + } + }, + "hypothesis": { + "name": "WeibullHypothesis" + } + } + }, + "listeners": [ + { + "name": "TimeEstimationListener" + } + ] + }, + "report_configuration": { + "report_builder": { + "name": "PdfPowerReportBuilder" + }, + "listeners": [] + }, + "rvs_store": + { + "name": "RvsDbStore", + "params": + { + "db_url": "sqlite:///weibull_experiment.sqlite" + } + }, + "result_store": + { + "name": "ResultDbStore", + "params": + { + "db_url": "sqlite:///weibull_experiment.sqlite" + } + } +} \ No newline at end of file diff --git a/config_examples/config_example_full.json b/config_examples/config_example_full.json deleted file mode 100644 index 226a7ea..0000000 --- a/config_examples/config_example_full.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "generator_path": "", - "alternatives_configuration": { - "alternatives": [ - { - "name": "BBBRVSGenerator", - "params": { - "a": 0.3, - "b": 0.5 - } - } - ], - "sizes": [ - 30, - 40 - ], - "count": 1000, - "threads": 4, - "skip_if_exists": true, - "clear_before": false, - "skip_step": false, - "listeners": [ - "StepListener" - ] - }, - "test_configuration": { - "tests": [ - "AbstractTest" - ], - "worker": "TestWorker", - "hypothesis": "AbstractHypothesis", - "threads": 4, - "listeners": [ - "StepListener" - ], - "skip_step": false - }, - "report_configuration": { - "report_builder": "ReportBuilder", - "data_reader": "", - "listeners": [ - "StepListener" - ] - } -} \ No newline at end of file diff --git a/config_examples/weibull_experiment.json b/config_examples/weibull_experiment.json new file mode 100644 index 0000000..c49d76b --- /dev/null +++ b/config_examples/weibull_experiment.json @@ -0,0 +1,72 @@ +{ + "generator_configuration": { + "generators": [ + { + "name": "ExponentialGenerator", + "params": { + "lam": 0.5 + } + } + ], + "sizes": [100, 200], + "count": 1000, + "threads": 1, + "listeners": [ + { + "name": "TimeEstimationListener" + } + ] + }, + "test_configuration": { + "tests": [ + { + "name": "KSWeibullTest" + } + ], + "threads": 8, + "worker": + { + "name": "PowerCalculationWorker", + "params": { + "alpha": 0.05, + "monte_carlo_count": 100000, + "cv_store": { + "name": "CriticalValueDbStore", + "params": { + "db_url": "sqlite:///weibull_experiment.sqlite" + } + }, + "hypothesis": { + "name": "WeibullHypothesis" + } + } + }, + "listeners": [ + { + "name": "TimeEstimationListener" + } + ] + }, + "report_configuration": { + "report_builder": { + "name": "PdfPowerReportBuilder" + }, + "listeners": [] + }, + "rvs_store": + { + "name": "RvsDbStore", + "params": + { + "db_url": "sqlite:///weibull_experiment.sqlite" + } + }, + "result_store": + { + "name": "ResultDbStore", + "params": + { + "db_url": "sqlite:///weibull_experiment.sqlite" + } + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ec83518 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +--- +services: + stattest: + build: + context: . + # dockerfile: "./docker/Dockerfile.custom" + restart: unless-stopped + container_name: pysatl-experiment + volumes: + - "./user_data:/stattest/user_data" + # Default command used when running `docker compose up` + command: > + experiment + --logfile /stattest/user_data/logs/pysatl_experiment.log + --config /stattest/user_data/config.json \ No newline at end of file diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 3ed50ea..f925d87 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -2,6 +2,6 @@ markdown==3.7 mkdocs==1.6.1 mkdocs-material==9.5.44 mdx_truly_sane_lists==1.3 -pymdown-extensions==10.12 -jinja2==3.1.4 +pymdown-extensions==10.14.3 +jinja2==3.1.6 mike==2.1.3 diff --git a/graph_norm_experiment.py b/experiments/graph_norm_experiment.py similarity index 94% rename from graph_norm_experiment.py rename to experiments/graph_norm_experiment.py index 7b05851..e22847e 100644 --- a/graph_norm_experiment.py +++ b/experiments/graph_norm_experiment.py @@ -9,8 +9,8 @@ from stattest.experiment import Experiment from stattest.experiment.configuration.configuration import ( - AlternativeConfiguration, ExperimentConfiguration, + GeneratorConfiguration, ReportConfiguration, TestConfiguration, ) @@ -23,7 +23,7 @@ ) from stattest.experiment.hypothesis import NormalHypothesis from stattest.experiment.listener.listeners import TimeEstimationListener -from stattest.experiment.report.model import PdfPowerReportBuilder +from stattest.experiment.report.builders import PdfPowerReportBuilder from stattest.experiment.test.worker import PowerCalculationWorker from stattest.persistence.db_store import CriticalValueDbStore, ResultDbStore, RvsDbStore @@ -67,7 +67,7 @@ KolmogorovSmirnovNormalityGofStatistic(), ] - alternatives_configuration = AlternativeConfiguration( + alternatives_configuration = GeneratorConfiguration( alternatives, sizes, count=1_000, threads=generation_threads, listeners=listeners ) diff --git a/weibull_experiment.py b/experiments/weibull_experiment.py similarity index 100% rename from weibull_experiment.py rename to experiments/weibull_experiment.py diff --git a/generators/gen.py b/generators/gen.py index e78a9b7..e3d0746 100644 --- a/generators/gen.py +++ b/generators/gen.py @@ -2,14 +2,14 @@ from stattest.experiment.generator import AbstractRVSGenerator -class BBBRVSGenerator(AbstractRVSGenerator): - def __init__(self, a, b, **kwargs): +class GeneratorTest(AbstractRVSGenerator): + def __init__(self, a=1, b=2, **kwargs): super().__init__(**kwargs) self.a = a self.b = b def code(self): - return super()._convert_to_code(["beta", self.a, self.b]) + return super()._convert_to_code(["generator_test", self.a, self.b]) def generate(self, size): return generate_beta(size=size, a=self.a, b=self.b) diff --git a/pyproject.toml b/pyproject.toml index fbb86b8..f8af12c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ package-mode = false [tool.poetry.group.dev.dependencies] markdown = "3.7" mkdocs = "1.6.1" -mkdocs-material = "9.6.9" +mkdocs-material = "9.6.11" mdx-truly-sane-lists = "1.3" pymdown-extensions = "10.14.3" jinja2 = "3.1.6" @@ -40,9 +40,9 @@ mike = "2.1.3" isort = "6.0.1" coveralls = "4.0.1" pytest = "8.3.5" -pytest-cov = "6.0.0" +pytest-cov = "6.1.1" pytest-random-order = "1.1.1" -ruff = "0.11.2" +ruff = "0.11.4" pytest-mock = "3.14.0" pre-commit = "4.2.0" diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..dcd2127 --- /dev/null +++ b/setup.sh @@ -0,0 +1,250 @@ +#!/usr/bin/env bash +#encoding=utf8 + +function echo_block() { + echo "----------------------------" + echo $1 + echo "----------------------------" +} + +function check_installed_pip() { + ${PYTHON} -m pip > /dev/null + if [ $? -ne 0 ]; then + echo_block "Installing Pip for ${PYTHON}" + curl https://bootstrap.pypa.io/get-pip.py -s -o get-pip.py + ${PYTHON} get-pip.py + rm get-pip.py + fi +} + +# Check which python version is installed +function check_installed_python() { + if [ -n "${VIRTUAL_ENV}" ]; then + echo "Please deactivate your virtual environment before running setup.sh." + echo "You can do this by running 'deactivate'." + exit 2 + fi + + for v in 12 11 10 + do + PYTHON="python3.${v}" + which $PYTHON + if [ $? -eq 0 ]; then + echo "using ${PYTHON}" + check_installed_pip + return + fi + done + + echo "No usable python found. Please make sure to have python3.10 or newer installed." + exit 1 +} + +function update_env() { + echo_block "Updating your virtual environment" + if [ ! -f .venv/bin/activate ]; then + echo "Something went wrong, no virtual environment found." + exit 1 + fi + source .venv/bin/activate + SYS_ARCH=$(uname -m) + echo "pip install in-progress. Please wait..." + ${PYTHON} -m pip install --upgrade pip wheel setuptools + REQUIREMENTS=requirements.txt + + read -p "Do you want to install dependencies for development (Performs a full install with all dependencies) [y/N]? " + dev=$REPLY + if [[ $REPLY =~ ^[Yy]$ ]] + REQUIREMENTS=requirements-dev.txt + fi + + ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} + if [ $? -ne 0 ]; then + echo "Failed installing dependencies" + exit 1 + fi + ${PYTHON} -m pip install -e . + if [ $? -ne 0 ]; then + echo "Failed installing PySATL Experiment" + exit 1 + fi + + echo "pip install completed" + echo + if [[ $dev =~ ^[Yy]$ ]]; then + ${PYTHON} -m pre_commit install + if [ $? -ne 0 ]; then + echo "Failed installing pre-commit" + exit 1 + fi + fi +} + + +# Install bot MacOS +function install_macos() { + if [ ! -x "$(command -v brew)" ] + then + echo_block "Installing Brew" + /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" + fi + + brew install gettext libomp + + #Gets number after decimal in python version + version=$(egrep -o 3.\[0-9\]+ <<< $PYTHON | sed 's/3.//g') +} + +# Install Debian_ubuntu +function install_debian() { + sudo apt-get update + sudo apt-get install -y gcc build-essential autoconf libtool pkg-config make wget git curl $(echo lib${PYTHON}-dev ${PYTHON}-venv) +} + +# Install RedHat_CentOS +function install_redhat() { + sudo yum update + sudo yum install -y gcc gcc-c++ make autoconf libtool pkg-config wget git $(echo ${PYTHON}-devel | sed 's/\.//g') +} + +# Upgrade the +function update() { + git pull + if [ -f .env/bin/activate ]; then + # Old environment found - updating to new environment. + recreate_environments + fi + update_env + echo "Update completed." + echo_block "Don't forget to activate your virtual environment with 'source .venv/bin/activate'!" + +} + +function check_git_changes() { + if [ -z "$(git status --porcelain)" ]; then + echo "No changes in git directory" + return 1 + else + echo "Changes in git directory" + return 0 + fi +} + +function recreate_environments() { + if [ -d ".env" ]; then + # Remove old virtual env + echo "- Deleting your previous virtual env" + echo "Warning: Your new environment will be at .venv!" + rm -rf .env + fi + if [ -d ".venv" ]; then + echo "- Deleting your previous virtual env" + rm -rf .venv + fi + + echo + ${PYTHON} -m venv .venv + if [ $? -ne 0 ]; then + echo "Could not create virtual environment. Leaving now" + exit 1 + fi + +} + +# Reset Develop or Stable branch +function reset() { + echo_block "Resetting branch and virtual env" + + if [ "1" == $(git branch -vv |grep -cE "\* develop|\* stable") ] + then + if check_git_changes; then + read -p "Keep your local changes? (Otherwise will remove all changes you made!) [Y/n]? " + if [[ $REPLY =~ ^[Nn]$ ]]; then + + git fetch -a + + if [ "1" == $(git branch -vv | grep -c "* develop") ] + then + echo "- Hard resetting of 'develop' branch." + git reset --hard origin/develop + elif [ "1" == $(git branch -vv | grep -c "* stable") ] + then + echo "- Hard resetting of 'stable' branch." + git reset --hard origin/stable + fi + fi + fi + else + echo "Reset ignored because you are not on 'stable' or 'develop'." + fi + recreate_environments + + update_env +} + +function config() { + echo_block "Please use 'pysatl-experiment new-config -c user_data/config.json' to generate a new configuration file." +} + +function install() { + + echo_block "Installing mandatory dependencies" + + if [ "$(uname -s)" == "Darwin" ]; then + echo "macOS detected. Setup for this system in-progress" + install_macos + elif [ -x "$(command -v apt-get)" ]; then + echo "Debian/Ubuntu detected. Setup for this system in-progress" + install_debian + elif [ -x "$(command -v yum)" ]; then + echo "Red Hat/CentOS detected. Setup for this system in-progress" + install_redhat + else + echo "This script does not support your OS." + echo "If you have Python version 3.10 - 3.12, pip, virtualenv, ta-lib you can continue." + echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell." + sleep 10 + fi + echo + reset + config + echo_block "Run the bot !" + echo "You can now use the bot by executing 'source .venv/bin/activate; pysatl-experiment '." + echo "You can see the list of available bot sub-commands by executing 'source .venv/bin/activate; pysatl-experiment --help'." + echo "You verify that pysatl-experiment is installed successfully by running 'source .venv/bin/activate; pysatl-experiment --version'." +} + +function plot() { + echo_block "Installing dependencies for Plotting scripts" + ${PYTHON} -m pip install plotly --upgrade +} + +function help() { + echo "usage:" + echo " -i,--install Install pysatl-experiment from scratch" + echo " -u,--update Command git pull to update." + echo " -r,--reset Hard reset your develop/stable branch." + echo " -c,--config Easy config generator (Will override your existing file)." +} + +# Verify if 3.10+ is installed +check_installed_python + +case $* in +--install|-i) +install +;; +--config|-c) +config +;; +--update|-u) +update +;; +--reset|-r) +reset +;; +*) +help +;; +esac +exit 0 diff --git a/stattest/__init__.py b/stattest/__init__.py index e69de29..91cbf4a 100644 --- a/stattest/__init__.py +++ b/stattest/__init__.py @@ -0,0 +1 @@ +__version__ = "2025.3-dev" diff --git a/stattest/commands/__init__.py b/stattest/commands/__init__.py new file mode 100644 index 0000000..ee320e9 --- /dev/null +++ b/stattest/commands/__init__.py @@ -0,0 +1,11 @@ +# flake8: noqa: F401 +""" +Commands module. +Contains all start-commands, subcommands and CLI Interface creation. + +Note: Be careful with file-scoped imports in these subfiles. + as they are parsed on startup, nothing containing optional modules should be loaded. +""" + +from stattest.commands.arguments import Arguments +from stattest.commands.experiment_commands import start_experiment diff --git a/stattest/commands/arguments.py b/stattest/commands/arguments.py new file mode 100644 index 0000000..12619a7 --- /dev/null +++ b/stattest/commands/arguments.py @@ -0,0 +1,73 @@ +from argparse import ArgumentParser, Namespace, _ArgumentGroup +from typing import Any + +from stattest.commands.cli_options import AVAILABLE_CLI_OPTIONS + + +ARGS_COMMON = ["logfile", "config", "version"] + +ARGS_EXPERIMENT = [] + + +class Arguments: + """ + Arguments Class. Manage the arguments received by the cli + """ + + def __init__(self, args: list[str] | None) -> None: + self.parser = None + self.args = args + self._parsed_arg: Namespace | None = None + + def get_parsed_arg(self) -> dict[str, Any]: + """ + Return the list of arguments + :return: List[str] List of arguments + """ + if self._parsed_arg is None: + self._build_subcommands() + self._parsed_arg = self._parse_args() + + return vars(self._parsed_arg) + + def _parse_args(self) -> Namespace: + """ + Parses given arguments and returns an argparse Namespace instance. + """ + parsed_arg = self.parser.parse_args(self.args) + + return parsed_arg + + def _build_args(self, optionlist: list[str], parser: ArgumentParser | _ArgumentGroup) -> None: + for val in optionlist: + opt = AVAILABLE_CLI_OPTIONS[val] + parser.add_argument(*opt.cli, dest=val, **opt.kwargs) + + def _build_subcommands(self) -> None: + """ + Builds and attaches all subcommands. + :return: None + """ + # Build shared arguments (as group Common Options) + _common_parser = ArgumentParser(add_help=False) + group = _common_parser.add_argument_group("Common arguments") + self._build_args(optionlist=ARGS_COMMON, parser=group) + + # Build main command + self.parser = ArgumentParser(prog="statest", description="Free, open source statistic lib") + + from stattest.commands import start_experiment + + subparsers = self.parser.add_subparsers( + dest="command", + # Use custom message when no subhandler is added + # shown from `main.py` + # required=True + ) + + # Add trade subcommand + trade_cmd = subparsers.add_parser( + "experiment", help="Experiment module.", parents=[_common_parser] + ) + trade_cmd.set_defaults(func=start_experiment) + self._build_args(optionlist=ARGS_EXPERIMENT, parser=trade_cmd) diff --git a/stattest/commands/cli_options.py b/stattest/commands/cli_options.py new file mode 100644 index 0000000..3d0f49b --- /dev/null +++ b/stattest/commands/cli_options.py @@ -0,0 +1,38 @@ +class Arg: + # Optional CLI arguments + def __init__(self, *args, **kwargs): + self.cli = args + self.kwargs = kwargs + + +# List of available command line options +AVAILABLE_CLI_OPTIONS = { + # Common options + "logfile": Arg( + "--logfile", + "--log-file", + help="Log to the file specified. Special values are: 'syslog', 'journald'. " + "See the documentation for more details.", + metavar="FILE", + ), + "config": Arg( + "-c", + "--config", + help="Specify configuration file (default: `config/config.json` ", + action="append", + metavar="PATH", + ), + "version": Arg( + "-V", + "--version", + help="show program's version number and exit", + action="store_true", + ), + "version_main": Arg( + # Copy of version - used to have -V available with and without subcommand. + "-V", + "--version", + help="show program's version number and exit", + action="store_true", + ), +} diff --git a/stattest/commands/experiment_commands.py b/stattest/commands/experiment_commands.py new file mode 100644 index 0000000..224520e --- /dev/null +++ b/stattest/commands/experiment_commands.py @@ -0,0 +1,34 @@ +import logging +import signal +from typing import Any + + +logger = logging.getLogger(__name__) + + +def start_experiment(args: dict[str, Any]) -> int: + """ + Main entry point for experiment mode + """ + # Import here to avoid loading worker module when it's not used + from stattest.configuration.configuration_parser import ConfigurationParser + from stattest.experiment import Experiment + + def term_handler(signum, frame): + # Raise KeyboardInterrupt - so we can handle it in the same way as Ctrl-C + raise KeyboardInterrupt() + + # Create and run worker + try: + experiment_configurations = ConfigurationParser.parse_configs(args.get("config")) + + for experiment_configuration in experiment_configurations: + experiment = Experiment(experiment_configuration) + + # Execute experiment + experiment.execute() + + signal.signal(signal.SIGTERM, term_handler) + finally: + logger.info("calling exit") + return 0 diff --git a/stattest/configuration/__init__.py b/stattest/configuration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stattest/configuration/configuration_parser.py b/stattest/configuration/configuration_parser.py new file mode 100644 index 0000000..4adadb8 --- /dev/null +++ b/stattest/configuration/configuration_parser.py @@ -0,0 +1,154 @@ +import json +import multiprocessing +from json import JSONDecodeError +from pathlib import Path + +from stattest.experiment.configuration.configuration import ( + ExperimentConfiguration, + GeneratorConfiguration, + ReportConfiguration, + TestConfiguration, +) +from stattest.parsable import Parsable +from stattest.resolvers.builder_resolver import BuilderResolver +from stattest.resolvers.generator_resolver import GeneratorResolver +from stattest.resolvers.hypothesis_resolver import HypothesisResolver +from stattest.resolvers.iresolver import IResolver +from stattest.resolvers.listener_resolver import ListenerResolver +from stattest.resolvers.store_resolver import StoreResolver +from stattest.resolvers.test_resolver import TestResolver +from stattest.resolvers.worker_resolver import WorkerResolver + + +class ConfigurationParser: + @staticmethod + def _parse_json_class_list(resolver: IResolver, json_dicts_list: list[dict]) -> list[Parsable]: + class_list = list() + + for json_dict in json_dicts_list: + class_name = json_dict.get("name") + + class_params = json_dict.get("params") + + class_ = resolver.load(name=class_name, params=class_params) + class_list.append(class_) + + return class_list + + @staticmethod + def _parse_json_class(resolver: IResolver, json_dict: dict) -> Parsable: + class_name = json_dict["name"] + + try: + class_params = json_dict["params"] + except KeyError: + class_params = None + + class_ = resolver.load(name=class_name, params=class_params) + + return class_ + + @staticmethod + def parse_generator_config(config) -> GeneratorConfiguration: + return GeneratorConfiguration( + alternatives=ConfigurationParser._parse_json_class_list( + GeneratorResolver, config["generators"] + ), + sizes=config["sizes"], + count=config["count"], + skip_if_exists=config.get("skip_if_exists", True), + clear_before=config.get("clear_before", False), + skip_step=config.get("skip_step", False), + show_progress=config.get("show_progress", False), + threads=config.get("threads", multiprocessing.cpu_count()), + listeners=ConfigurationParser._parse_json_class_list( + ListenerResolver, config["listeners"] + ), + ) + + @staticmethod + def parse_configs(paths: list[str]) -> list[ExperimentConfiguration]: + return [ConfigurationParser.parse_config(c) for c in paths] + + @staticmethod + def parse_config(path: str) -> ExperimentConfiguration | None: + try: + # Configuring experiment + cfg_dir = Path(__file__).parent + r_path = (cfg_dir / path).resolve() + with r_path.open() as configFile: + config_data = json.load(configFile) + + default_threads = multiprocessing.cpu_count() + + generator_configuration = config_data.get("generator_configuration", {}) + alternative_configuration = ConfigurationParser.parse_generator_config( + generator_configuration + ) + + tests_config_data = config_data["test_configuration"] + + tests = ConfigurationParser._parse_json_class_list( + TestResolver, tests_config_data["tests"] + ) + test_threads = tests_config_data.get("threads", default_threads) + + tests_worker_config_data = tests_config_data["worker"] + tests_worker_params_config_data = tests_worker_config_data["params"] + + critical_value_store = ConfigurationParser._parse_json_class( + StoreResolver, tests_worker_params_config_data["cv_store"] + ) + # tests_worker_params_config_data["critical_value_store"]["params"]["db_url"] + + hypothesis = ConfigurationParser._parse_json_class( + HypothesisResolver, tests_worker_config_data["params"]["hypothesis"] + ) + + power_calculation_worker = ConfigurationParser._parse_json_class( + WorkerResolver, tests_worker_config_data + ) + power_calculation_worker.cv_store = critical_value_store + power_calculation_worker.hypothesis = hypothesis + + test_data_tels = ConfigurationParser._parse_json_class_list( + ListenerResolver, tests_config_data["listeners"] + ) + + test_configuration = TestConfiguration( + tests=tests, + threads=test_threads, + worker=power_calculation_worker, + listeners=test_data_tels, + ) + + report_data = config_data["report_configuration"] + report_builder = ConfigurationParser._parse_json_class( + BuilderResolver, report_data["report_builder"] + ) + report_listeners = ConfigurationParser._parse_json_class_list( + ListenerResolver, report_data["listeners"] + ) + report_configuration = ReportConfiguration( + report_builder=report_builder, listeners=report_listeners + ) + + rvs_store_data = config_data["rvs_store"] + rvs_store = ConfigurationParser._parse_json_class(StoreResolver, rvs_store_data) + # (db_url=rvs_store_data["params"]["db_url"])) + + result_store_data = config_data["result_store"] + result_store = ConfigurationParser._parse_json_class(StoreResolver, result_store_data) + # (db_url=result_store_data["params"]["db_url"])) + + print("Successfully parsed configuration") + return ExperimentConfiguration( + alternative_configuration=alternative_configuration, + test_configuration=test_configuration, + report_configuration=report_configuration, + rvs_store=rvs_store, + result_store=result_store, + ) + except (JSONDecodeError, TypeError, OSError, FileExistsError) as e: + print(f"Error with configuration file: {e}") + return None diff --git a/stattest/constants.py b/stattest/constants.py index 6c6169c..dae0e22 100644 --- a/stattest/constants.py +++ b/stattest/constants.py @@ -5,3 +5,10 @@ USERPATH_GENERATORS = "generators" USERPATH_HYPOTHESIS = "hypothesis" +USERPATH_LISTENERS = "listeners" +USERPATH_STORES = "stores" +USERPATH_TESTS = "tests" +USERPATH_WORKERS = "workers" +USERPATH_BUILDERS = "builders" + +DOCS_LINK = "https://pysatl-experiment.readthedocs.io/en/stable" diff --git a/stattest/experiment/configuration/__init__.py b/stattest/experiment/configuration/__init__.py index a85da85..b2482b7 100644 --- a/stattest/experiment/configuration/__init__.py +++ b/stattest/experiment/configuration/__init__.py @@ -1,6 +1,6 @@ from stattest.experiment.configuration.configuration import ( - AlternativeConfiguration, ExperimentConfiguration, + GeneratorConfiguration, ReportBuilder, ReportConfiguration, StepListener, @@ -11,7 +11,7 @@ __all__ = [ - "AlternativeConfiguration", + "GeneratorConfiguration", "ExperimentConfiguration", "ReportBuilder", "ReportConfiguration", diff --git a/stattest/experiment/configuration/config_schema.py b/stattest/experiment/configuration/config_schema.py index a9961f2..56418ab 100644 --- a/stattest/experiment/configuration/config_schema.py +++ b/stattest/experiment/configuration/config_schema.py @@ -7,7 +7,7 @@ "minimum": -1, }, "timeframe": { - "description": ("The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...)."), + "description": "The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...).", "type": "string", }, "stake_currency": { diff --git a/stattest/experiment/configuration/config_validation.py b/stattest/experiment/configuration/config_validation.py index 9490564..8fdff02 100644 --- a/stattest/experiment/configuration/config_validation.py +++ b/stattest/experiment/configuration/config_validation.py @@ -28,7 +28,7 @@ def set_defaults(validator, properties, instance, schema): return validators.extend(validator_class, {"properties": set_defaults}) -FreqtradeValidator = _extend_validator(Draft4Validator) +PysatlValidator = _extend_validator(Draft4Validator) def validate_config_schema(conf: dict[str, Any], preliminary: bool = False) -> dict[str, Any]: @@ -40,7 +40,7 @@ def validate_config_schema(conf: dict[str, Any], preliminary: bool = False) -> d """ conf_schema = deepcopy(CONF_SCHEMA) try: - FreqtradeValidator(conf_schema).validate(conf) + PysatlValidator(conf_schema).validate(conf) return conf except ValidationError as e: logger.critical(f"Invalid configuration. Reason: {e}") @@ -53,6 +53,7 @@ def validate_config_consistency(conf: dict[str, Any], *, preliminary: bool = Fal Should be run after loading both configuration and strategy, since strategies can set certain configuration settings too. :param conf: Config in JSON format + :preliminary: TODO :return: Returns None if everything is ok, otherwise throw an ConfigurationError """ diff --git a/stattest/experiment/configuration/configuration.py b/stattest/experiment/configuration/configuration.py index 601b382..b6cd1c8 100644 --- a/stattest/experiment/configuration/configuration.py +++ b/stattest/experiment/configuration/configuration.py @@ -3,6 +3,7 @@ from pysatl.criterion import AbstractStatistic from stattest.experiment.generator import AbstractRVSGenerator +from stattest.parsable import Parsable from stattest.persistence import IRvsStore from stattest.persistence.models import IResultStore @@ -11,7 +12,7 @@ class TestWorkerResult: pass -class ReportBuilder: +class ReportBuilder(Parsable): def process(self, data: TestWorkerResult): pass @@ -19,7 +20,7 @@ def build(self): pass -class StepListener: +class StepListener(Parsable): def before(self) -> None: pass @@ -55,7 +56,7 @@ def __init__( self.listeners = listeners -class AlternativeConfiguration: +class GeneratorConfiguration: def __init__( self, alternatives: Sequence[AbstractRVSGenerator], @@ -104,7 +105,7 @@ def __init__( class ExperimentConfiguration: def __init__( self, - alternative_configuration: AlternativeConfiguration, + alternative_configuration: GeneratorConfiguration, test_configuration: TestConfiguration, report_configuration: ReportConfiguration, rvs_store: IRvsStore, diff --git a/stattest/experiment/generator/__init__.py b/stattest/experiment/generator/__init__.py index 79ae331..d99fdde 100644 --- a/stattest/experiment/generator/__init__.py +++ b/stattest/experiment/generator/__init__.py @@ -2,13 +2,17 @@ BetaRVSGenerator, CauchyRVSGenerator, Chi2Generator, + ExponentialGenerator, GammaGenerator, + GompertzGenerator, GumbelGenerator, + InvGaussGenerator, LaplaceRVSGenerator, LoConNormGenerator, LogisticRVSGenerator, LognormGenerator, MixConNormGenerator, + RiceGenerator, ScConNormGenerator, TruncnormGenerator, TRVSGenerator, @@ -18,83 +22,25 @@ from stattest.experiment.generator.model import AbstractRVSGenerator -symmetric_generators = [ - BetaRVSGenerator(a=0.5, b=0.5), - BetaRVSGenerator(a=1, b=1), - BetaRVSGenerator(a=2, b=2), - CauchyRVSGenerator(t=0, s=0.5), - CauchyRVSGenerator(t=0, s=1), - CauchyRVSGenerator(t=0, s=2), - LaplaceRVSGenerator(t=0, s=1), - LogisticRVSGenerator(t=2, s=2), - TRVSGenerator(df=1), - TRVSGenerator(df=2), - TRVSGenerator(df=4), - TRVSGenerator(df=10), - TukeyRVSGenerator(lam=0.14), - TukeyRVSGenerator(lam=0.5), - TukeyRVSGenerator(lam=2), - TukeyRVSGenerator(lam=5), - TukeyRVSGenerator(lam=10), +__all__ = [ + "AbstractRVSGenerator", + "BetaRVSGenerator", + "CauchyRVSGenerator", + "Chi2Generator", + "ExponentialGenerator", + "GammaGenerator", + "GompertzGenerator", + "GumbelGenerator", + "InvGaussGenerator", + "LaplaceRVSGenerator", + "LoConNormGenerator", + "LogisticRVSGenerator", + "LognormGenerator", + "MixConNormGenerator", + "RiceGenerator", + "ScConNormGenerator", + "TruncnormGenerator", + "TRVSGenerator", + "TukeyRVSGenerator", + "WeibullGenerator", ] -asymmetric_generators = [ - BetaRVSGenerator(a=2, b=1), - BetaRVSGenerator(a=2, b=5), - BetaRVSGenerator(a=4, b=0.5), - BetaRVSGenerator(a=5, b=1), - Chi2Generator(df=1), - Chi2Generator(df=2), - Chi2Generator(df=4), - Chi2Generator(df=10), - GammaGenerator(alfa=2, beta=2), - GammaGenerator(alfa=3, beta=2), - GammaGenerator(alfa=5, beta=1), - GammaGenerator(alfa=9, beta=1), - GammaGenerator(alfa=15, beta=1), - GammaGenerator(alfa=100, beta=1), - GumbelGenerator(mu=1, beta=2), - LognormGenerator(s=1, mu=0), - WeibullGenerator(a=0.5, k=1), - WeibullGenerator(a=1, k=2), - WeibullGenerator(a=2, k=3.4), - WeibullGenerator(a=3, k=4), -] -modified_generators = [ - TruncnormGenerator(a=-1, b=1), - TruncnormGenerator(a=-2, b=2), - TruncnormGenerator(a=-3, b=3), - TruncnormGenerator(a=-3, b=1), - TruncnormGenerator(a=-3, b=2), - LoConNormGenerator(p=0.3, a=1), - LoConNormGenerator(p=0.4, a=1), - LoConNormGenerator(p=0.5, a=1), - LoConNormGenerator(p=0.3, a=3), - LoConNormGenerator(p=0.4, a=3), - LoConNormGenerator(p=0.5, a=3), - LoConNormGenerator(p=0.3, a=5), - LoConNormGenerator(p=0.4, a=5), - LoConNormGenerator(p=0.5, a=5), - ScConNormGenerator(p=0.05, b=0.25), - ScConNormGenerator(p=0.10, b=0.25), - ScConNormGenerator(p=0.20, b=0.25), - ScConNormGenerator(p=0.05, b=2), - ScConNormGenerator(p=0.10, b=2), - ScConNormGenerator(p=0.20, b=2), - ScConNormGenerator(p=0.05, b=4), - ScConNormGenerator(p=0.10, b=4), - ScConNormGenerator(p=0.20, b=4), - MixConNormGenerator(p=0.3, a=1, b=0.25), - MixConNormGenerator(p=0.4, a=1, b=0.25), - MixConNormGenerator(p=0.5, a=1, b=0.25), - MixConNormGenerator(p=0.3, a=3, b=0.25), - MixConNormGenerator(p=0.4, a=3, b=0.25), - MixConNormGenerator(p=0.5, a=3, b=0.25), - MixConNormGenerator(p=0.3, a=1, b=4), - MixConNormGenerator(p=0.4, a=1, b=4), - MixConNormGenerator(p=0.5, a=1, b=4), - MixConNormGenerator(p=0.3, a=3, b=4), - MixConNormGenerator(p=0.4, a=3, b=4), - MixConNormGenerator(p=0.5, a=3, b=4), -] - -__all__ = ["AbstractRVSGenerator"] diff --git a/stattest/experiment/generator/generator_step.py b/stattest/experiment/generator/generator_step.py index 97dc786..7596ff3 100644 --- a/stattest/experiment/generator/generator_step.py +++ b/stattest/experiment/generator/generator_step.py @@ -2,7 +2,7 @@ from multiprocessing import Queue from multiprocessing.synchronize import Event as EventClass -from stattest.experiment.configuration.configuration import AlternativeConfiguration +from stattest.experiment.configuration.configuration import GeneratorConfiguration from stattest.experiment.generator import AbstractRVSGenerator from stattest.experiment.pipeline import start_pipeline from stattest.persistence.models import IRvsStore @@ -70,7 +70,7 @@ def fill_queue( generate_shutdown_event.set() -def data_generation_step(alternative_configuration: AlternativeConfiguration, store: IRvsStore): +def data_generation_step(alternative_configuration: GeneratorConfiguration, store: IRvsStore): """ Generate data and save it to store. diff --git a/stattest/experiment/generator/model.py b/stattest/experiment/generator/model.py index a945e82..137ef44 100644 --- a/stattest/experiment/generator/model.py +++ b/stattest/experiment/generator/model.py @@ -1,4 +1,7 @@ -class AbstractRVSGenerator: +from stattest.parsable import Parsable + + +class AbstractRVSGenerator(Parsable): def __init__(self, **kwargs): pass diff --git a/stattest/experiment/hypothesis/__init__.py b/stattest/experiment/hypothesis/__init__.py index b81e1f8..bf6d1ec 100644 --- a/stattest/experiment/hypothesis/__init__.py +++ b/stattest/experiment/hypothesis/__init__.py @@ -2,4 +2,4 @@ from stattest.experiment.hypothesis.model import AbstractHypothesis -__all__ = ["NormalHypothesis", "WeibullHypothesis", "AbstractHypothesis"] +__all__ = ["AbstractHypothesis", "NormalHypothesis", "WeibullHypothesis"] diff --git a/stattest/experiment/hypothesis/model.py b/stattest/experiment/hypothesis/model.py index 3918e5a..50bb2de 100644 --- a/stattest/experiment/hypothesis/model.py +++ b/stattest/experiment/hypothesis/model.py @@ -1,7 +1,9 @@ from abc import ABC, abstractmethod +from stattest.parsable import Parsable -class AbstractHypothesis(ABC): + +class AbstractHypothesis(Parsable, ABC): @abstractmethod def generate(self, size, **kwargs): raise NotImplementedError("Method is not implemented") diff --git a/stattest/experiment/listener/__init__.py b/stattest/experiment/listener/__init__.py index e69de29..9e6d9e7 100644 --- a/stattest/experiment/listener/__init__.py +++ b/stattest/experiment/listener/__init__.py @@ -0,0 +1,4 @@ +from stattest.experiment.listener.listeners import TimeEstimationListener + + +__all__ = ["TimeEstimationListener"] diff --git a/stattest/experiment/report/__init__.py b/stattest/experiment/report/__init__.py index e69de29..08dcf6b 100644 --- a/stattest/experiment/report/__init__.py +++ b/stattest/experiment/report/__init__.py @@ -0,0 +1,4 @@ +from stattest.experiment.report.builders import ChartPowerReportBuilder, PdfPowerReportBuilder + + +__all__ = ["PdfPowerReportBuilder", "ChartPowerReportBuilder"] diff --git a/stattest/experiment/report/builders.py b/stattest/experiment/report/builders.py new file mode 100644 index 0000000..46f5a4a --- /dev/null +++ b/stattest/experiment/report/builders.py @@ -0,0 +1,55 @@ +from matplotlib import pyplot as plt + +from stattest.experiment.configuration import ReportBuilder, TestWorkerResult +from stattest.experiment.test.worker import PowerWorkerResult + + +class ChartPowerReportBuilder(ReportBuilder): + def __init__(self): + self.data = {} + + def process(self, result: TestWorkerResult): + if not isinstance(result, PowerWorkerResult): + raise TypeError(f"Type {type(result)} is not instance of PowerWorkerResult") + + key = ChartPowerReportBuilder.__build_path(result) + point = (result.size, result.power) + if key in self.data.keys(): + self.data[key].append(point) + else: + self.data[key] = [point] + + def build(self): + for key in self.data: + value = self.data[key] + sorted_value = sorted(value, key=lambda tup: tup[0]) + s = [x[0] for x in sorted_value] + p = [x[1] for x in sorted_value] + + fig, ax = plt.subplots() + ax.plot(s, p) + + ax.set( + xlabel="time (s)", + ylabel="voltage (mV)", + title="About as simple as it gets, folks", + ) + ax.grid() + + fig.savefig("test.png") + plt.show() + + @staticmethod + def __build_path(result: PowerWorkerResult): + return "_".join([result.test_code, str(result.alternative_code), str(result.alpha)]) + + +class PdfPowerReportBuilder(ReportBuilder): + def __init__(self): + self.data = {} + + def process(self, result: TestWorkerResult): + pass + + def build(self): + pass diff --git a/stattest/experiment/report/model.py b/stattest/experiment/report/model.py index b14d771..6267223 100644 --- a/stattest/experiment/report/model.py +++ b/stattest/experiment/report/model.py @@ -1,11 +1,5 @@ from typing import Any -from fpdf import FPDF -from matplotlib import pyplot as plt - -from stattest.experiment.configuration import TestWorkerResult -from stattest.experiment.configuration.configuration import ReportBuilder -from stattest.experiment.test.worker import PowerWorkerResult from stattest.persistence.models import IResultStore @@ -51,137 +45,6 @@ def __build_path(result: BenchmarkWorkerResult): """ -class ChartPowerReportBuilder(ReportBuilder): - def __init__(self): - self.data = {} - - def process(self, result: TestWorkerResult): - if not isinstance(result, PowerWorkerResult): - raise TypeError(f"Type {type(result)} is not instance of PowerWorkerResult") - - key = ChartPowerReportBuilder.__build_path(result) - point = (result.size, result.power) - if key in self.data.keys(): - self.data[key].append(point) - else: - self.data[key] = [point] - - def build(self): - for key in self.data: - value = self.data[key] - sorted_value = sorted(value, key=lambda tup: tup[0]) - s = [x[0] for x in sorted_value] - p = [x[1] for x in sorted_value] - - fig, ax = plt.subplots() - ax.plot(s, p) - - ax.set( - xlabel="time (s)", - ylabel="voltage (mV)", - title="About as simple as it gets, folks", - ) - ax.grid() - - fig.savefig("test.png") - plt.show() - - @staticmethod - def __build_path(result: PowerWorkerResult): - return "_".join([result.test_code, str(result.alternative_code), str(result.alpha)]) - - -class PdfPowerReportBuilder(ReportBuilder): - def __init__(self): - self.data = {} - self.sizes = set() - self.tests = set() - self.font = "helvetica" - self.border = 1 - self.align = "C" - self.col_width = 30 - self.header_font_size = 12 - self.entry_font_size = 10 - self.output_filename = "power_report.pdf" - - def process(self, result: TestWorkerResult): - if not isinstance(result, PowerWorkerResult): - raise TypeError(f"Type {type(result)} is not an instance of PowerWorkerResult") - - key = PdfPowerReportBuilder.__build_path(result) - self.sizes.add(result.size) - self.tests.add(result.test_code) - - if key not in self.data: - self.data[key] = {} - - if result.test_code not in self.data[key]: - self.data[key][result.test_code] = {} - - self.data[key][result.test_code][result.size] = result.power - - def build(self): - pdf = FPDF(orientation="L") - pdf.set_auto_page_break(auto=True, margin=15) - pdf.add_page() - pdf.set_font(self.font, size=self.entry_font_size) - - sorted_sizes = sorted(self.sizes) - sorted_tests = sorted(self.tests) - table_width = (len(sorted_sizes) + 1) * self.col_width - margin_x = (pdf.w - table_width) / 2 - - for key, results in self.data.items(): - pdf.set_font(self.font, "B", self.header_font_size) - pdf.cell(0, self.entry_font_size, f"{key}", ln=True, align="C") - pdf.ln(5) - - pdf.set_x(margin_x) - pdf.set_font(self.font, "B", self.entry_font_size) - pdf.cell( - self.col_width, self.entry_font_size, "Test", border=self.border, align=self.align - ) - for size in sorted_sizes: - pdf.cell( - self.col_width, - self.entry_font_size, - str(size), - border=self.border, - align=self.align, - ) - pdf.ln() - - pdf.set_font(self.font, size=self.entry_font_size) - for test in sorted_tests: - test_name = test.split("_")[0] - pdf.set_x(margin_x) - pdf.cell( - self.col_width, - self.entry_font_size, - test_name, - border=self.border, - align=self.align, - ) - for size in sorted_sizes: - power = results.get(test, {}).get(size, "N/A") - pdf.cell( - self.col_width, - self.entry_font_size, - f"{power:.3f}" if isinstance(power, float) else str(power), - border=self.border, - align=self.align, - ) - pdf.ln() - pdf.ln(self.entry_font_size) - - pdf.output(self.output_filename) - print(f"PDF report saved as: {self.output_filename}") - - @staticmethod - def __build_path(result: PowerWorkerResult): - return f"Alternative: {result.alternative_code} alpha: {result.alpha}" - - class ResultReader: def __init__(self, result_store: IResultStore, batch_size=100): self.result_store = result_store diff --git a/stattest/experiment/test/__init__.py b/stattest/experiment/test/__init__.py index e69de29..83b02ea 100644 --- a/stattest/experiment/test/__init__.py +++ b/stattest/experiment/test/__init__.py @@ -0,0 +1,4 @@ +from stattest.experiment.test.worker import PowerCalculationWorker + + +__all__ = ["PowerCalculationWorker"] diff --git a/stattest/experiment/test/worker.py b/stattest/experiment/test/worker.py index ac3eddf..2bb5599 100644 --- a/stattest/experiment/test/worker.py +++ b/stattest/experiment/test/worker.py @@ -4,6 +4,7 @@ from stattest.experiment.configuration.configuration import TestWorker, TestWorkerResult from stattest.experiment.hypothesis import AbstractHypothesis from stattest.experiment.test.power_calculation import calculate_test_power +from stattest.parsable import Parsable from stattest.persistence.models import ICriticalValueStore @@ -23,7 +24,7 @@ def __init__(self, size: int, test_code: str, benchmark: list[float]): self.test_code = test_code -class PowerCalculationWorker(TestWorker): +class PowerCalculationWorker(TestWorker, Parsable): def __init__( self, alpha: float, diff --git a/stattest/main.py b/stattest/main.py new file mode 100644 index 0000000..25db44d --- /dev/null +++ b/stattest/main.py @@ -0,0 +1,54 @@ +import logging +import sys +from typing import Any + +from stattest import __version__ +from stattest.commands.arguments import Arguments +from stattest.constants import DOCS_LINK +from stattest.exceptions import ConfigurationError, OperationalException +from stattest.system.gc_setup import gc_set_threshold +from stattest.system.version_info import print_version_info + + +logger = logging.getLogger(__name__) + + +def main(sysargv: list[str] | None = None) -> None: + return_code: Any = 1 + try: + arguments = Arguments(sysargv) + args = arguments.get_parsed_arg() + + # Call subcommand. + if args.get("version") or args.get("version_main"): + print_version_info() + return_code = 0 + elif "func" in args: + logger.info(f"PySATL Experiment {__version__}") + gc_set_threshold() + return_code = args["func"](args) + else: + # No subcommand was issued. + raise OperationalException( + "Usage of PySATL requires a subcommand to be specified.\n" + "To see the full list of options available, please use " + "`pysatl --help` or `pysatl --help`." + ) + except SystemExit as e: # pragma: no cover + return_code = e + except KeyboardInterrupt: + logger.info("SIGINT received, aborting ...") + return_code = 0 + except ConfigurationError as e: + logger.error( + f"Configuration error: {e}\n" + f"Please make sure to review the documentation at {DOCS_LINK}." + ) + except Exception: + logger.exception("Fatal exception!") + finally: + sys.exit(return_code) + + +if __name__ == "__main__": + main() diff --git a/stattest/parsable.py b/stattest/parsable.py new file mode 100644 index 0000000..f4b078c --- /dev/null +++ b/stattest/parsable.py @@ -0,0 +1,2 @@ +class Parsable: + pass diff --git a/stattest/persistence/__init__.py b/stattest/persistence/__init__.py index bbe0c11..995d57d 100644 --- a/stattest/persistence/__init__.py +++ b/stattest/persistence/__init__.py @@ -1,4 +1,14 @@ +from stattest.persistence.db_store import CriticalValueDbStore, ResultDbStore, RvsDbStore +from stattest.persistence.file_store import RvsFileStore from stattest.persistence.models import ICriticalValueStore, IRvsStore, IStore -__all__ = ["ICriticalValueStore", "IRvsStore", "IStore"] +__all__ = [ + "ICriticalValueStore", + "IRvsStore", + "IStore", + "RvsDbStore", + "ResultDbStore", + "RvsFileStore", + "CriticalValueDbStore", +] diff --git a/stattest/persistence/db_store/model.py b/stattest/persistence/db_store/model.py index 5b2dba1..8796545 100644 --- a/stattest/persistence/db_store/model.py +++ b/stattest/persistence/db_store/model.py @@ -6,9 +6,9 @@ from sqlalchemy.orm import scoped_session, sessionmaker from typing_extensions import override -from stattest.persistence import IStore from stattest.persistence.db_store.base import ModelBase, SessionType from stattest.persistence.db_store.db_init import get_request_or_thread_id, init_db +from stattest.persistence.models import IStore class AbstractDbStore(IStore, ABC): diff --git a/stattest/persistence/db_store/rvs_store.py b/stattest/persistence/db_store/rvs_store.py index f0741b0..e52ebf7 100644 --- a/stattest/persistence/db_store/rvs_store.py +++ b/stattest/persistence/db_store/rvs_store.py @@ -4,9 +4,9 @@ from sqlalchemy.orm import Mapped, mapped_column from typing_extensions import override -from stattest.persistence import IRvsStore from stattest.persistence.db_store.base import ModelBase, SessionType from stattest.persistence.db_store.model import AbstractDbStore +from stattest.persistence.models import IRvsStore class RVS(ModelBase): diff --git a/stattest/persistence/file_store/critical_value_store.py b/stattest/persistence/file_store/critical_value_store.py index ce90800..c467f4d 100644 --- a/stattest/persistence/file_store/critical_value_store.py +++ b/stattest/persistence/file_store/critical_value_store.py @@ -3,8 +3,8 @@ from typing_extensions import override -from stattest.persistence import ICriticalValueStore from stattest.persistence.file_store.store import read_json, write_json +from stattest.persistence.models import ICriticalValueStore class CriticalValueFileStore(ICriticalValueStore): diff --git a/stattest/persistence/file_store/rvs_store.py b/stattest/persistence/file_store/rvs_store.py index 5801bea..3e7a414 100644 --- a/stattest/persistence/file_store/rvs_store.py +++ b/stattest/persistence/file_store/rvs_store.py @@ -5,7 +5,7 @@ from typing_extensions import override -from stattest.persistence import IRvsStore +from stattest.persistence.models import IRvsStore class RvsFileStore(IRvsStore): diff --git a/stattest/persistence/models.py b/stattest/persistence/models.py index 223e873..6db38a3 100644 --- a/stattest/persistence/models.py +++ b/stattest/persistence/models.py @@ -1,8 +1,10 @@ from abc import ABC, abstractmethod from typing import Any +from stattest.parsable import Parsable -class IStore: + +class IStore(Parsable): def migrate(self): """ Migrate store. diff --git a/stattest/resolvers/builder_resolver.py b/stattest/resolvers/builder_resolver.py new file mode 100644 index 0000000..3f8e31e --- /dev/null +++ b/stattest/resolvers/builder_resolver.py @@ -0,0 +1,88 @@ +# pragma pylint: disable=attribute-defined-outside-init + +""" +This module load custom builders +""" + +import logging +from typing import Any + +from stattest.constants import USERPATH_BUILDERS +from stattest.exceptions import OperationalException +from stattest.experiment.configuration import ReportBuilder +from stattest.resolvers.iresolver import IResolver + + +logger = logging.getLogger(__name__) + + +class BuilderResolver(IResolver): + """ + This class contains the logic to load custom RVS generator class + """ + + object_type = ReportBuilder + object_type_str = "ReportBuilder" + user_subdir = USERPATH_BUILDERS + initial_search_path = None + extra_path = "builder_path" + module_names = ["stattest.experiment.report"] + + @staticmethod + def load( + name: str, path: str | None = None, params: dict[str, Any] | None = None + ) -> ReportBuilder: + """ + Load the custom class from config parameter + :param params: + :param path: + :param name: + """ + + builder: ReportBuilder = BuilderResolver._load(name, params=params, extra_dir=path) + + return builder + + @staticmethod + def validate(builder: ReportBuilder) -> ReportBuilder: + # Validation can be added + return builder + + @staticmethod + def _load( + builder_name: str, + params: dict[str, Any] | None, + extra_dir: str | None = None, + ) -> ReportBuilder: + """ + Search and loads the specified strategy. + :param builder_name: name of the module to import + :param extra_dir: additional directory to search for the given strategy + :return: Strategy instance or None + """ + extra_dirs = [] + + if extra_dir: + extra_dirs.append(extra_dir) + + abs_paths = BuilderResolver.build_search_paths( + user_data_dir=None, user_subdir=USERPATH_BUILDERS, extra_dirs=extra_dirs + ) + + worker = BuilderResolver._load_object( + paths=abs_paths, + object_name=builder_name, + add_source=True, + kwargs=params, + ) + + if not worker: + worker = BuilderResolver._load_modules_object(object_name=builder_name, kwargs=params) + + if worker: + return BuilderResolver.validate(worker) + + raise OperationalException( + f"Impossible to load RVS generator '{builder_name}'. This class does not exist " + "or contains Python code errors." + ) diff --git a/stattest/resolvers/generator_resolver.py b/stattest/resolvers/generator_resolver.py index 019ef83..15ae695 100644 --- a/stattest/resolvers/generator_resolver.py +++ b/stattest/resolvers/generator_resolver.py @@ -29,7 +29,7 @@ class GeneratorResolver(IResolver): module_names = ["stattest.experiment.generator"] @staticmethod - def load_generators(config: Config | None) -> list[AbstractRVSGenerator]: + def load_from_config(config: Config | None) -> list[AbstractRVSGenerator]: if not config: raise OperationalException("No configuration set. Please specify configuration.") @@ -43,37 +43,35 @@ def load_generators(config: Config | None) -> list[AbstractRVSGenerator]: alternatives = alternatives_configuration["alternatives"] generators = [] for generator_conf in alternatives: - generator = GeneratorResolver.load_generator( - generator_conf["name"], generator_conf["params"] - ) + generator = GeneratorResolver.load(generator_conf["name"], generator_conf["params"]) generators.append(generator) return generators @staticmethod - def load_generator( - generator_name: str, path: str | None = None, params: dict[str, Any] | None = None + def load( + name: str, path: str | None = None, params: dict[str, Any] | None = None ) -> AbstractRVSGenerator: """ Load the custom class from config parameter :param params: :param path: - :param generator_name: + :param name: """ - generator: AbstractRVSGenerator = GeneratorResolver._load_generator( - generator_name, params=params, extra_dir=path + generator: AbstractRVSGenerator = GeneratorResolver._load( + name, params=params, extra_dir=path ) return generator @staticmethod - def validate_generator(generator: AbstractRVSGenerator) -> AbstractRVSGenerator: + def validate(generator: AbstractRVSGenerator) -> AbstractRVSGenerator: # Validation can be added return generator @staticmethod - def _load_generator( + def _load( generator_name: str, params: dict[str, Any] | None, extra_dir: str | None = None, @@ -81,7 +79,6 @@ def _load_generator( """ Search and loads the specified strategy. :param generator_name: name of the module to import - :param config: configuration for the strategy :param extra_dir: additional directory to search for the given strategy :return: Strategy instance or None """ @@ -107,7 +104,7 @@ def _load_generator( ) if generator: - return GeneratorResolver.validate_generator(generator) + return GeneratorResolver.validate(generator) raise OperationalException( f"Impossible to load RVS generator '{generator_name}'. This class does not exist " diff --git a/stattest/resolvers/hypothesis_resolver.py b/stattest/resolvers/hypothesis_resolver.py index 38e1232..57fa13c 100644 --- a/stattest/resolvers/hypothesis_resolver.py +++ b/stattest/resolvers/hypothesis_resolver.py @@ -1,13 +1,13 @@ # pragma pylint: disable=attribute-defined-outside-init """ -This module load custom RVS generators +This module load custom hypotheses """ import logging from typing import Any -from stattest.constants import USERPATH_GENERATORS +from stattest.constants import USERPATH_HYPOTHESIS from stattest.exceptions import OperationalException from stattest.experiment.hypothesis import AbstractHypothesis from stattest.resolvers.iresolver import IResolver @@ -23,35 +23,35 @@ class HypothesisResolver(IResolver): object_type = AbstractHypothesis object_type_str = "AbstractHypothesis" - user_subdir = USERPATH_GENERATORS + user_subdir = USERPATH_HYPOTHESIS initial_search_path = None extra_path = "hypothesis_path" - module_name = "stattest.experiment.hypothesis" + module_names = ["stattest.experiment.hypothesis"] @staticmethod - def load_hypothesis( - hypothesis_name: str, path: str | None = None, params: dict[str, Any] | None = None + def load( + name: str, path: str | None = None, params: dict[str, Any] | None = None ) -> AbstractHypothesis: """ Load the custom class from config parameter :param params: :param path: - :param hypothesis_name: + :param name: """ - hypothesis: AbstractHypothesis = HypothesisResolver._load_hypothesis( - hypothesis_name, params=params, extra_dir=path + hypothesis: AbstractHypothesis = HypothesisResolver._load( + name, params=params, extra_dir=path ) return hypothesis @staticmethod - def validate_hypothesis(generator: AbstractHypothesis) -> AbstractHypothesis: + def validate(hypothesis: AbstractHypothesis) -> AbstractHypothesis: # Validation can be added - return generator + return hypothesis @staticmethod - def _load_hypothesis( + def _load( hypothesis_name: str, params: dict[str, Any] | None, extra_dir: str | None = None, @@ -59,7 +59,6 @@ def _load_hypothesis( """ Search and loads the specified strategy. :param hypothesis_name: name of the module to import - :param config: configuration for the strategy :param extra_dir: additional directory to search for the given strategy :return: Strategy instance or None """ @@ -69,7 +68,7 @@ def _load_hypothesis( extra_dirs.append(extra_dir) abs_paths = HypothesisResolver.build_search_paths( - user_data_dir=None, user_subdir=USERPATH_GENERATORS, extra_dirs=extra_dirs + user_data_dir=None, user_subdir=USERPATH_HYPOTHESIS, extra_dirs=extra_dirs ) hypothesis = HypothesisResolver._load_object( @@ -80,12 +79,12 @@ def _load_hypothesis( ) if not hypothesis: - hypothesis = HypothesisResolver._load_module_object( - object_name=hypothesis_name, kwargs=params, module_name="" + hypothesis = HypothesisResolver._load_modules_object( + object_name=hypothesis_name, kwargs=params ) if hypothesis: - return HypothesisResolver.validate_hypothesis(hypothesis) + return HypothesisResolver.validate(hypothesis) raise OperationalException( f"Impossible to load RVS hypothesis '{hypothesis_name}'. This class does not exist " diff --git a/stattest/resolvers/iresolver.py b/stattest/resolvers/iresolver.py index c580404..9a5be83 100644 --- a/stattest/resolvers/iresolver.py +++ b/stattest/resolvers/iresolver.py @@ -172,6 +172,9 @@ def _load_object( Try to load object from path list. """ + if kwargs is None: + kwargs = {} + for _path in paths: try: (module, module_path) = cls._search_object( @@ -211,6 +214,8 @@ def _load_module_object( """ Try to load object from path list. """ + if kwargs is None: + kwargs = {} try: module = getattr(importlib.import_module(module_name), object_name) @@ -238,6 +243,7 @@ def load_object( Search and loads the specified object as configured in the child class. :param object_name: name of the module to import :param config: configuration dictionary + :param kwargs: some additional parameters :param extra_dir: additional directory to search for the given pairlist :raises: OperationalException if the class is invalid or does not exist. :return: Object instance or None @@ -316,3 +322,11 @@ def _search_all_objects( } ) return objects + + @staticmethod + def load(name: str, path: str | None = None, params: dict[str, Any] | None = None): + raise Exception("Not implemented") + + @staticmethod + def validate(resolver: Any): # TODO: do validation properly + raise Exception("Not implemented") diff --git a/stattest/resolvers/listener_resolver.py b/stattest/resolvers/listener_resolver.py new file mode 100644 index 0000000..08ae0eb --- /dev/null +++ b/stattest/resolvers/listener_resolver.py @@ -0,0 +1,90 @@ +# pragma pylint: disable=attribute-defined-outside-init + +""" +This module load custom listeners +""" + +import logging +from typing import Any + +from stattest.constants import USERPATH_LISTENERS +from stattest.exceptions import OperationalException +from stattest.experiment.configuration import StepListener +from stattest.resolvers.iresolver import IResolver + + +logger = logging.getLogger(__name__) + + +class ListenerResolver(IResolver): + """ + This class contains the logic to load custom RVS generator class + """ + + object_type = StepListener + object_type_str = "StepListener" + user_subdir = USERPATH_LISTENERS + initial_search_path = None + extra_path = "listener_path" + module_names = ["stattest.experiment.listener"] + + @staticmethod + def load( + name: str, path: str | None = None, params: dict[str, Any] | None = None + ) -> StepListener: + """ + Load the custom class from config parameter + :param params: + :param path: + :param name: + """ + + listener: StepListener = ListenerResolver._load(name, params=params, extra_dir=path) + + return listener + + @staticmethod + def validate(listener: StepListener) -> StepListener: + # Validation can be added + return listener + + @staticmethod + def _load( + listener_name: str, + params: dict[str, Any] | None, + extra_dir: str | None = None, + ) -> StepListener: + """ + Search and loads the specified strategy. + :param listener_name: name of the module to import + :param extra_dir: additional directory to search for the given strategy + :return: Strategy instance or None + """ + extra_dirs = [] + + if extra_dir: + extra_dirs.append(extra_dir) + + abs_paths = ListenerResolver.build_search_paths( + user_data_dir=None, user_subdir=USERPATH_LISTENERS, extra_dirs=extra_dirs + ) + + listener = ListenerResolver._load_object( + paths=abs_paths, + object_name=listener_name, + add_source=True, + kwargs=params, + ) + + if not listener: + listener = ListenerResolver._load_modules_object( + object_name=listener_name, kwargs=params + ) + + if listener: + return ListenerResolver.validate(listener) + + raise OperationalException( + f"Impossible to load RVS generator '{listener_name}'. This class does not exist " + "or contains Python code errors." + ) diff --git a/stattest/resolvers/store_resolver.py b/stattest/resolvers/store_resolver.py new file mode 100644 index 0000000..ceb3cad --- /dev/null +++ b/stattest/resolvers/store_resolver.py @@ -0,0 +1,86 @@ +# pragma pylint: disable=attribute-defined-outside-init + +""" +This module load custom RVS stores +""" + +import logging +from typing import Any + +from stattest.constants import USERPATH_STORES +from stattest.exceptions import OperationalException +from stattest.persistence import IStore +from stattest.resolvers.iresolver import IResolver + + +logger = logging.getLogger(__name__) + + +class StoreResolver(IResolver): + """ + This class contains the logic to load custom RVS generator class + """ + + object_type = IStore + object_type_str = "IStore" + user_subdir = USERPATH_STORES + initial_search_path = None + extra_path = "store_path" + module_names = ["stattest.persistence"] + + @staticmethod + def load(name: str, path: str | None = None, params: dict[str, Any] | None = None) -> IStore: + """ + Load the custom class from config parameter + :param params: + :param path: + :param name: + """ + + store: IStore = StoreResolver._load(name, params=params, extra_dir=path) + + return store + + @staticmethod + def validate(store: IStore) -> IStore: + # Validation can be added + return store + + @staticmethod + def _load( + store_name: str, + params: dict[str, Any] | None, + extra_dir: str | None = None, + ) -> IStore: + """ + Search and loads the specified strategy. + :param store_name: name of the module to import + :param extra_dir: additional directory to search for the given strategy + :return: Strategy instance or None + """ + extra_dirs = [] + + if extra_dir: + extra_dirs.append(extra_dir) + + abs_paths = StoreResolver.build_search_paths( + user_data_dir=None, user_subdir=USERPATH_STORES, extra_dirs=extra_dirs + ) + + store = StoreResolver._load_object( + paths=abs_paths, + object_name=store_name, + add_source=True, + kwargs=params, + ) + + if not store: + store = StoreResolver._load_modules_object(object_name=store_name, kwargs=params) + + if store: + return StoreResolver.validate(store) + + raise OperationalException( + f"Impossible to load RVS generator '{store_name}'. This class does not exist " + "or contains Python code errors." + ) diff --git a/stattest/resolvers/test_resolver.py b/stattest/resolvers/test_resolver.py new file mode 100644 index 0000000..f74a3d8 --- /dev/null +++ b/stattest/resolvers/test_resolver.py @@ -0,0 +1,89 @@ +# pragma pylint: disable=attribute-defined-outside-init + +""" +This module load custom statistic tests +""" + +import logging +from typing import Any + +from pysatl.criterion import AbstractStatistic + +from stattest.constants import USERPATH_TESTS +from stattest.exceptions import OperationalException +from stattest.resolvers.iresolver import IResolver + + +logger = logging.getLogger(__name__) + + +class TestResolver(IResolver): + """ + This class contains the logic to load custom RVS generator class + """ + + object_type = AbstractStatistic + object_type_str = "AbstractTestStatistic" + user_subdir = USERPATH_TESTS + initial_search_path = None + extra_path = "test_path" + module_names = ["pysatl.criterion"] + + @staticmethod + def load( + name: str, path: str | None = None, params: dict[str, Any] | None = None + ) -> AbstractStatistic: + """ + Load the custom class from config parameter + :param params: + :param path: + :param name: + """ + + test: AbstractStatistic = TestResolver._load(name, params=params, extra_dir=path) + + return test + + @staticmethod + def validate(test: AbstractStatistic) -> AbstractStatistic: + # Validation can be added + return test + + @staticmethod + def _load( + test_name: str, + params: dict[str, Any] | None, + extra_dir: str | None = None, + ) -> AbstractStatistic: + """ + Search and loads the specified strategy. + :param test_name: name of the module to import + :param extra_dir: additional directory to search for the given strategy + :return: Strategy instance or None + """ + extra_dirs = [] + + if extra_dir: + extra_dirs.append(extra_dir) + + abs_paths = TestResolver.build_search_paths( + user_data_dir=None, user_subdir=USERPATH_TESTS, extra_dirs=extra_dirs + ) + + test = TestResolver._load_object( + paths=abs_paths, + object_name=test_name, + add_source=True, + kwargs=params, + ) + + if not test: + test = TestResolver._load_modules_object(object_name=test_name, kwargs=params) + + if test: + return TestResolver.validate(test) + + raise OperationalException( + f"Impossible to load RVS generator '{test_name}'. This class does not exist " + "or contains Python code errors." + ) diff --git a/stattest/resolvers/worker_resolver.py b/stattest/resolvers/worker_resolver.py new file mode 100644 index 0000000..0cbba34 --- /dev/null +++ b/stattest/resolvers/worker_resolver.py @@ -0,0 +1,88 @@ +# pragma pylint: disable=attribute-defined-outside-init + +""" +This module load custom workers +""" + +import logging +from typing import Any + +from stattest.constants import USERPATH_WORKERS +from stattest.exceptions import OperationalException +from stattest.experiment.test.worker import PowerCalculationWorker +from stattest.resolvers.iresolver import IResolver + + +logger = logging.getLogger(__name__) + + +class WorkerResolver(IResolver): + """ + This class contains the logic to load custom RVS generator class + """ + + object_type = PowerCalculationWorker + object_type_str = "PowerCalculationWorker" + user_subdir = USERPATH_WORKERS + initial_search_path = None + extra_path = "worker_path" + module_names = ["stattest.experiment.test"] + + @staticmethod + def load( + name: str, path: str | None = None, params: dict[str, Any] | None = None + ) -> PowerCalculationWorker: + """ + Load the custom class from config parameter + :param params: + :param path: + :param name: + """ + + worker: PowerCalculationWorker = WorkerResolver._load(name, params=params, extra_dir=path) + + return worker + + @staticmethod + def validate(worker: PowerCalculationWorker) -> PowerCalculationWorker: + # Validation can be added + return worker + + @staticmethod + def _load( + worker_name: str, + params: dict[str, Any] | None, + extra_dir: str | None = None, + ) -> PowerCalculationWorker: + """ + Search and loads the specified strategy. + :param worker_name: name of the module to import + :param extra_dir: additional directory to search for the given strategy + :return: Strategy instance or None + """ + extra_dirs = [] + + if extra_dir: + extra_dirs.append(extra_dir) + + abs_paths = WorkerResolver.build_search_paths( + user_data_dir=None, user_subdir=USERPATH_WORKERS, extra_dirs=extra_dirs + ) + + worker = WorkerResolver._load_object( + paths=abs_paths, + object_name=worker_name, + add_source=True, + kwargs=params, + ) + + if not worker: + worker = WorkerResolver._load_modules_object(object_name=worker_name, kwargs=params) + + if worker: + return WorkerResolver.validate(worker) + + raise OperationalException( + f"Impossible to load RVS generator '{worker_name}'. This class does not exist " + "or contains Python code errors." + ) diff --git a/stattest/system/__init__.py b/stattest/system/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stattest/system/gc_setup.py b/stattest/system/gc_setup.py new file mode 100644 index 0000000..a3532cb --- /dev/null +++ b/stattest/system/gc_setup.py @@ -0,0 +1,18 @@ +import gc +import logging +import platform + + +logger = logging.getLogger(__name__) + + +def gc_set_threshold(): + """ + Reduce number of GC runs to improve performance (explanation video) + https://www.youtube.com/watch?v=p4Sn6UcFTOU + + """ + if platform.python_implementation() == "CPython": + # allocs, g1, g2 = gc.get_threshold() + gc.set_threshold(50_000, 500, 1000) + logger.debug("Adjusting python allocations to reduce GC runs") diff --git a/stattest/system/version_info.py b/stattest/system/version_info.py new file mode 100644 index 0000000..2638a08 --- /dev/null +++ b/stattest/system/version_info.py @@ -0,0 +1,15 @@ +from stattest import __version__ + + +def print_version_info(): + """Print version information for pysatl-experiment and its key dependencies.""" + import platform + import sys + + import ccxt + + print(f"Operating System:\t{platform.platform()}") + print(f"Python Version:\t\tPython {sys.version.split(' ')[0]}") + print(f"CCXT Version:\t\t{ccxt.__version__}") + print() + print(f"PySATL Experiment Version:\tpysatl-experiment {__version__}") diff --git a/tests/configuration/configuration_test.py b/tests/configuration/configuration_test.py new file mode 100644 index 0000000..1d5b9b1 --- /dev/null +++ b/tests/configuration/configuration_test.py @@ -0,0 +1,18 @@ +import pytest + +from stattest.configuration.configuration_parser import ConfigurationParser +from stattest.experiment import Experiment + + +@pytest.mark.parametrize( + "path", + [ + "../../config_examples/config_example.json", + ], +) +def test_load_with_params(path): + config = ConfigurationParser.parse_config(path) + assert config is not None + + experiment = Experiment(configuration=config) + assert experiment is not None diff --git a/tests/resolvers/builder_resolver_tests.py b/tests/resolvers/builder_resolver_tests.py new file mode 100644 index 0000000..f7917d0 --- /dev/null +++ b/tests/resolvers/builder_resolver_tests.py @@ -0,0 +1,14 @@ +import pytest + +from stattest.experiment.report import PdfPowerReportBuilder +from stattest.resolvers.builder_resolver import BuilderResolver + + +@pytest.mark.parametrize( + ("name", "expected"), + [("PdfPowerReportBuilder", PdfPowerReportBuilder)], +) +def test_load_without_params(name, expected): + builder = BuilderResolver.load(name) + + assert builder is not None diff --git a/tests/resolvers/config/generators/generator_test_with_default_params.py b/tests/resolvers/config/generators/generator_test_with_default_params.py new file mode 100644 index 0000000..e3d0746 --- /dev/null +++ b/tests/resolvers/config/generators/generator_test_with_default_params.py @@ -0,0 +1,15 @@ +from stattest.core.distribution.beta import generate_beta +from stattest.experiment.generator import AbstractRVSGenerator + + +class GeneratorTest(AbstractRVSGenerator): + def __init__(self, a=1, b=2, **kwargs): + super().__init__(**kwargs) + self.a = a + self.b = b + + def code(self): + return super()._convert_to_code(["generator_test", self.a, self.b]) + + def generate(self, size): + return generate_beta(size=size, a=self.a, b=self.b) diff --git a/tests/resolvers/config/generators/generator_test_without_params.py b/tests/resolvers/config/generators/generator_test_without_params.py new file mode 100644 index 0000000..31acd41 --- /dev/null +++ b/tests/resolvers/config/generators/generator_test_without_params.py @@ -0,0 +1,15 @@ +from stattest.core.distribution.beta import generate_beta +from stattest.experiment.generator import AbstractRVSGenerator + + +class GeneratorTestWithoutParams(AbstractRVSGenerator): + def __init__(self, a, b, **kwargs): + super().__init__(**kwargs) + self.a = a + self.b = b + + def code(self): + return super()._convert_to_code(["generator_test_without", self.a, self.b]) + + def generate(self, size): + return generate_beta(size=size, a=self.a, b=self.b) diff --git a/tests/resolvers/generator_resolver_tests.py b/tests/resolvers/generator_resolver_tests.py new file mode 100644 index 0000000..f4980ef --- /dev/null +++ b/tests/resolvers/generator_resolver_tests.py @@ -0,0 +1,35 @@ +from pathlib import Path + +import pytest + +from stattest.resolvers.generator_resolver import GeneratorResolver + + +@pytest.mark.parametrize( + ("name", "params", "expected"), + [ + ("CauchyRVSGenerator", {"t": 1, "s": 3}, "cauchy_1_3"), + ("LognormGenerator", {"s": 1, "mu": 3}, "lognorm_1_3"), + ("BetaRVSGenerator", {"a": 2, "b": 3}, "beta_2_3"), + ], +) +def test_load_with_params(name, params, expected): + generator = GeneratorResolver.load(name, None, params) + assert generator is not None + assert generator.code() == expected + + +def test_load_from_file_with_default_params(): + default_location = Path(__file__).parent / "config/generators" + generator = GeneratorResolver.load("GeneratorTest", str(default_location)) + assert generator is not None + assert "generator_test_1_2" == generator.code() + + +def test_load_from_file_without_default_params(): + default_location = Path(__file__).parent / "config/generators" + generator = GeneratorResolver.load( + "GeneratorTestWithoutParams", str(default_location), {"a": 3, "b": 2} + ) + assert generator is not None + assert "generator_test_without_3_2" == generator.code() diff --git a/tests/resolvers/hypothesis_resolver_tests.py b/tests/resolvers/hypothesis_resolver_tests.py new file mode 100644 index 0000000..349343b --- /dev/null +++ b/tests/resolvers/hypothesis_resolver_tests.py @@ -0,0 +1,18 @@ +import pytest + +from stattest.experiment.hypothesis import NormalHypothesis, WeibullHypothesis +from stattest.resolvers.hypothesis_resolver import HypothesisResolver + + +@pytest.mark.parametrize( + ("name", "expected"), + [ + ("NormalHypothesis", NormalHypothesis), + ("WeibullHypothesis", WeibullHypothesis), + ], +) +def test_load_without_params(name, expected): + hypothesis = HypothesisResolver.load(name) + + assert hypothesis is not None + assert type(hypothesis) is expected diff --git a/tests/resolvers/listener_resolver_tests.py b/tests/resolvers/listener_resolver_tests.py new file mode 100644 index 0000000..c31e2c7 --- /dev/null +++ b/tests/resolvers/listener_resolver_tests.py @@ -0,0 +1,15 @@ +import pytest + +from stattest.experiment.listener import TimeEstimationListener +from stattest.resolvers.listener_resolver import ListenerResolver + + +@pytest.mark.parametrize( + ("name", "expected"), + [("TimeEstimationListener", TimeEstimationListener)], +) +def test_load_without_params(name, expected): + listener = ListenerResolver.load(name) + + assert listener is not None + assert type(listener) is expected diff --git a/tests/resolvers/store_resolver_tests.py b/tests/resolvers/store_resolver_tests.py new file mode 100644 index 0000000..a632dd3 --- /dev/null +++ b/tests/resolvers/store_resolver_tests.py @@ -0,0 +1,20 @@ +import pytest + +from stattest.persistence import CriticalValueDbStore, ResultDbStore, RvsDbStore +from stattest.resolvers.store_resolver import StoreResolver + + +@pytest.mark.parametrize( + ("name", "expected"), + [ + ("RvsDbStore", RvsDbStore), + ("CriticalValueDbStore", CriticalValueDbStore), + ("ResultDbStore", ResultDbStore), + ], +) +def test_load_without_params(name, expected): + # Always load stores with params! + worker = StoreResolver.load(name) + + assert worker is not None + # assert type(worker) is expected diff --git a/tests/resolvers/test_resolver_tests.py b/tests/resolvers/test_resolver_tests.py new file mode 100644 index 0000000..17d0bac --- /dev/null +++ b/tests/resolvers/test_resolver_tests.py @@ -0,0 +1,29 @@ +import pytest +from pysatl.criterion import ( + AndersonDarlingWeibullGofStatistic, + Chi2PearsonWeibullGofStatistic, + CrammerVonMisesWeibullGofStatistic, + KolmogorovSmirnovWeibullGofStatistic, + LillieforsWeibullGofStatistic, + LOSWeibullGofStatistic, +) + +from stattest.resolvers.test_resolver import TestResolver + + +@pytest.mark.parametrize( + ("name", "expected"), + [ + ("AndersonDarlingWeibullGofStatistic", AndersonDarlingWeibullGofStatistic), + ("Chi2PearsonWeibullGofStatistic", Chi2PearsonWeibullGofStatistic), + ("CrammerVonMisesWeibullGofStatistic", CrammerVonMisesWeibullGofStatistic), + ("KolmogorovSmirnovWeibullGofStatistic", KolmogorovSmirnovWeibullGofStatistic), + ("LillieforsWeibullGofStatistic", LillieforsWeibullGofStatistic), + ("LOSWeibullGofStatistic", LOSWeibullGofStatistic), + ], +) +def test_load_without_params(name, expected): + test = TestResolver.load(name) + + assert test is not None + assert type(test) is expected diff --git a/tests/resolvers/worker_resolver_tests.py b/tests/resolvers/worker_resolver_tests.py new file mode 100644 index 0000000..83cbab8 --- /dev/null +++ b/tests/resolvers/worker_resolver_tests.py @@ -0,0 +1,16 @@ +from stattest.resolvers.worker_resolver import WorkerResolver + + +def test_load_power_calculation_worker(): + params = { + "alpha": 0.05, + "monte_carlo_count": 100000, + "cv_store": { + "name": "CriticalValueDbStore", + "params": {"db_url": "sqlite:///weibull_experiment.sqlite"}, + }, + "hypothesis": {"name": "WeibullHypothesis"}, + } + worker = WorkerResolver.load("PowerCalculationWorker", None, params) + + assert worker is not None