diff --git a/CHANGES.md b/CHANGES.md index 5c74c68..6adae4a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,54 @@ # Changelog + + +## [1.3.1] - 2023-09-26 + +### Changed + +- feat(database): simplify launcher database implementation [`#66`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/66) +- feat(cli): add the `--solver-version` option to the command line [`#63`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/63) +- feat(parameters): handle the `--partition` and `--qos` parameters for the `sbatch` command [`#58`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/58) +- feat(retrival): correct the retrival of remote files and improve exception handling to avoid infinite loops [`88efc98`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/88efc98af6a8fd494f07cc9a366a52109eb3ac2d) +- feat(zip-extractor): the uncompress directory is calculated according to the content: study directory or simulation output [`1ffc86e`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/1ffc86e0439814e4549f59c193731c71080c0d59) + +### Fixes + +- fix(cli): preserve backward compatibility in CLI options [`#65`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/65) +- fix(job-state): consider the `COMPLETING` value as a possible job state [`#61`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/61) +- fix(results-retrieval): handle exceptions in log and ZIP result retrival [`#60`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/60) +- fix(console): use the ISO8601 date format to display messages on the console [`0dbf971`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/0dbf971b1ccc924f4b11cf44b0e0cf16562622c9) + +### Refactorings + +- refactor: remove `IDisplay` abstract class [`#64`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/64) +- refactor(launch-controller): simplification of the `LaunchController` class [`4c07551`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/4c07551ae8acf15d784553e7877b9017626b306b) +- refactor(file-manager): remove unused or trivial methods from `FileManager` [`fbb60e0`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/fbb60e0efca6989e7ea79324ed746b55da3cfb3d) +- refactoring(file-manager): drop the `FileManager` class [`9797799`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/9797799df6bf4fd626ea1bc997d11503989d5b94) +- refactoring(tree-structure): drop the `TreeStructureInitializer` class [`8a119af`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/8a119afb06d64f0ccd1e112ef82367f8fdee7ce0) +- refactoring(data-provider): drop the `DataProvider` class [`272965e`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/272965ed618f94ecf0de718bf7e8e0788c4bbb3a) + +### Code Style + +- style: reformat source code using iSort and Black [`e243fba`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/e243fbab177c46ffc867440b3701d7672566066c) + +### Chore + +- chore(typing): improve the typing of study parameters [`f11641d`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/f11641d4d233d61f91b9cbebf6263780ff14eb88) +- chore(typing): improve typing in source code [`4ff6abf`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/4ff6abf512b03944d0132d868484ef2d677c8b77) +- chore: replace `COMPETING` with `COMPLETING` (typo) [`e98b7a8`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/e98b7a8627b09a883e48a9b4b883f6b1560da0e9) + +### Tests + +- test: correct the test fixtures for study retrival [`6f78bd6`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/6f78bd62a5f7c6b61a6fcb4a9a42c7710e986301) + + ## [1.3.0] - 2023-06-16 ### Changed @@ -95,6 +144,8 @@ - Remove unnecessary Optional - Enable ssh_config_file to be `None` +[1.3.1]: https://github.com/AntaresSimulatorTeam/antares-launcher/releases/tag/v1.3.1 + [1.3.0]: https://github.com/AntaresSimulatorTeam/antares-launcher/releases/tag/v1.3.0 [1.2.4]: https://github.com/AntaresSimulatorTeam/antares-launcher/releases/tag/v1.2.4 diff --git a/antareslauncher/__init__.py b/antareslauncher/__init__.py index 9ce0ab1..250662a 100644 --- a/antareslauncher/__init__.py +++ b/antareslauncher/__init__.py @@ -9,9 +9,9 @@ # Standard project metadata -__version__ = "1.3.0" +__version__ = "1.3.1" __author__ = "RTE, Antares Web Team" -__date__ = "2023-06-16" +__date__ = "2023-09-26" # noinspection SpellCheckingInspection __credits__ = "(c) Réseau de Transport de l’Électricité (RTE)" @@ -19,7 +19,7 @@ __project_name__ = "Antares_Launcher" -def _check_metadata(): +def _check_metadata() -> None: # noinspection SpellCheckingInspection """ Check the project metadata. diff --git a/antareslauncher/advanced_launch.py b/antareslauncher/advanced_launch.py index 6fc2f61..bcb23aa 100644 --- a/antareslauncher/advanced_launch.py +++ b/antareslauncher/advanced_launch.py @@ -1,3 +1,4 @@ +import sys from pathlib import Path from antareslauncher.config import Config, get_config_path @@ -6,7 +7,7 @@ from antareslauncher.parameters_reader import ParametersReader -def main(): +def main() -> None: config_path: Path = get_config_path() config = Config.load_config(config_path) param_reader = ParametersReader( @@ -15,8 +16,17 @@ def main(): ) parser_parameters: ParserParameters = param_reader.get_parser_parameters() parser: MainOptionParser = MainOptionParser(parser_parameters) - parser.add_basic_arguments().add_advanced_arguments() - arguments = parser.parse_args() + parser.add_basic_arguments(antares_versions=param_reader.antares_versions) + ssh_config_required = parser_parameters.ssh_config_file_is_required + alt_ssh_paths = [ + parser_parameters.ssh_configfile_path_alternate1, + parser_parameters.ssh_configfile_path_alternate1, + ] + parser.add_advanced_arguments( + ssh_config_required=ssh_config_required, + alt_ssh_paths=alt_ssh_paths, + ) + arguments = parser.parser.parse_args(sys.argv[1:]) main_parameters: MainParameters = param_reader.get_main_parameters() run_with(arguments=arguments, parameters=main_parameters, show_banner=True) diff --git a/antareslauncher/antares_launcher.py b/antareslauncher/antares_launcher.py index 1e8abdb..c483555 100644 --- a/antareslauncher/antares_launcher.py +++ b/antareslauncher/antares_launcher.py @@ -1,22 +1,12 @@ from dataclasses import dataclass from typing import Optional -from antareslauncher.use_cases.check_remote_queue.check_queue_controller import ( - CheckQueueController, -) -from antareslauncher.use_cases.create_list.study_list_composer import ( - StudyListComposer, -) -from antareslauncher.use_cases.kill_job.job_kill_controller import ( - JobKillController, -) +from antareslauncher.use_cases.check_remote_queue.check_queue_controller import CheckQueueController +from antareslauncher.use_cases.create_list.study_list_composer import StudyListComposer +from antareslauncher.use_cases.kill_job.job_kill_controller import JobKillController from antareslauncher.use_cases.launch.launch_controller import LaunchController -from antareslauncher.use_cases.retrieve.retrieve_controller import ( - RetrieveController, -) -from antareslauncher.use_cases.wait_loop_controller.wait_controller import ( - WaitController, -) +from antareslauncher.use_cases.retrieve.retrieve_controller import RetrieveController +from antareslauncher.use_cases.wait_loop_controller.wait_controller import WaitController @dataclass @@ -29,7 +19,7 @@ class AntaresLauncher: wait_controller: WaitController wait_mode: bool wait_time: int - xpansion_mode: Optional[str] + xpansion_mode: str check_queue_bool: bool job_id_to_kill: Optional[int] = None @@ -47,7 +37,7 @@ def run_once_mode(self): def run_wait_mode(self): """Run antares_launcher once then it keeps checking the status of the unfinished jobs until all jobs are finished, - The code exit when all job are finished the the results are retrieved and extracted + The code exits when all jobs are finished, the results are retrieved and extracted """ self.run_once_mode() while not self.retrieve_controller.all_studies_done: diff --git a/antareslauncher/basic_launch.py b/antareslauncher/basic_launch.py index 9b45481..4b734f6 100644 --- a/antareslauncher/basic_launch.py +++ b/antareslauncher/basic_launch.py @@ -1,3 +1,4 @@ +import sys from pathlib import Path from antareslauncher.config import Config, get_config_path @@ -6,7 +7,7 @@ from antareslauncher.parameters_reader import ParametersReader -def main(): +def main() -> None: config_path: Path = get_config_path() config = Config.load_config(config_path) param_reader = ParametersReader( @@ -15,8 +16,8 @@ def main(): ) parser_parameters: ParserParameters = param_reader.get_parser_parameters() parser: MainOptionParser = MainOptionParser(parser_parameters) - parser.add_basic_arguments() - arguments = parser.parse_args() + parser.add_basic_arguments(antares_versions=param_reader.antares_versions) + arguments = parser.parser.parse_args(sys.argv[1:]) main_parameters: MainParameters = param_reader.get_main_parameters() run_with(arguments=arguments, parameters=main_parameters, show_banner=True) diff --git a/antareslauncher/config.py b/antareslauncher/config.py index 5fe9342..e27a5fc 100644 --- a/antareslauncher/config.py +++ b/antareslauncher/config.py @@ -12,11 +12,7 @@ import yaml from antareslauncher import __author__, __project_name__, __version__ -from antareslauncher.exceptions import ( - InvalidConfigValueError, - UnknownFileSuffixError, - ConfigFileNotFoundError, -) +from antareslauncher.exceptions import ConfigFileNotFoundError, InvalidConfigValueError, UnknownFileSuffixError APP_NAME = __project_name__ APP_AUTHOR = __author__.split(",")[0] @@ -119,9 +115,7 @@ def load_config(cls, ssh_config_path: pathlib.Path) -> "SSHConfig": obj = parse_config(ssh_config_path) kwargs = {k.lower(): v for k, v in obj.items()} private_key_file = kwargs.pop("private_key_file", None) - kwargs["private_key_file"] = ( - None if private_key_file is None else pathlib.Path(private_key_file) - ) + kwargs["private_key_file"] = None if private_key_file is None else pathlib.Path(private_key_file) try: return cls(config_path=ssh_config_path, **kwargs) except TypeError as exc: @@ -139,11 +133,7 @@ def save_config(self, ssh_config_path: pathlib.Path) -> None: """ obj = dataclasses.asdict(self) del obj["config_path"] - obj = { - k: v - for k, v in obj.items() - if v or k not in {"private_key_file", "key_password", "password"} - } + obj = {k: v for k, v in obj.items() if v or k not in {"private_key_file", "key_password", "password"}} if "private_key_file" in obj: obj["private_key_file"] = obj["private_key_file"].as_posix() dump_config(ssh_config_path, obj) @@ -212,9 +202,7 @@ def load_config(cls, config_path: pathlib.Path) -> "Config": obj = parse_config(config_path) kwargs = {k.lower(): v for k, v in obj.items()} try: - kwargs["remote_solver_versions"] = kwargs.pop( - "antares_versions_on_remote_server" - ) + kwargs["remote_solver_versions"] = kwargs.pop("antares_versions_on_remote_server") # handle paths for key in [ "log_dir", @@ -226,9 +214,7 @@ def load_config(cls, config_path: pathlib.Path) -> "Config": kwargs[key] = pathlib.Path(kwargs[key]) ssh_configfile_name = kwargs.pop("default_ssh_configfile_name") except KeyError as exc: - raise InvalidConfigValueError( - config_path, f"missing parameter '{exc}'" - ) from None + raise InvalidConfigValueError(config_path, f"missing parameter '{exc}'") from None # handle SSH configuration config_dir = config_path.parent ssh_config_path = config_dir.joinpath(ssh_configfile_name) @@ -286,10 +272,9 @@ def get_user_config_dir(system: str = ""): """ username = getpass.getuser() system = system or sys.platform + config_dir: pathlib.Path if system == "win32": - config_dir = pathlib.WindowsPath( - rf"C:\Users\{username}\AppData\Local\{APP_AUTHOR}" - ) + config_dir = pathlib.WindowsPath(rf"C:\Users\{username}\AppData\Local\{APP_AUTHOR}") elif system == "darwin": config_dir = pathlib.PosixPath("~/Library/Preferences").expanduser() else: diff --git a/antareslauncher/data_repo/data_provider.py b/antareslauncher/data_repo/data_provider.py deleted file mode 100644 index 85ff87c..0000000 --- a/antareslauncher/data_repo/data_provider.py +++ /dev/null @@ -1,11 +0,0 @@ -from dataclasses import dataclass - -from antareslauncher.data_repo.idata_repo import IDataRepo - - -@dataclass -class DataProvider: - data_repo: IDataRepo - - def get_list_of_studies(self): - return self.data_repo.get_list_of_studies() diff --git a/antareslauncher/data_repo/data_repo_tinydb.py b/antareslauncher/data_repo/data_repo_tinydb.py index 3a34067..8231aff 100644 --- a/antareslauncher/data_repo/data_repo_tinydb.py +++ b/antareslauncher/data_repo/data_repo_tinydb.py @@ -1,36 +1,44 @@ import logging -from typing import List +import typing as t import tinydb -from antareslauncher.data_repo.idata_repo import IDataRepo -from antareslauncher.study_dto import StudyDTO -from tinydb import TinyDB, where +from antareslauncher.study_dto import StudyDTO -class DataRepoTinydb(IDataRepo): +logger = logging.getLogger(__name__) + + +def _calc_diff( + old: t.Mapping[str, t.Any], + new: t.Mapping[str, t.Any], +) -> t.Mapping[str, t.Any]: + old_keys = frozenset(old) + new_keys = frozenset(new) + diff_map = { + "DEL": {k: old[k] for k in old_keys - new_keys}, + "ADD": {k: new[k] for k in new_keys - old_keys}, + "UPD": { + k: f"{old[k]!r} => {new[k]!r}" + for k in old_keys & new_keys + if old[k] != new[k] + }, + } + diff_map = {k: v for k, v in diff_map.items() if v} + return diff_map + + +class DataRepoTinydb: def __init__(self, database_file_path, db_primary_key: str): super(DataRepoTinydb, self).__init__() self.database_file_path = database_file_path - self.logger = logging.getLogger(f"{__name__}.{__class__.__name__}") self.db_primary_key = db_primary_key @property def db(self) -> tinydb.database.TinyDB: - return TinyDB(self.database_file_path, sort_keys=True, indent=4) - - @staticmethod - def doc_to_study(doc: tinydb.database.Document): - """Create a studyDTO from a tinydb.database.Document - - Args: - doc: Document representing a study - - Returns: - studyDTO object - """ - study = StudyDTO(path="empty/path") - study.__dict__ = doc - return study + if not hasattr(self, "_tiny_db"): + db = tinydb.TinyDB(self.database_file_path, sort_keys=True, indent=4) + setattr(self, "_tiny_db", db) + return getattr(self, "_tiny_db") def is_study_inside_database(self, study: StudyDTO) -> bool: """Get the study with selected primary key from the database @@ -43,7 +51,7 @@ def is_study_inside_database(self, study: StudyDTO) -> bool: """ pk_name = self.db_primary_key pk_value = getattr(study, pk_name) - found_studies = self.db.search(where(key=pk_name) == pk_value) + found_studies = self.db.search(tinydb.where(key=pk_name) == pk_value) return len(found_studies) == 1 def is_job_id_inside_database(self, job_id: int): @@ -58,12 +66,12 @@ def is_job_id_inside_database(self, job_id: int): studies_list = self.get_list_of_studies() return any(study.job_id == job_id for study in studies_list) - def get_list_of_studies(self) -> List[StudyDTO]: + def get_list_of_studies(self) -> t.Sequence[StudyDTO]: """ Returns: List of all studies inside the database """ - return [self.doc_to_study(doc) for doc in self.db.all()] + return [StudyDTO.from_dict(doc) for doc in self.db.all()] def save_study(self, study: StudyDTO): """Saves the selected study inside the database. If the study already exists inside the @@ -74,14 +82,17 @@ def save_study(self, study: StudyDTO): """ pk_name = self.db_primary_key pk_value = getattr(study, pk_name) - if self.is_study_inside_database(study=study): - self.logger.info(f"Updating study {pk_name}='{pk_value}' in database") - self.db.update(study.__dict__, where(pk_name) == pk_value) + old = self.db.get(tinydb.where(pk_name) == pk_value) + new = vars(study) + if old: + diff = _calc_diff(old, new) + logger.info(f"Updating study '{pk_value}' in database: {diff!r}") + self.db.update(new, tinydb.where(pk_name) == pk_value) else: - self.logger.info(f"Inserting new study {pk_name}='{pk_value}' in database") - self.db.insert(study.__dict__) + logger.info(f"Inserting study '{pk_value}' in database: {new!r}") + self.db.insert(new) def remove_study(self, study_name: str) -> None: pk_name = self.db_primary_key - self.logger.info(f"Removing study {pk_name}='{study_name}' from database") - self.db.remove(where(pk_name) == study_name) + logger.info(f"Removing study '{study_name}' from database") + self.db.remove(tinydb.where(pk_name) == study_name) diff --git a/antareslauncher/data_repo/data_reporter.py b/antareslauncher/data_repo/data_reporter.py index ea94203..c4063ec 100644 --- a/antareslauncher/data_repo/data_reporter.py +++ b/antareslauncher/data_repo/data_reporter.py @@ -1,9 +1,9 @@ -from antareslauncher.data_repo.idata_repo import IDataRepo +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.study_dto import StudyDTO class DataReporter: - def __init__(self, data_repo: IDataRepo): + def __init__(self, data_repo: DataRepoTinydb): self._data_repo = data_repo def save_study(self, study: StudyDTO): diff --git a/antareslauncher/data_repo/idata_repo.py b/antareslauncher/data_repo/idata_repo.py deleted file mode 100644 index 4996536..0000000 --- a/antareslauncher/data_repo/idata_repo.py +++ /dev/null @@ -1,25 +0,0 @@ -from abc import ABC, abstractmethod -from typing import List - -from antareslauncher.study_dto import StudyDTO - - -class IDataRepo(ABC): - def __init__(self): - pass - - @abstractmethod - def get_list_of_studies(self) -> List[StudyDTO]: - raise NotImplementedError - - @abstractmethod - def save_study(self, study: StudyDTO): - raise NotImplementedError - - @abstractmethod - def is_study_inside_database(self, study: StudyDTO) -> bool: - raise NotImplementedError - - @abstractmethod - def is_job_id_inside_database(self, job_id: int): - raise NotImplementedError diff --git a/antareslauncher/display/display_terminal.py b/antareslauncher/display/display_terminal.py index 22ce7c9..cbd0fbc 100644 --- a/antareslauncher/display/display_terminal.py +++ b/antareslauncher/display/display_terminal.py @@ -1,16 +1,16 @@ import datetime import logging +import typing as t from tqdm import tqdm -from antareslauncher.display.idisplay import IDisplay +class DisplayTerminal: + def __init__(self) -> None: + # Use the ISO8601 date format to display messages on the console + self.format = "%Y-%m-%d %H:%M:%S%z" -class DisplayTerminal(IDisplay): - def __init__(self): - self.format = "%Y%m%d %H:%M" - - def show_message(self, message: str, class_name: str, end: str = "\n"): + def show_message(self, message: str, class_name: str, end: str = "\n") -> None: """Displays a message on the terminal Args: @@ -23,7 +23,7 @@ def show_message(self, message: str, class_name: str, end: str = "\n"): if end != "\r": logging.getLogger(class_name).info(message) - def show_error(self, error: str, class_name: str): + def show_error(self, error: str, class_name: str) -> None: """Displays a error on the terminal Args: @@ -34,7 +34,12 @@ def show_error(self, error: str, class_name: str): print(f"ERROR - [{now.strftime(self.format)}] " + error) logging.getLogger(class_name).error(error) - def generate_progress_bar(self, iterator, desc="", total=None): + def generate_progress_bar( + self, + iterator: t.Iterable[t.Any], + desc: str = "", + total: t.Optional[int] = None, + ) -> t.Iterable[t.Any]: """Generates al loading bar and shows it in the terminal Args: @@ -53,8 +58,6 @@ def generate_progress_bar(self, iterator, desc="", total=None): desc=desc, leave=False, dynamic_ncols=True, - bar_format="[" - + str(now.strftime(self.format)) - + "] {l_bar}{bar}| {n_fmt}/{total_fmt} ", + bar_format="[" + str(now.strftime(self.format)) + "] {l_bar}{bar}| {n_fmt}/{total_fmt} ", ) return progress_bar diff --git a/antareslauncher/display/idisplay.py b/antareslauncher/display/idisplay.py deleted file mode 100644 index 024969a..0000000 --- a/antareslauncher/display/idisplay.py +++ /dev/null @@ -1,15 +0,0 @@ -from abc import ABC, abstractmethod - - -class IDisplay(ABC): - @abstractmethod - def show_message(self, message, class_name, end="\n"): - raise NotImplementedError - - @abstractmethod - def show_error(self, string, class_name): - raise NotImplementedError - - @abstractmethod - def generate_progress_bar(self, iterator, desc, total=None): - raise NotImplementedError diff --git a/antareslauncher/exceptions.py b/antareslauncher/exceptions.py index 201abfa..491e532 100644 --- a/antareslauncher/exceptions.py +++ b/antareslauncher/exceptions.py @@ -12,9 +12,7 @@ class AntaresLauncherException(Exception): class ConfigFileNotFoundError(AntaresLauncherException): """Configuration file not found.""" - def __init__( - self, possible_dirs: Sequence[pathlib.Path], config_name: str, *args - ) -> None: + def __init__(self, possible_dirs: Sequence[pathlib.Path], config_name: str, *args) -> None: super().__init__(possible_dirs, config_name, *args) @property diff --git a/antareslauncher/file_manager/__init__.py b/antareslauncher/file_manager/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/antareslauncher/file_manager/file_manager.py b/antareslauncher/file_manager/file_manager.py deleted file mode 100644 index bdd902f..0000000 --- a/antareslauncher/file_manager/file_manager.py +++ /dev/null @@ -1,206 +0,0 @@ -import configparser -import json -import logging -import os -import zipfile -from pathlib import Path - -from antareslauncher.display.idisplay import IDisplay - - -class FileManager: - def __init__(self, display_terminal: IDisplay): - self.logger = logging.getLogger(__name__ + "." + __class__.__name__) - self.display = display_terminal - - def get_config_from_file(self, file_path): - """Reads the configuration file of antares - - Args: - file_path: Path to the configuration file - - Returns: - The corresponding config object - """ - self.logger.info(f"Getting config from file {file_path}") - config_parser = configparser.ConfigParser() - if Path(file_path).exists(): - config_parser.read(file_path) - return config_parser - - def listdir_of(self, directory): - """Make a list of all the folders inside a directory - - Args: - directory: The directory that will be the root of the wanted list - - Returns: - A list of all the folders inside a directory - """ - self.logger.info(f"Getting directory list from path {directory}") - list_dir = os.listdir(directory) - list_dir.sort() - return list_dir - - @staticmethod - def is_dir(dir_path: Path): - return Path(dir_path).is_dir() - - def _get_list_dir_without_subdir(self, dir_path, subdir_to_exclude): - """Make a list of all the folders inside a directory except one - - Args: - dir_path: The directory that will be the root of the wanted list - - subdir_to_exclude:the subdir to remove from the list - - Returns: - A list of all the folders inside a directory without subdir_to_exclude - """ - list_dir = self.listdir_of(dir_path) - if subdir_to_exclude in list_dir: - list_dir.remove(subdir_to_exclude) - return list_dir - - def _get_list_of_files_recursively(self, element_path): - """Make a list of all the files inside a directory recursively - - Args: - element_path: Root dir of the list of files - - Returns: - List of all the files inside a directory recursively - """ - self.logger.info( - f"Getting list of all files inside the directory {element_path}" - ) - element_file_paths = [] - for root, _, files in os.walk(element_path): - for filename in files: - file_path = os.path.join(root, filename) - element_file_paths.append(file_path) - return element_file_paths - - def _get_complete_list_of_files_and_dirs_in_list_dir(self, dir_path, list_dir): - file_paths = [] - for element in list_dir: - element_path = os.path.join(dir_path, element) - file_paths.append(element_path) - if os.path.isdir(element_path): - element_file_paths = self._get_list_of_files_recursively(element_path) - file_paths.extend(element_file_paths) - return file_paths - - def zip_file_paths_with_rootdir_to_zipfile_path( - self, zipfile_path, file_paths, root_dir - ): - """Zips all the files in file_paths inside zipfile_path - while printing a progress bar on the terminal - - Args: - zipfile_path: Path of the zipfile that will be created - - file_paths: Paths of all the files that need to be zipped - - root_dir: Root directory - """ - self.logger.info(f"Zipping list of files to archive {zipfile_path}") - with zipfile.ZipFile( - zipfile_path, "w", compression=zipfile.ZIP_DEFLATED - ) as my_zip: - loading_bar = self.display.generate_progress_bar( - file_paths, desc="Compressing files: " - ) - for f in loading_bar: - my_zip.write(f, os.path.relpath(f, root_dir)) - - def zip_dir_excluding_subdir(self, dir_path, zipfile_path, subdir_to_exclude): - """Zips a whole directory without one subdir - - Args: - dir_path: Path of the directory to zip - - zipfile_path: Path of the zip file that will be created - - subdir_to_exclude: Subdirectory that will not be zipped - """ - list_dir = self._get_list_dir_without_subdir(dir_path, subdir_to_exclude) - file_paths = self._get_complete_list_of_files_and_dirs_in_list_dir( - dir_path, list_dir - ) - root_dir = str(Path(dir_path).parent) - self.zip_file_paths_with_rootdir_to_zipfile_path( - zipfile_path, file_paths, root_dir - ) - return Path(zipfile_path).is_file() - - def unzip(self, file_path: str): - """Unzips the result of the antares job once is has been downloaded - - Args: - file_path: The path to the file - - Returns: - True if the file has been extracted, False otherwise - """ - self.logger.info(f"Unzipping {file_path}") - try: - with zipfile.ZipFile(file=file_path) as zip_file: - progress_bar = self.display.generate_progress_bar( - zip_file.namelist(), - desc="Extracting archive:", - total=len(zip_file.namelist()), - ) - for file in progress_bar: - zip_file.extract( - member=file, - path=Path(file_path).parent, - ) - return True - except ValueError: - return False - except FileNotFoundError: - return False - - def make_dir(self, directory_name): - self.logger.info(f"Creating directory {directory_name}") - os.makedirs(directory_name, exist_ok=True) - - def convert_json_file_to_dict(self, file_path): - self.logger.info(f"Converting json file {file_path} to dict") - try: - with open(file_path, "r") as readFile: - config = json.load(readFile) - except OSError: - self.logger.error( - f"Unable to convert {file_path} to json (file not found or invalid type)" - ) - config = None - return config - - def remove_file(self, file_path: str): - """ - Given a file path, it removes it - - Args: - file_path: File path - - Returns: None - """ - try: - Path(file_path).unlink() - self.logger.info(f"file: {file_path} got deleted") - except FileNotFoundError: - self.logger.warning(f"Could not find path: {str(file_path)}") - - @staticmethod - def file_exists(file_path: str) -> bool: - """Checks if the given file path, is a regular file - - Args: - file_path: file_path - - Returns: - file path, is a regular file - """ - return Path(file_path).is_file() diff --git a/antareslauncher/logger_initializer.py b/antareslauncher/logger_initializer.py index e242193..cca6d11 100644 --- a/antareslauncher/logger_initializer.py +++ b/antareslauncher/logger_initializer.py @@ -13,12 +13,8 @@ def init_logger(self): Returns: """ - formatter = logging.Formatter( - "%(asctime)s - %(levelname)s - %(name)s - %(message)s" - ) - f_handler = RotatingFileHandler( - self.file_path, maxBytes=200000, backupCount=5, mode="a+" - ) + formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + f_handler = RotatingFileHandler(self.file_path, maxBytes=200000, backupCount=5, mode="a+") f_handler.setFormatter(formatter) f_handler.setLevel(logging.DEBUG) logging.basicConfig(level=logging.INFO, handlers=[f_handler]) diff --git a/antareslauncher/main.py b/antareslauncher/main.py index 076c054..02b3226 100644 --- a/antareslauncher/main.py +++ b/antareslauncher/main.py @@ -1,45 +1,25 @@ import argparse -from dataclasses import dataclass +import dataclasses +import json +import typing as t from pathlib import Path -from typing import List, Dict from antareslauncher import __version__ from antareslauncher.antares_launcher import AntaresLauncher from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.logger_initializer import LoggerInitializer from antareslauncher.remote_environnement import ssh_connection -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) -from antareslauncher.remote_environnement.slurm_script_features import ( - SlurmScriptFeatures, -) -from antareslauncher.use_cases.check_remote_queue.check_queue_controller import ( - CheckQueueController, -) -from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import ( - SlurmQueueShow, -) -from antareslauncher.use_cases.create_list.study_list_composer import ( - StudyListComposer, - StudyListComposerParameters, -) -from antareslauncher.use_cases.generate_tree_structure.tree_structure_initializer import ( - TreeStructureInitializer, -) -from antareslauncher.use_cases.kill_job.job_kill_controller import ( - JobKillController, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm +from antareslauncher.remote_environnement.slurm_script_features import SlurmScriptFeatures +from antareslauncher.use_cases.check_remote_queue.check_queue_controller import CheckQueueController +from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import SlurmQueueShow +from antareslauncher.use_cases.create_list.study_list_composer import StudyListComposer, StudyListComposerParameters +from antareslauncher.use_cases.kill_job.job_kill_controller import JobKillController from antareslauncher.use_cases.launch.launch_controller import LaunchController -from antareslauncher.use_cases.retrieve.retrieve_controller import ( - RetrieveController, -) +from antareslauncher.use_cases.retrieve.retrieve_controller import RetrieveController from antareslauncher.use_cases.retrieve.state_updater import StateUpdater -from antareslauncher.use_cases.wait_loop_controller.wait_controller import ( - WaitController, -) +from antareslauncher.use_cases.wait_loop_controller.wait_controller import WaitController class NoJsonConfigFileError(Exception): @@ -65,19 +45,36 @@ class SshConnectionNotEstablishedException(Exception): # fmt: on -@dataclass +@dataclasses.dataclass class MainParameters: + """ + Represents the main parameters of the application. + + Attributes: + json_dir: Path to the directory where the JSON database will be stored. + default_json_db_name: The default JSON database name. + slurm_script_path: Path to the SLURM script used to launch studies (a Shell script). + antares_versions_on_remote_server: A list of available Antares Solver versions on the remote server. + default_ssh_dict: A dictionary containing the SSH settings read from `ssh_config.json`. + db_primary_key: The primary key for the database, default to "name". + partition: Extra `sbatch` option to request a specific partition for resource allocation. + If not specified, the default behavior is to allow the SLURM controller + to select the default partition as designated by the system administrator. + quality_of_service: Extra `sbatch` option to request a quality of service for the job. + QOS values can be defined for each user/cluster/account association in the Slurm database. + """ + json_dir: Path default_json_db_name: str slurm_script_path: str - antares_versions_on_remote_server: List[str] - default_ssh_dict: Dict + antares_versions_on_remote_server: t.Sequence[str] + default_ssh_dict: t.Mapping[str, t.Any] db_primary_key: str + partition: str = "" + quality_of_service: str = "" -def run_with( - arguments: argparse.Namespace, parameters: MainParameters, show_banner=False -): +def run_with(arguments: argparse.Namespace, parameters: MainParameters, show_banner=False): """Instantiates all the objects necessary to antares-launcher, and runs the program""" if arguments.version: print(f"Antares_Launcher v{__version__}") @@ -87,41 +84,31 @@ def run_with( print(ANTARES_LAUNCHER_BANNER) display = DisplayTerminal() - file_manager = FileManager(display) db_json_file_path = parameters.json_dir / parameters.default_json_db_name - tree_structure_initializer = TreeStructureInitializer( - display, - file_manager, - arguments.studies_in, - arguments.log_dir, - arguments.output_dir, - ) + Path(arguments.studies_in).mkdir(parents=True, exist_ok=True) + Path(arguments.log_dir).mkdir(parents=True, exist_ok=True) + Path(arguments.output_dir).mkdir(parents=True, exist_ok=True) + display.show_message("Tree structure initialized", __name__) - tree_structure_initializer.init_tree_structure() - logger_initializer = LoggerInitializer( - str(Path(arguments.log_dir) / "antares_launcher.log") - ) + logger_initializer = LoggerInitializer(str(Path(arguments.log_dir) / "antares_launcher.log")) logger_initializer.init_logger() # connection - ssh_dict = get_ssh_config_dict( - file_manager, - arguments.json_ssh_config, - parameters.default_ssh_dict, - ) + ssh_dict = get_ssh_config_dict(arguments.json_ssh_config, parameters.default_ssh_dict) connection = ssh_connection.SshConnection(config=ssh_dict) verify_connection(connection, display) - slurm_script_features = SlurmScriptFeatures(parameters.slurm_script_path) - environment = RemoteEnvironmentWithSlurm(connection, slurm_script_features) - data_repo = DataRepoTinydb( - database_file_path=db_json_file_path, db_primary_key=parameters.db_primary_key + slurm_script_features = SlurmScriptFeatures( + parameters.slurm_script_path, + partition=parameters.partition, + quality_of_service=parameters.quality_of_service, ) + environment = RemoteEnvironmentWithSlurm(connection, slurm_script_features) + data_repo = DataRepoTinydb(database_file_path=db_json_file_path, db_primary_key=parameters.db_primary_key) study_list_composer = StudyListComposer( repo=data_repo, - file_manager=file_manager, display=display, parameters=StudyListComposerParameters( studies_in_dir=arguments.studies_in, @@ -133,19 +120,14 @@ def run_with( post_processing=arguments.post_processing, antares_versions_on_remote_server=parameters.antares_versions_on_remote_server, other_options=arguments.other_options or "", + antares_version=arguments.antares_version, ), ) - launch_controller = LaunchController( - repo=data_repo, - env=environment, - file_manager=file_manager, - display=display, - ) + launch_controller = LaunchController(repo=data_repo, env=environment, display=display) state_updater = StateUpdater(env=environment, display=display) retrieve_controller = RetrieveController( repo=data_repo, env=environment, - file_manager=file_manager, display=display, state_updater=state_updater, ) @@ -187,11 +169,12 @@ def verify_connection(connection, display): # fmt: on -def get_ssh_config_dict(file_manager, json_ssh_config, ssh_dict: dict): - if json_ssh_config is None: +def get_ssh_config_dict( + json_ssh_config: str, + ssh_dict: t.Mapping[str, t.Any], +) -> t.Mapping[str, t.Any]: + if not json_ssh_config: ssh_dict = ssh_dict else: - ssh_dict = file_manager.convert_json_file_to_dict(json_ssh_config) - if ssh_dict is None: - raise Exception("Could not find any SSH configuration file") + ssh_dict = json.loads(Path(json_ssh_config).read_text(encoding="utf-8")) return ssh_dict diff --git a/antareslauncher/main_option_parser.py b/antareslauncher/main_option_parser.py index 7030e82..ad7eb98 100644 --- a/antareslauncher/main_option_parser.py +++ b/antareslauncher/main_option_parser.py @@ -1,11 +1,13 @@ from __future__ import annotations import argparse +import datetime import getpass import pathlib +import typing as t from argparse import RawTextHelpFormatter from dataclasses import dataclass -from typing import List, Optional +from pathlib import Path @dataclass @@ -17,92 +19,81 @@ class ParserParameters: log_dir: str finished_dir: str ssh_config_file_is_required: bool - ssh_configfile_path_alternate1: Optional[pathlib.Path] - ssh_configfile_path_alternate2: Optional[pathlib.Path] + ssh_configfile_path_alternate1: t.Optional[pathlib.Path] + ssh_configfile_path_alternate2: t.Optional[pathlib.Path] class MainOptionParser: def __init__(self, parameters: ParserParameters) -> None: self.parser = argparse.ArgumentParser(formatter_class=RawTextHelpFormatter) - self.default_argument_values = {} - self.parameters = parameters - self._set_default_argument_values() - - def _set_default_argument_values(self) -> None: - """Fills the "default_argument_values" dictionary""" - self.default_argument_values = { + defaults = { "wait_mode": False, - "wait_time": self.parameters.default_wait_time, - "studies_in": str(self.parameters.studies_in_dir), - "output_dir": str(self.parameters.finished_dir), + "wait_time": parameters.default_wait_time, + "studies_in": str(parameters.studies_in_dir), + "output_dir": str(parameters.finished_dir), "check_queue": False, - "time_limit": self.parameters.default_time_limit, - "json_ssh_config": look_for_default_ssh_conf_file(self.parameters), - "log_dir": str(self.parameters.log_dir), - "n_cpu": self.parameters.default_n_cpu, + "time_limit": parameters.default_time_limit, + "json_ssh_config": look_for_default_ssh_conf_file(parameters), + "log_dir": str(parameters.log_dir), + "n_cpu": parameters.default_n_cpu, + "antares_version": 0, "job_id_to_kill": None, - "xpansion_mode": None, + "xpansion_mode": "", "version": False, "post_processing": False, "other_options": None, } + self.parser.set_defaults(**defaults) - def parse_args(self, args: List[str] = None) -> argparse.Namespace: - """Parses the args given with the selected options. - If args is None, the standard input will be parsed - - Args: - args: Arguments given to the program - - Returns: - argparse.Namespace: namespace containing all the options + # NOTE: keep this delegation to preserve backward compatibility with v1.3.0 + def parse_args(self, args: t.Union[t.Sequence[str], None]) -> argparse.Namespace: + return self.parser.parse_args(args) - """ - output: argparse.Namespace = self.parser.parse_args(args) - - for key, value in self.default_argument_values.items(): - if not hasattr(output, key): - setattr(output, key, value) - return output - - def add_basic_arguments(self) -> MainOptionParser: + def add_basic_arguments(self, *, antares_versions: t.Sequence[str] = ()) -> MainOptionParser: """Adds to the parser all the arguments for the light mode""" self.parser.add_argument( "-w", "--wait-mode", action="store_true", dest="wait_mode", - default=self.default_argument_values["wait_mode"], - help="Activate the wait mode: the Antares_Launcher waits for all the jobs to finish\n" - "it check every WAIT_TIME seconds (default value = 900 = 15 minutes).", + help=( + "Activate the wait mode: the Antares_Launcher waits for all the jobs to finish\n" + "it check every WAIT_TIME seconds (default value = 900 = 15 minutes)." + ), ) + wait_time = self.parser.get_default("wait_time") + delta = datetime.timedelta(seconds=wait_time) self.parser.add_argument( "--wait-time", dest="wait_time", type=int, - default=self.default_argument_values["wait_time"], - help="Number of seconds between each verification of the end of the simulations\n" - "changes the value of WAIT_TIME used for the wait-mode.", + help=( + "Number of seconds between each verification of the end of the simulations\n" + "changes the value of WAIT_TIME used for the wait-mode.\n" + f"The default value will be used: {delta.total_seconds():.0f}s = {delta}." + ), ) self.parser.add_argument( "-i", "--studies-in-dir", dest="studies_in", - default=self.default_argument_values["studies_in"], - help="Directory containing the studies to be executed.\n" - "If the directory does not exist, it will be created (empty).\n" - "if no directory is indicated, the default value will be used STUDIES-IN", + help=( + "Directory containing the studies to be executed.\n" + "If the directory does not exist, it will be created (empty).\n" + "if no directory is indicated, the default value will be used STUDIES-IN" + ), ) self.parser.add_argument( "-o", "--output-dir", dest="output_dir", - default=self.default_argument_values["output_dir"], - help="Directory where the finished studies will be downloaded and extracted.\n" - 'If the directory does not exist, it will be created (default value "FINISHED").', + help=( + "Directory where the finished studies will be downloaded and extracted.\n" + 'If the directory does not exist, it will be created (default value "FINISHED").' + ), ) self.parser.add_argument( @@ -110,33 +101,38 @@ def add_basic_arguments(self) -> MainOptionParser: "--check-queue", action="store_true", dest="check_queue", - default=self.default_argument_values["check_queue"], - help="Displays from the remote queue all job statuses.\n" - "If the option is used, it will override the standard execution.\n" - "It can be overridden by the kill job option (-k).", + help=( + "Displays from the remote queue all job statuses.\n" + "If the option is used, it will override the standard execution.\n" + "It can be overridden by the kill job option (-k)." + ), ) - seconds_in_hour = 3600 + + time_limit = self.parser.get_default("time_limit") + delta = datetime.timedelta(seconds=time_limit) self.parser.add_argument( "-t", "--time-limit", dest="time_limit", type=int, - default=self.default_argument_values["time_limit"], - help="Time limit in seconds of a single job.\n" - "If nothing is specified here and" - "if the study is not initialised with a specific value,\n" - f"the default value will be used: {self.parameters.default_time_limit}={int(self.parameters.default_time_limit / seconds_in_hour)}h.", + help=( + "Time limit in seconds of a single job.\n" + "If nothing is specified here and" + "if the study is not initialised with a specific value,\n" + f"The default value will be used: {delta.total_seconds():.0f}s = {delta}." + ), ) self.parser.add_argument( "-x", "--xpansion-mode", dest="xpansion_mode", - default=None, - help="Activate the xpansion mode:\n" - "Antares_Launcher will launch all the new studies in xpansion mode if\n" - "the studies contains the information necessary for AntaresXpansion.\n" - 'if rhe flag is set to "r", the xpansion mode will be activated with the R version.\n', + help=( + "Activate the xpansion mode:\n" + "Antares_Launcher will launch all the new studies in xpansion mode if\n" + "the studies contains the information necessary for AntaresXpansion.\n" + 'if rhe flag is set to "r", the xpansion mode will be activated with the R version.\n' + ), ) self.parser.add_argument( @@ -144,7 +140,6 @@ def add_basic_arguments(self) -> MainOptionParser: "--version", action="store_true", dest="version", - default=False, help="Shows the version of Antares_Launcher", ) @@ -153,7 +148,6 @@ def add_basic_arguments(self) -> MainOptionParser: "--post-processing", action="store_true", dest="post_processing", - default=False, help='Enables the post processing of the antares study by executing the "post_processing.R" file', ) @@ -168,71 +162,82 @@ def add_basic_arguments(self) -> MainOptionParser: "--kill-job", dest="job_id_to_kill", type=int, - default=self.default_argument_values["job_id_to_kill"], - help=f"JobID of the run to be cancelled on the remote server.\n" - f"the JobID can be retrieved with option -q to show the queue." - f"If option is given it overrides the -q and the standard execution.", + help=( + f"JobID of the run to be cancelled on the remote server.\n" + f"the JobID can be retrieved with option -q to show the queue." + f"If option is given it overrides the -q and the standard execution." + ), ) + + self.parser.add_argument( + "--solver-version", + dest="antares_version", + type=int, + choices=[int(v) for v in antares_versions], + help="Antares Solver version to use for simulation", + ) + return self - def add_advanced_arguments(self) -> MainOptionParser: + def add_advanced_arguments( + self, + *, + ssh_config_required: bool = False, + alt_ssh_paths: t.Sequence[t.Optional[Path]] = (), + ) -> MainOptionParser: """Adds to the parser all the arguments for the advanced mode""" + n_cpu = self.parser.get_default("n_cpu") self.parser.add_argument( "-n", "--n-cores", dest="n_cpu", type=int, - default=self.default_argument_values["n_cpu"], - help=f"Number of cores to be used for a single job.\n" - f"If nothing is specified here and " - f"if the study is not initialised with a specific value,\n" - f"the default value will be used: n_cpu=={self.parameters.default_n_cpu}", + help=( + f"Number of cores to be used for a single job.\n" + f"If nothing is specified here and " + f"if the study is not initialised with a specific value,\n" + f"the default value will be used: n_cpu=={n_cpu}" + ), ) self.parser.add_argument( "--log-dir", dest="log_dir", - default=self.default_argument_values["log_dir"], - help="Directory where the logs of the jobs will be found.\n" - "If the directory does not exist, it will be created.", + help=( + "Directory where the logs of the jobs will be found.\n" + "If the directory does not exist, it will be created." + ), ) + ssh_paths = "\n".join(f"'{p}'" for p in dict.fromkeys(alt_ssh_paths)) self.parser.add_argument( "--ssh-settings-file", dest="json_ssh_config", - default=self.default_argument_values["json_ssh_config"], - required=self.parameters.ssh_config_file_is_required, - help=f"Path to the configuration file for the ssh connection.\n" - f"If no value is given, " - f"it will look for it in default location with this order:\n" - f"1st: {self.parameters.ssh_configfile_path_alternate1}\n" - f"2nd: {self.parameters.ssh_configfile_path_alternate2}\n", + required=ssh_config_required, + help=( + f"Path to the configuration file for the ssh connection.\n" + f"If no value is given, " + f"it will look for it in default location with this order:\n" + f"{ssh_paths}" + ), ) return self def look_for_default_ssh_conf_file( parameters: ParserParameters, -) -> pathlib.Path: +) -> t.Union[None, pathlib.Path]: """Checks if the ssh config file exists. Returns: path to the ssh config file is it exists, None otherwise """ - ssh_conf_file: pathlib.Path - if ( - parameters.ssh_configfile_path_alternate1 - and parameters.ssh_configfile_path_alternate1.is_file() - ): - ssh_conf_file = parameters.ssh_configfile_path_alternate1 - elif ( - parameters.ssh_configfile_path_alternate2 - and parameters.ssh_configfile_path_alternate2.is_file() - ): - ssh_conf_file = parameters.ssh_configfile_path_alternate2 + if parameters.ssh_configfile_path_alternate1 and parameters.ssh_configfile_path_alternate1.is_file(): + return parameters.ssh_configfile_path_alternate1 + elif parameters.ssh_configfile_path_alternate2 and parameters.ssh_configfile_path_alternate2.is_file(): + return parameters.ssh_configfile_path_alternate2 else: - ssh_conf_file = None - return ssh_conf_file + return None def get_default_db_name() -> str: diff --git a/antareslauncher/parameters_reader.py b/antareslauncher/parameters_reader.py index c9189d6..6e04777 100644 --- a/antareslauncher/parameters_reader.py +++ b/antareslauncher/parameters_reader.py @@ -1,10 +1,11 @@ +import getpass import json import os.path +import typing as t from pathlib import Path -from typing import Dict, Any import yaml -import getpass + from antareslauncher.main import MainParameters from antareslauncher.main_option_parser import ParserParameters @@ -13,45 +14,53 @@ DEFAULT_JSON_DB_NAME = f"{getpass.getuser()}_antares_launcher_db.json" -class ParametersReader: - class EmptyFileException(TypeError): - pass +class MissingValueException(Exception): + def __init__(self, yaml_filepath: Path, key: str) -> None: + super().__init__(f"Missing key '{key}' in '{yaml_filepath}'") - class MissingValueException(KeyError): - pass +class ParametersReader: def __init__(self, json_ssh_conf: Path, yaml_filepath: Path): self.json_ssh_conf = json_ssh_conf - with open(Path(yaml_filepath)) as yaml_file: - self.yaml_content = yaml.load(yaml_file, Loader=yaml.FullLoader) or {} + with open(yaml_filepath) as yaml_file: + obj = yaml.load(yaml_file, Loader=yaml.FullLoader) or {} - # fmt: off - self._wait_time = self._get_compulsory_value("DEFAULT_WAIT_TIME") - self.time_limit = self._get_compulsory_value("DEFAULT_TIME_LIMIT") - self.n_cpu = self._get_compulsory_value("DEFAULT_N_CPU") - self.studies_in_dir = os.path.expanduser(self._get_compulsory_value("STUDIES_IN_DIR")) - self.log_dir = os.path.expanduser(self._get_compulsory_value("LOG_DIR")) - self.finished_dir = os.path.expanduser(self._get_compulsory_value("FINISHED_DIR")) - self.ssh_conf_file_is_required = self._get_compulsory_value("SSH_CONFIG_FILE_IS_REQUIRED") - # fmt: on + try: + self.default_wait_time = obj["DEFAULT_WAIT_TIME"] + self.time_limit = obj["DEFAULT_TIME_LIMIT"] + self.n_cpu = obj["DEFAULT_N_CPU"] + self.studies_in_dir = os.path.expanduser(obj["STUDIES_IN_DIR"]) + self.log_dir = os.path.expanduser(obj["LOG_DIR"]) + self.finished_dir = os.path.expanduser(obj["FINISHED_DIR"]) + self.ssh_conf_file_is_required = obj["SSH_CONFIG_FILE_IS_REQUIRED"] + default_ssh_configfile_name = obj["DEFAULT_SSH_CONFIGFILE_NAME"] + except KeyError as e: + raise MissingValueException(yaml_filepath, str(e)) from None - alt1, alt2 = self._get_ssh_conf_file_alts() - self.ssh_conf_alt1, self.ssh_conf_alt2 = alt1, alt2 - self.default_ssh_dict = self._get_ssh_dict_from_json() - self.remote_slurm_script_path = self._get_compulsory_value("SLURM_SCRIPT_PATH") - self.antares_versions = self._get_compulsory_value( - "ANTARES_VERSIONS_ON_REMOTE_SERVER" - ) - self.db_primary_key = self._get_compulsory_value("DB_PRIMARY_KEY") - self.json_dir = Path(self._get_compulsory_value("JSON_DIR")).expanduser() - self.json_db_name = self.yaml_content.get( - "DEFAULT_JSON_DB_NAME", DEFAULT_JSON_DB_NAME - ) + default_alternate1 = ALT1_PARENT / default_ssh_configfile_name + default_alternate2 = ALT2_PARENT / default_ssh_configfile_name + + alt1 = obj.get("SSH_CONFIGFILE_PATH_ALTERNATE1", default_alternate1) + alt2 = obj.get("SSH_CONFIGFILE_PATH_ALTERNATE2", default_alternate2) + + try: + self.ssh_conf_alt1 = alt1 + self.ssh_conf_alt2 = alt2 + self.default_ssh_dict = self._get_ssh_dict_from_json() + self.remote_slurm_script_path = obj["SLURM_SCRIPT_PATH"] + self.partition = obj.get("PARTITION", "") + self.quality_of_service = obj.get("QUALITY_OF_SERVICE", "") + self.antares_versions = obj["ANTARES_VERSIONS_ON_REMOTE_SERVER"] + self.db_primary_key = obj["DB_PRIMARY_KEY"] + self.json_dir = Path(obj["JSON_DIR"]).expanduser() + self.json_db_name = obj.get("DEFAULT_JSON_DB_NAME", DEFAULT_JSON_DB_NAME) + except KeyError as e: + raise MissingValueException(yaml_filepath, str(e)) from None def get_parser_parameters(self): - options = ParserParameters( - default_wait_time=self._wait_time, + return ParserParameters( + default_wait_time=self.default_wait_time, default_time_limit=self.time_limit, default_n_cpu=self.n_cpu, studies_in_dir=self.studies_in_dir, @@ -61,52 +70,22 @@ def get_parser_parameters(self): ssh_configfile_path_alternate1=self.ssh_conf_alt1, ssh_configfile_path_alternate2=self.ssh_conf_alt2, ) - return options def get_main_parameters(self) -> MainParameters: - main_parameters = MainParameters( + return MainParameters( json_dir=self.json_dir, default_json_db_name=self.json_db_name, slurm_script_path=self.remote_slurm_script_path, + partition=self.partition, + quality_of_service=self.quality_of_service, antares_versions_on_remote_server=self.antares_versions, default_ssh_dict=self.default_ssh_dict, db_primary_key=self.db_primary_key, ) - return main_parameters - - def _get_ssh_conf_file_alts(self): - default_alternate1, default_alternate2 = self._get_default_alternate_values() - ssh_conf_alternate1 = self.yaml_content.get( - "SSH_CONFIGFILE_PATH_ALTERNATE1", - default_alternate1, - ) - ssh_conf_alternate2 = self.yaml_content.get( - "SSH_CONFIGFILE_PATH_ALTERNATE2", - default_alternate2, - ) - return ssh_conf_alternate1, ssh_conf_alternate2 - - def _get_default_alternate_values(self): - default_ssh_configfile_name = self._get_compulsory_value( - "DEFAULT_SSH_CONFIGFILE_NAME" - ) - default_alternate1 = ALT1_PARENT / default_ssh_configfile_name - default_alternate2 = ALT2_PARENT / default_ssh_configfile_name - return default_alternate1, default_alternate2 - - def _get_compulsory_value(self, key: str): - try: - value = self.yaml_content[key] - except KeyError as e: - print(f"missing value: {str(e)}") - raise ParametersReader.MissingValueException(e) from None - return value - def _get_ssh_dict_from_json(self) -> Dict[str, Any]: + def _get_ssh_dict_from_json(self) -> t.Dict[str, t.Any]: with open(self.json_ssh_conf) as ssh_connection_json: ssh_dict = json.load(ssh_connection_json) if "private_key_file" in ssh_dict: - ssh_dict["private_key_file"] = os.path.expanduser( - ssh_dict["private_key_file"] - ) + ssh_dict["private_key_file"] = os.path.expanduser(ssh_dict["private_key_file"]) return ssh_dict diff --git a/antareslauncher/remote_environnement/remote_environment_with_slurm.py b/antareslauncher/remote_environnement/remote_environment_with_slurm.py index 0401f79..71d75ba 100644 --- a/antareslauncher/remote_environnement/remote_environment_with_slurm.py +++ b/antareslauncher/remote_environnement/remote_environment_with_slurm.py @@ -6,13 +6,10 @@ import socket import textwrap import time +import typing as t from pathlib import Path, PurePosixPath -from typing import List, Optional -from antareslauncher.remote_environnement.slurm_script_features import ( - ScriptParametersDTO, - SlurmScriptFeatures, -) +from antareslauncher.remote_environnement.slurm_script_features import ScriptParametersDTO, SlurmScriptFeatures from antareslauncher.remote_environnement.ssh_connection import SshConnection from antareslauncher.study_dto import StudyDTO @@ -25,20 +22,13 @@ class RemoteEnvBaseError(Exception): class GetJobStateError(RemoteEnvBaseError): def __init__(self, job_id: int, job_name: str, reason: str): - msg = ( - f"Unable to retrieve the status of the SLURM job {job_id}" - f" (study job '{job_name})." - f" {reason}" - ) + msg = f"Unable to retrieve the status of the SLURM job {job_id} (study job '{job_name}). {reason}" super().__init__(msg) class JobNotFoundError(RemoteEnvBaseError): def __init__(self, job_id: int, job_name: str): - msg = ( - f"Unable to retrieve the status of the SLURM job {job_id}" - f" (study job '{job_name}): Job not found." - ) + msg = f"Unable to retrieve the status of the SLURM job {job_id} (study job '{job_name}): Job not found." super().__init__(msg) @@ -84,6 +74,9 @@ class JobStateCodes(enum.Enum): # Job has terminated all processes on all nodes with an exit code of zero. COMPLETED = "COMPLETED" + # Indicates that the only job on the node or that all jobs on the node are in the process of completing. + COMPLETING = "COMPLETING" + # Job terminated on deadline. DEADLINE = "DEADLINE" @@ -138,9 +131,7 @@ def __init__( def _initialise_remote_path(self): remote_home_dir = PurePosixPath(self.connection.home_dir) - remote_base_path = remote_home_dir.joinpath( - f"REMOTE_{getpass.getuser()}_{socket.gethostname()}" - ) + remote_base_path = remote_home_dir.joinpath(f"REMOTE_{getpass.getuser()}_{socket.gethostname()}") self.remote_base_path = str(remote_base_path) if not self.connection.make_dir(self.remote_base_path): raise NoRemoteBaseDirError(remote_base_path) @@ -209,9 +200,7 @@ def submit_job(self, my_study: StudyDTO): Raises: SubmitJobErrorException if the job has not been successfully submitted """ - time_limit = self.convert_time_limit_from_seconds_to_minutes( - my_study.time_limit - ) + time_limit = self.convert_time_limit_from_seconds_to_minutes(my_study.time_limit) script_params = ScriptParametersDTO( study_dir_name=Path(my_study.path).name, input_zipfile_name=Path(my_study.zipfile_path).name, @@ -230,15 +219,10 @@ def submit_job(self, my_study: StudyDTO): raise SubmitJobError(my_study.name, reason) # should match "Submitted batch job 123456" - if match := re.match( - r"Submitted.*?(?P\d+)", output, flags=re.IGNORECASE - ): + if match := re.match(r"Submitted.*?(?P\d+)", output, flags=re.IGNORECASE): return int(match["job_id"]) - reason = ( - f"The command [{command}] return an non-parsable output:" - f"\n{textwrap.indent(output, 'OUTPUT> ')}" - ) + reason = f"The command [{command}] return an non-parsable output:\n{textwrap.indent(output, 'OUTPUT> ')}" raise SubmitJobError(my_study.name, reason) def get_job_state_flags( @@ -247,7 +231,7 @@ def get_job_state_flags( *, attempts=5, sleep_time=0.5, - ) -> [bool, bool, bool]: + ) -> t.Tuple[bool, bool, bool]: """ Retrieves the current state of a SLURM job with the given job ID and name. @@ -267,8 +251,7 @@ def get_job_state_flags( if job_state is None: # noinspection SpellCheckingInspection logger.info( - f"Job '{study.job_id}' no longer active in SLURM," - f" the job status is read from the SACCT database..." + f"Job '{study.job_id}' no longer active in SLURM, the job status is read from the SACCT database..." ) job_state = self._retrieve_slurm_acct_state( study.job_id, @@ -288,6 +271,7 @@ def get_job_state_flags( JobStateCodes.BOOT_FAIL: (False, False, False), JobStateCodes.CANCELLED: (True, True, True), JobStateCodes.COMPLETED: (True, True, False), + JobStateCodes.COMPLETING: (True, False, False), JobStateCodes.DEADLINE: (True, True, True), # similar to timeout JobStateCodes.FAILED: (True, True, True), JobStateCodes.NODE_FAIL: (True, True, True), @@ -306,7 +290,7 @@ def _retrieve_slurm_control_state( self, job_id: int, job_name: str, - ) -> Optional[JobStateCodes]: + ) -> t.Optional[JobStateCodes]: """ Use the `scontrol` command to retrieve job status information in SLURM. See: https://slurm.schedmd.com/scontrol.html @@ -329,10 +313,7 @@ def _retrieve_slurm_control_state( if match := re.search(r"JobState=(\w+)", output): return JobStateCodes(match[1]) - reason = ( - f"The command [{command}] return an non-parsable output:" - f"\n{textwrap.indent(output, 'OUTPUT> ')}" - ) + reason = f"The command [{command}] return an non-parsable output:\n{textwrap.indent(output, 'OUTPUT> ')}" raise GetJobStateError(job_id, job_name, reason) def _retrieve_slurm_acct_state( @@ -342,7 +323,7 @@ def _retrieve_slurm_acct_state( *, attempts: int = 5, sleep_time: float = 0.5, - ) -> Optional[JobStateCodes]: + ) -> t.Optional[JobStateCodes]: # Construct the command line arguments used to check the jobs state. # See the man page: https://slurm.schedmd.com/sacct.html # noinspection SpellCheckingInspection @@ -361,7 +342,7 @@ def _retrieve_slurm_acct_state( # Makes several attempts to get the job state. # I don't really know why, but it's better to reproduce the old behavior. - output: Optional[str] + output: t.Optional[str] last_error: str = "" for attempt in range(attempts): output, error = self.connection.execute_command(command) @@ -370,10 +351,7 @@ def _retrieve_slurm_acct_state( last_error = error time.sleep(sleep_time) else: - reason = ( - f"The command [{command}] failed after {attempts} attempts:" - f" {last_error}" - ) + reason = f"The command [{command}] failed after {attempts} attempts: {last_error}" raise GetJobStateError(job_id, job_name, reason) # When the output is empty it mean that the job is not found @@ -388,16 +366,15 @@ def _retrieve_slurm_acct_state( out_job_id, out_job_name, out_state = parts if out_job_id == str(job_id) and out_job_name == job_name: # Match the first word only, e.g.: "CANCEL by 123456798" - job_state_str = re.match(r"(\w+)", out_state)[1] - return JobStateCodes(job_state_str) + match = re.match(r"(\w+)", out_state) + if not match: + raise GetJobStateError(job_id, job_name, f"Unable to parse the job state: '{out_state}'") + return JobStateCodes(match[1]) - reason = ( - f"The command [{command}] return an non-parsable output:" - f"\n{textwrap.indent(output, 'OUTPUT> ')}" - ) + reason = f"The command [{command}] return an non-parsable output:\n{textwrap.indent(output, 'OUTPUT> ')}" raise GetJobStateError(job_id, job_name, reason) - def upload_file(self, src): + def upload_file(self, src) -> bool: """Uploads a file to the remote server Args: @@ -409,7 +386,7 @@ def upload_file(self, src): dst = f"{self.remote_base_path}/{Path(src).name}" return self.connection.upload_file(src, dst) - def download_logs(self, study: StudyDTO) -> List[Path]: + def download_logs(self, study: StudyDTO) -> t.Sequence[Path]: """ Download the slurm logs of a given study. @@ -419,8 +396,7 @@ def download_logs(self, study: StudyDTO) -> List[Path]: to download the log files. Returns: - True if all the logs have been downloaded, False if all the logs - have not been downloaded or if there are no files to download + The paths of the downloaded logs on the local filesystem. """ src_dir = PurePosixPath(self.remote_base_path) dst_dir = Path(study.job_log_dir) @@ -431,7 +407,7 @@ def download_logs(self, study: StudyDTO) -> List[Path]: remove=study.finished, ) - def download_final_zip(self, study: StudyDTO) -> Optional[Path]: + def download_final_zip(self, study: StudyDTO) -> t.Optional[Path]: """ Download the final ZIP file for the specified study from the remote server and save it to the local output directory. @@ -460,7 +436,7 @@ def download_final_zip(self, study: StudyDTO) -> Optional[Path]: ) return next(iter(downloaded_files), None) - def remove_input_zipfile(self, study: StudyDTO): + def remove_input_zipfile(self, study: StudyDTO) -> bool: """Removes initial zipfile Args: @@ -471,12 +447,10 @@ def remove_input_zipfile(self, study: StudyDTO): """ if not study.input_zipfile_removed: zip_name = Path(study.zipfile_path).name - study.input_zipfile_removed = self.connection.remove_file( - f"{self.remote_base_path}/{zip_name}" - ) + study.input_zipfile_removed = self.connection.remove_file(f"{self.remote_base_path}/{zip_name}") return study.input_zipfile_removed - def remove_remote_final_zipfile(self, study: StudyDTO): + def remove_remote_final_zipfile(self, study: StudyDTO) -> bool: """Removes final zipfile Args: @@ -485,11 +459,9 @@ def remove_remote_final_zipfile(self, study: StudyDTO): Returns: True if the file has been successfully removed, False otherwise """ - return self.connection.remove_file( - f"{self.remote_base_path}/{Path(study.local_final_zipfile_path).name}" - ) + return self.connection.remove_file(f"{self.remote_base_path}/{Path(study.local_final_zipfile_path).name}") - def clean_remote_server(self, study: StudyDTO): + def clean_remote_server(self, study: StudyDTO) -> bool: """ Removes the input and the output zipfile from the remote host @@ -502,6 +474,5 @@ def clean_remote_server(self, study: StudyDTO): return ( False if study.remote_server_is_clean - else self.remove_remote_final_zipfile(study) - & self.remove_input_zipfile(study) + else self.remove_remote_final_zipfile(study) & self.remove_input_zipfile(study) ) diff --git a/antareslauncher/remote_environnement/slurm_script_features.py b/antareslauncher/remote_environnement/slurm_script_features.py index 6d3de9c..e741b04 100644 --- a/antareslauncher/remote_environnement/slurm_script_features.py +++ b/antareslauncher/remote_environnement/slurm_script_features.py @@ -1,15 +1,16 @@ -from dataclasses import dataclass +import dataclasses +import shlex from antareslauncher.study_dto import Modes -@dataclass +@dataclasses.dataclass class ScriptParametersDTO: study_dir_name: str input_zipfile_name: str time_limit: int n_cpu: int - antares_version: str + antares_version: int run_mode: Modes post_processing: bool other_options: str @@ -19,77 +20,72 @@ class SlurmScriptFeatures: """Class that returns data related to the remote SLURM script Installed on the remote server""" - def __init__(self, slurm_script_path: str): - self.JOB_TYPE_PLACEHOLDER = "TO_BE_REPLACED_WITH_JOB_TYPE" - self.JOB_TYPE_ANTARES = "ANTARES" - self.JOB_TYPE_XPANSION_R = "ANTARES_XPANSION_R" - self.JOB_TYPE_XPANSION_CPP = "ANTARES_XPANSION_CPP" + def __init__( + self, + slurm_script_path: str, + *, + partition: str, + quality_of_service: str, + ): + """ + Initialize the slurm script feature. + + Args: + slurm_script_path: Path to the SLURM script used to launch studies (a Shell script). + partition: Request a specific partition for the resource allocation. + If not specified, the default behavior is to allow the slurm controller + to select the default partition as designated by the system administrator. + quality_of_service: Request a quality of service for the job. + QOS values can be defined for each user/cluster/account association in the Slurm database. + """ self.solver_script_path = slurm_script_path - self._script_params = None - self._remote_launch_dir = None + self.partition = partition + self.quality_of_service = quality_of_service def compose_launch_command( self, remote_launch_dir: str, script_params: ScriptParametersDTO, ) -> str: - """Compose and return the complete command to be executed to launch the Antares Solver script. - It includes the change of directory to remote_base_path + """ + Compose and return the complete command to be executed to launch the Antares Solver script. Args: - script_params: ScriptFeaturesDTO dataclass container for script parameters remote_launch_dir: remote directory where the script is launched + script_params: ScriptFeaturesDTO dataclass container for script parameters Returns: - str: the complete command to be executed to launch the including the change of directory to remote_base_path - + str: the complete command to be executed to launch a study on the SLURM server """ - self._script_params = script_params - self._remote_launch_dir = remote_launch_dir - complete_command = self._get_complete_command_with_placeholders() + # The following options can be added to the `sbatch` command + # if they are not empty (or null for integer options). + _opts = { + "--partition": self.partition, # non-empty string + "--qos": self.quality_of_service, # non-empty string + "--job-name": script_params.study_dir_name, # non-empty string + "--time": script_params.time_limit, # greater than 0 + "--cpus-per-task": script_params.n_cpu, # greater than 0 + } - if script_params.run_mode == Modes.antares: - complete_command = complete_command.replace( - self.JOB_TYPE_PLACEHOLDER, self.JOB_TYPE_ANTARES - ) - elif script_params.run_mode == Modes.xpansion_r: - complete_command = complete_command.replace( - self.JOB_TYPE_PLACEHOLDER, self.JOB_TYPE_XPANSION_R - ) - elif script_params.run_mode == Modes.xpansion_cpp: - complete_command = complete_command.replace( - self.JOB_TYPE_PLACEHOLDER, self.JOB_TYPE_XPANSION_CPP - ) + _job_type = { + Modes.antares: "ANTARES", # Mode for Antares Solver + Modes.xpansion_r: "ANTARES_XPANSION_R", # Mode for Old Xpansion implemented in R + Modes.xpansion_cpp: "ANTARES_XPANSION_CPP", # Mode for Xpansion implemented in C++ + }[script_params.run_mode] - return complete_command - - def _bash_options(self): - option1_zipfile_name = f' "{self._script_params.input_zipfile_name}"' - option2_antares_version = f" {self._script_params.antares_version}" - option3_job_type = f" {self.JOB_TYPE_PLACEHOLDER}" - option4_post_processing = f" {self._script_params.post_processing}" - option5_other_options = f" '{self._script_params.other_options}'" - bash_options = ( - option1_zipfile_name - + option2_antares_version - + option3_job_type - + option4_post_processing - + option5_other_options + # Construct the `sbatch` command + args = ["sbatch"] + args.extend(f"{k}={shlex.quote(str(v))}" for k, v in _opts.items() if v) + args.extend( + shlex.quote(arg) + for arg in [ + self.solver_script_path, + script_params.input_zipfile_name, + str(script_params.antares_version), + _job_type, + str(script_params.post_processing), + script_params.other_options, + ] ) - return bash_options - - def _sbatch_command_with_slurm_options(self): - call_sbatch = f"sbatch" - job_name = f' --job-name="{self._script_params.study_dir_name}"' - time_limit_opt = f" --time={self._script_params.time_limit}" - cpu_per_task = f" --cpus-per-task={self._script_params.n_cpu}" - slurm_options = call_sbatch + job_name + time_limit_opt + cpu_per_task - return slurm_options - - def _get_complete_command_with_placeholders(self): - change_dir = f"cd {self._remote_launch_dir}" - slurm_options = self._sbatch_command_with_slurm_options() - bash_options = self._bash_options() - submit_command = slurm_options + " " + self.solver_script_path + bash_options - complete_command = change_dir + " && " + submit_command - return complete_command + launch_cmd = f"cd {remote_launch_dir} && {' '.join(args)}" + return launch_cmd diff --git a/antareslauncher/remote_environnement/ssh_connection.py b/antareslauncher/remote_environnement/ssh_connection.py index ed8a92a..7b2e3ff 100644 --- a/antareslauncher/remote_environnement/ssh_connection.py +++ b/antareslauncher/remote_environnement/ssh_connection.py @@ -6,19 +6,12 @@ import textwrap import time from pathlib import Path, PurePosixPath -from typing import List, Tuple +import typing as t import paramiko -try: - # noinspection PyUnresolvedReferences - from typing import TypeAlias -except ImportError: - RemotePath = PurePosixPath - LocalPath = Path -else: - RemotePath: TypeAlias = PurePosixPath - LocalPath: TypeAlias = Path +RemotePath = PurePosixPath +LocalPath = Path class SshConnectionError(Exception): @@ -109,9 +102,7 @@ def __str__(self) -> str: # 0 duration total_duration # 0% percent 100% duration = time.time() - self._start_time - eta = int( - duration * (self.total_size - total_transferred) / total_transferred - ) + eta = int(duration * (self.total_size - total_transferred) / total_transferred) return f"{self.msg:<20} ETA: {eta}s [{rate:.0%}]" return f"{self.msg:<20} ETA: ??? [{rate:.0%}]" @@ -131,7 +122,7 @@ def accumulate(self): class SshConnection: """Class to _connect to remote server""" - def __init__(self, config: dict = None): + def __init__(self, config: t.Mapping[str, t.Any]): """ Initialize the SSH connection. @@ -140,9 +131,9 @@ def __init__(self, config: dict = None): "password" (not compulsory if private_key_file is given), "private_key_file": path to private rsa key """ super(SshConnection, self).__init__() - self.logger = logging.getLogger(f"{__name__}.{__class__.__name__}") - self.__client = None - self.__home_dir = None + self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") + self._client = None + self._home_dir = "" self.timeout = 10 self.host = "" self.username = "" @@ -152,19 +143,15 @@ def __init__(self, config: dict = None): if config: self.logger.info("Loading ssh connection from config dictionary") - self.__init_from_config(config) + self._init_from_config(config) else: - error = InvalidConfigError( - config, "missing values: 'hostname', 'username', 'password'..." - ) + error = InvalidConfigError(config, "missing values: 'hostname', 'username', 'password'...") self.logger.debug(str(error)) raise error self.initialize_home_dir() - self.logger.info( - f"Connection created with host = {self.host} and username = {self.username}" - ) + self.logger.info(f"Connection created with host = {self.host} and username = {self.username}") - def __initialise_public_key(self, key_file_name, key_password): + def _init_public_key(self, key_file_name, key_password): """Initialises self.private_key Args: @@ -174,53 +161,47 @@ def __initialise_public_key(self, key_file_name, key_password): True if a valid key was found, False otherwise """ try: - self.private_key = paramiko.Ed25519Key.from_private_key_file( - filename=key_file_name - ) + self.private_key = paramiko.Ed25519Key.from_private_key_file(filename=key_file_name) return True except paramiko.SSHException: try: - self.private_key = paramiko.RSAKey.from_private_key_file( - filename=key_file_name, password=key_password - ) + self.private_key = paramiko.RSAKey.from_private_key_file(filename=key_file_name, password=key_password) return True except paramiko.SSHException: self.private_key = None return False - def __init_from_config(self, config: dict): + def _init_from_config(self, config: t.Mapping[str, t.Any]) -> None: self.host = config.get("hostname", "") self.username = config.get("username", "") self.port = config.get("port", 22) self.password = config.get("password") key_password = config.get("key_password") if key_file := config.get("private_key_file"): - self.__initialise_public_key( - key_file_name=key_file, key_password=key_password - ) + self._init_public_key(key_file_name=key_file, key_password=key_password) elif self.password is None: error = InvalidConfigError(config, "missing 'password'") self.logger.debug(str(error)) raise error - def initialize_home_dir(self): - """Initializes self.__home_dir with the home directory retrieved by started "echo $HOME" connecting to the + def initialize_home_dir(self) -> None: + """Initializes self._home_dir with the home directory retrieved by started "echo $HOME" connecting to the remote server """ output, _ = self.execute_command("echo $HOME") - self.__home_dir = str(output).split()[0] + self._home_dir = str(output).split()[0] @property - def home_dir(self): + def home_dir(self) -> str: """ Returns: The home directory of the remote server """ - return self.__home_dir + return self._home_dir @contextlib.contextmanager - def ssh_client(self) -> paramiko.SSHClient: + def ssh_client(self) -> t.Generator[paramiko.SSHClient, None, None]: client = paramiko.SSHClient() try: try: @@ -250,27 +231,17 @@ def ssh_client(self) -> paramiko.SSHClient: look_for_keys=False, ) except paramiko.AuthenticationException as e: - self.logger.exception( - f"paramiko.AuthenticationException: {paramiko.AuthenticationException}" - ) - raise ConnectionFailedException( - self.host, self.port, self.username - ) from e + self.logger.exception(f"paramiko.AuthenticationException: {paramiko.AuthenticationException}") + raise ConnectionFailedException(self.host, self.port, self.username) from e except paramiko.SSHException as e: self.logger.exception(f"paramiko.SSHException: {paramiko.SSHException}") - raise ConnectionFailedException( - self.host, self.port, self.username - ) from e + raise ConnectionFailedException(self.host, self.port, self.username) from e except socket.timeout as e: self.logger.exception(f"socket.timeout: {socket.timeout}") - raise ConnectionFailedException( - self.host, self.port, self.username - ) from e + raise ConnectionFailedException(self.host, self.port, self.username) from e except socket.error as e: self.logger.exception(f"socket.error: {socket.error}") - raise ConnectionFailedException( - self.host, self.port, self.username - ) from e + raise ConnectionFailedException(self.host, self.port, self.username) from e yield client finally: @@ -373,7 +344,7 @@ def download_files( pattern: str, *patterns: str, remove: bool = True, - ) -> List[LocalPath]: + ) -> t.Sequence[LocalPath]: """ Download files matching the specified patterns from the remote source directory to the local destination directory, @@ -394,9 +365,7 @@ def download_files( The paths of the downloaded files on the local filesystem. """ try: - return self._download_files( - src_dir, dst_dir, (pattern,) + patterns, remove=remove - ) + return self._download_files(src_dir, dst_dir, (pattern,) + patterns, remove=remove) except TimeoutError as exc: self.logger.error(f"Timeout: {exc}", exc_info=True) return [] @@ -411,10 +380,10 @@ def _download_files( self, src_dir: RemotePath, dst_dir: LocalPath, - patterns: Tuple[str], + patterns: t.Tuple[str, ...], *, remove: bool = True, - ) -> List[LocalPath]: + ) -> t.Sequence[LocalPath]: """ Download files matching the specified patterns from the remote source directory to the local destination directory. @@ -432,17 +401,13 @@ def _download_files( The paths of the downloaded files on the local filesystem. """ with self.ssh_client() as client: - with contextlib.closing( - client.open_sftp() - ) as sftp: # type: paramiko.sftp_client.SFTPClient + with contextlib.closing(client.open_sftp()) as sftp: # type: paramiko.sftp_client.SFTPClient # Get list of files to download remote_attrs = sftp.listdir_attr(str(src_dir)) remote_files = [file_attr.filename for file_attr in remote_attrs] total_size = sum((file_attr.st_size or 0) for file_attr in remote_attrs) files_to_download = [ - f - for f in remote_files - if any(fnmatch.fnmatch(f, pattern) for pattern in patterns) + f for f in remote_files if any(fnmatch.fnmatch(f, pattern) for pattern in patterns) ] # Monitor the download progression monitor = DownloadMonitor(total_size, logger=self.logger) diff --git a/antareslauncher/study_dto.py b/antareslauncher/study_dto.py index af83e94..7202686 100644 --- a/antareslauncher/study_dto.py +++ b/antareslauncher/study_dto.py @@ -1,7 +1,7 @@ +import typing as t from dataclasses import dataclass, field from enum import IntEnum from pathlib import Path -from typing import Optional class Modes(IntEnum): @@ -16,28 +16,47 @@ class StudyDTO: path: str name: str = field(init=False) - zipfile_path: str = "" - zip_is_sent: bool = False + + # Job state flags started: bool = False finished: bool = False + done: bool = False with_error: bool = False - local_final_zipfile_path: str = "" + + # Job state message + job_state: str = "Pending" # "Running", "Finished", "Ended with error", "Internal error: ..." + + # Processing stage flags + zip_is_sent: bool = False input_zipfile_removed: bool = False logs_downloaded: bool = False - job_log_dir: str = "" - output_dir: str = "" remote_server_is_clean: bool = False final_zip_extracted: bool = False - done: bool = False - job_id: Optional[int] = None - job_state: str = "" - time_limit: Optional[int] = None - n_cpu: Optional[int] = None - antares_version: Optional[str] = None - xpansion_mode: Optional[str] = None + + # Processing stage data + job_id: int = 0 # sbatch job id + zipfile_path: str = "" + local_final_zipfile_path: str = "" + job_log_dir: str = "" + output_dir: str = "" + + # Simulation stage data + time_limit: t.Optional[int] = None + n_cpu: int = 1 + antares_version: int = 0 + xpansion_mode: str = "" # "", "r", "cpp" run_mode: Modes = Modes.antares post_processing: bool = False other_options: str = "" - def __post_init__(self): + def __post_init__(self) -> None: self.name = Path(self.path).name + + @classmethod + def from_dict(cls, doc: t.Mapping[str, t.Any]) -> "StudyDTO": + """ + Create a Study DTO from a mapping. + """ + attrs = dict(**doc) + attrs.pop("name", None) # calculated + return cls(**attrs) diff --git a/antareslauncher/use_cases/check_remote_queue/check_queue_controller.py b/antareslauncher/use_cases/check_remote_queue/check_queue_controller.py index 1173964..4788edd 100644 --- a/antareslauncher/use_cases/check_remote_queue/check_queue_controller.py +++ b/antareslauncher/use_cases/check_remote_queue/check_queue_controller.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from antareslauncher.data_repo.idata_repo import IDataRepo +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import SlurmQueueShow from antareslauncher.use_cases.retrieve.state_updater import StateUpdater @@ -9,7 +9,7 @@ class CheckQueueController: slurm_queue_show: SlurmQueueShow state_updater: StateUpdater - repo: IDataRepo + repo: DataRepoTinydb def check_queue(self): """Displays all the jobs un the slurm queue""" diff --git a/antareslauncher/use_cases/check_remote_queue/slurm_queue_show.py b/antareslauncher/use_cases/check_remote_queue/slurm_queue_show.py index d7e2074..f27c0b2 100644 --- a/antareslauncher/use_cases/check_remote_queue/slurm_queue_show.py +++ b/antareslauncher/use_cases/check_remote_queue/slurm_queue_show.py @@ -1,15 +1,13 @@ from dataclasses import dataclass -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm @dataclass class SlurmQueueShow: env: RemoteEnvironmentWithSlurm - display: IDisplay + display: DisplayTerminal def run(self): """Displays all the jobs un the slurm queue""" diff --git a/antareslauncher/use_cases/create_list/study_list_composer.py b/antareslauncher/use_cases/create_list/study_list_composer.py index 3b254f5..b874ea7 100644 --- a/antareslauncher/use_cases/create_list/study_list_composer.py +++ b/antareslauncher/use_cases/create_list/study_list_composer.py @@ -1,37 +1,59 @@ +import configparser +import typing as t from dataclasses import dataclass from pathlib import Path -from typing import List, Optional -from antareslauncher.data_repo.idata_repo import IDataRepo -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.file_manager.file_manager import FileManager +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.study_dto import Modes, StudyDTO +def get_solver_version(study_dir: Path, *, default: int = 0) -> int: + """ + Retrieve the solver version number or else the study version number + from the "study.antares" file. + + Args: + study_dir: Directory path which contains the "study.antares" file. + default: Default version number to use if the version is not found. + + Returns: + The value of `solver_version`, `version` or the default version number. + """ + study_path = study_dir.joinpath("study.antares") + config = configparser.ConfigParser() + config.read(study_path) + if "antares" not in config: + return default + section = config["antares"] + for key in "solver_version", "version": + if key in section: + return int(section[key]) + return default + + @dataclass class StudyListComposerParameters: studies_in_dir: str time_limit: int log_dir: str n_cpu: int - xpansion_mode: Optional[str] + xpansion_mode: str # "", "r", "cpp" output_dir: str post_processing: bool - antares_versions_on_remote_server: List[str] + antares_versions_on_remote_server: t.Sequence[str] other_options: str + antares_version: int = 0 -@dataclass class StudyListComposer: def __init__( self, - repo: IDataRepo, - file_manager: FileManager, - display: IDisplay, + repo: DataRepoTinydb, + display: DisplayTerminal, parameters: StudyListComposerParameters, ): self._repo = repo - self._file_manager = file_manager self._display = display self._studies_in_dir = parameters.studies_in_dir self.time_limit = parameters.time_limit @@ -41,11 +63,10 @@ def __init__( self.output_dir = parameters.output_dir self.post_processing = parameters.post_processing self.other_options = parameters.other_options + self.antares_version = parameters.antares_version self._new_study_added = False self.DEFAULT_JOB_LOG_DIR_PATH = str(Path(self.log_dir) / "JOB_LOGS") - self.ANTARES_VERSIONS_ON_REMOTE_SERVER = ( - parameters.antares_versions_on_remote_server - ) + self.ANTARES_VERSIONS_ON_REMOTE_SERVER = [int(v) for v in parameters.antares_versions_on_remote_server] def get_list_of_studies(self): """Retrieve the list of studies from the repo @@ -55,14 +76,12 @@ def get_list_of_studies(self): """ return self._repo.get_list_of_studies() - def _create_study(self, path, antares_version, xpansion_mode: str): - if self.xpansion_mode == "r": - run_mode = Modes.xpansion_r - elif self.xpansion_mode == "cpp": - run_mode = Modes.xpansion_cpp - else: - run_mode = Modes.antares - + def _create_study(self, path: Path, antares_version: int, xpansion_mode: str) -> StudyDTO: + run_mode = { + "": Modes.antares, + "r": Modes.xpansion_r, + "cpp": Modes.xpansion_cpp, + }.get(self.xpansion_mode, Modes.antares) new_study = StudyDTO( path=str(path), n_cpu=self.n_cpu, @@ -75,54 +94,8 @@ def _create_study(self, path, antares_version, xpansion_mode: str): post_processing=self.post_processing, other_options=self.other_options, ) - return new_study - def get_antares_version(self, directory_path: str): - """Checks if the directory is an antares study and returns the version - - Checks if the directory is an antares study by checking the presence of the study.antares file - and by checking the presence of the 'antares' field in this file. - - Args: - directory_path: Path of the directory to test - - Returns: - The version if the directory is an antares study, None otherwise - """ - file_path = Path(directory_path) / "study.antares" - config = self._file_manager.get_config_from_file(file_path) - if "antares" in config: - solver_version = config["antares"].get("solver_version", None) - return solver_version or config["antares"].get("version", None) - - def _is_valid_antares_study(self, antares_version): - if antares_version is None: - self._display.show_message( - "... not a valid Antares study", - __name__ + "." + __class__.__name__, - ) - return False - - elif antares_version in self.ANTARES_VERSIONS_ON_REMOTE_SERVER: - return True - else: - message = f"... Antares version ({antares_version}) is not supported (supported versions: {self.ANTARES_VERSIONS_ON_REMOTE_SERVER})" - self._display.show_message( - message, - __name__ + "." + __class__.__name__, - ) - return False - - def _is_there_candidates_file(self, directory_path: Path): - candidates_file_path = str( - Path.joinpath(directory_path, "user", "expansion", "candidates.ini") - ) - return self._file_manager.file_exists(candidates_file_path) - - def _is_xpansion_study(self, xpansion_study_path: str): - return self._is_there_candidates_file(Path(xpansion_study_path)) - def update_study_database(self): """List all directories inside the STUDIES_IN_DIR folder, if a directory is a valid antares study and is new, then creates a StudyDTO object then saves it in the repo @@ -134,10 +107,9 @@ def update_study_database(self): self._new_study_added = False - directories = self._file_manager.listdir_of(self._studies_in_dir) - for directory in directories: - directory_path = Path(self._studies_in_dir) / Path(directory) - if self._file_manager.is_dir(directory_path): + directories = Path(self._studies_in_dir).iterdir() + for directory_path in sorted(directories): + if directory_path.is_dir(): self._update_database_with_directory(directory_path) if not self._new_study_added: @@ -146,33 +118,35 @@ def update_study_database(self): f"{__name__}.{__class__.__name__}", ) - def _update_database_with_new_study( - self, antares_version, directory_path, xpansion_mode: str - ): - buffer_study = self._create_study( - directory_path, antares_version, xpansion_mode - ) - self._update_database_with_study(buffer_study) - - def _update_database_with_directory(self, directory_path): - antares_version = self.get_antares_version(directory_path) - if self._is_valid_antares_study(antares_version): - is_xpansion_study = self._is_xpansion_study(directory_path) - xpansion_mode = self.xpansion_mode if is_xpansion_study else None - - valid_xpansion_candidate = ( - self.xpansion_mode in ["r", "cpp"] and is_xpansion_study + def _update_database_with_directory(self, directory_path: Path): + solver_version = get_solver_version(directory_path) + antares_version = self.antares_version or solver_version + if not antares_version: + self._display.show_message( + "... not a valid Antares study", + __name__ + "." + self.__class__.__name__, + ) + elif antares_version not in self.ANTARES_VERSIONS_ON_REMOTE_SERVER: + message = ( + f"... Antares version {antares_version} is not supported" + f" (supported versions: {self.ANTARES_VERSIONS_ON_REMOTE_SERVER})" ) - valid_antares_candidate = self.xpansion_mode is None + self._display.show_message( + message, + __name__ + "." + self.__class__.__name__, + ) + else: + candidates_file_path = directory_path.joinpath("user", "expansion", "candidates.ini") + is_xpansion_study = candidates_file_path.is_file() + xpansion_mode = self.xpansion_mode if is_xpansion_study else "" - if valid_antares_candidate or valid_xpansion_candidate: - self._update_database_with_new_study( - antares_version, directory_path, xpansion_mode - ) + valid_xpansion_candidate = self.xpansion_mode in ["r", "cpp"] and is_xpansion_study + valid_antares_candidate = not self.xpansion_mode - def _update_database_with_study(self, buffer_study): - if not self._repo.is_study_inside_database(buffer_study): - self._add_study_to_database(buffer_study) + if valid_antares_candidate or valid_xpansion_candidate: + buffer_study = self._create_study(directory_path, antares_version, xpansion_mode) + if not self._repo.is_study_inside_database(buffer_study): + self._add_study_to_database(buffer_study) def _add_study_to_database(self, buffer_study): self._repo.save_study(buffer_study) @@ -181,6 +155,6 @@ def _add_study_to_database(self, buffer_study): f"(mode = {buffer_study.run_mode.name}, " f"version={buffer_study.antares_version}): " f'"{buffer_study.path}"', - __name__ + "." + __class__.__name__, + __name__ + "." + self.__class__.__name__, ) self._new_study_added = True diff --git a/antareslauncher/use_cases/generate_tree_structure/__init__.py b/antareslauncher/use_cases/generate_tree_structure/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py b/antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py deleted file mode 100644 index 71b1828..0000000 --- a/antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py +++ /dev/null @@ -1,22 +0,0 @@ -from dataclasses import dataclass - -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.file_manager.file_manager import FileManager - - -@dataclass -class TreeStructureInitializer: - display: IDisplay - file_manager: FileManager - studies_in: str - log_dir: str - finished: str - - def init_tree_structure(self): - """Initialize the structure""" - self.file_manager.make_dir(self.studies_in) - self.file_manager.make_dir(self.log_dir) - self.file_manager.make_dir(self.finished) - self.display.show_message( - "Tree structure initialized", __name__ + "." + __class__.__name__ - ) diff --git a/antareslauncher/use_cases/kill_job/job_kill_controller.py b/antareslauncher/use_cases/kill_job/job_kill_controller.py index fd7fcad..a6a56bb 100644 --- a/antareslauncher/use_cases/kill_job/job_kill_controller.py +++ b/antareslauncher/use_cases/kill_job/job_kill_controller.py @@ -1,17 +1,15 @@ from dataclasses import dataclass -from antareslauncher.data_repo.idata_repo import IDataRepo -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm @dataclass class JobKillController: env: RemoteEnvironmentWithSlurm - display: IDisplay - repo: IDataRepo + display: DisplayTerminal + repo: DataRepoTinydb def _check_if_job_is_killable(self, job_id): return self.repo.is_job_id_inside_database(job_id) @@ -23,12 +21,10 @@ def kill_job(self, job_id: int): job_id: The ID of the slurm job to be killed """ if self._check_if_job_is_killable(job_id): - self.display.show_message( - f"Killing job {job_id}", __name__ + "." + __class__.__name__ - ) + self.display.show_message(f"Killing job {job_id}", __name__ + "." + self.__class__.__name__) self.env.kill_remote_job(job_id) else: self.display.show_message( f"You are not authorized to kill job {job_id}", - __name__ + "." + __class__.__name__, + __name__ + "." + self.__class__.__name__, ) diff --git a/antareslauncher/use_cases/launch/launch_controller.py b/antareslauncher/use_cases/launch/launch_controller.py index 730bfd6..c5fe29c 100644 --- a/antareslauncher/use_cases/launch/launch_controller.py +++ b/antareslauncher/use_cases/launch/launch_controller.py @@ -1,83 +1,117 @@ +import getpass +import zipfile +from pathlib import Path + +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.data_repo.data_reporter import DataReporter -from antareslauncher.data_repo.idata_repo import IDataRepo -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.file_manager.file_manager import FileManager -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) -from antareslauncher.study_dto import StudyDTO +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.use_cases.launch.study_submitter import StudySubmitter -from antareslauncher.use_cases.launch.study_zip_cleaner import StudyZipCleaner from antareslauncher.use_cases.launch.study_zip_uploader import StudyZipfileUploader -from antareslauncher.use_cases.launch.study_zipper import StudyZipper + +LOG_NAME = f"{__name__}.StudyLauncher" class StudyLauncher: def __init__( self, - zipper: StudyZipper, study_uploader: StudyZipfileUploader, - zipfile_cleaner: StudyZipCleaner, study_submitter: StudySubmitter, reporter: DataReporter, + display: DisplayTerminal, ): - self._zipper = zipper + self.display = display self._study_uploader = study_uploader - self._zipfile_cleaner = zipfile_cleaner self._study_submitter = study_submitter self.reporter = reporter - self._current_study: StudyDTO = None - def _zip_study(self): - self._current_study = self._zipper.zip(self._current_study) - self.reporter.save_study(self._current_study) + def launch_study(self, study): + if study.job_id: + # No need to display a user message here; job already exists. + return - def _upload_zipfile(self): - self._current_study = self._study_uploader.upload(self._current_study) - self.reporter.save_study(self._current_study) + try: + # Compress the study folder and upload it to the SLURM server. + study_dir = Path(study.path) + zip_name = f"{study_dir.name}-{getpass.getuser()}.zip" + root_dir = study_dir.parent + zip_path = root_dir / zip_name - def _remove_input_zipfile(self): - if self._current_study.zip_is_sent is True: - self._current_study = self._zipfile_cleaner.remove_input_zipfile( - self._current_study - ) - self.reporter.save_study(self._current_study) + # Find all files to be compressed. + study_files = set(study_dir.rglob("*")) - def _submit_job(self): - self._current_study = self._study_submitter.submit_job(self._current_study) - self.reporter.save_study(self._current_study) + # NOTE: output filtering isn't currently handled. + # + # Antares Web sets up the study directory with pre-filtered outputs when launching, + # but this isn't the case with the CLI. + # We may introduce new parameters in `StudyDTO` for customizable output filtering + # at the study level, especially for scenarios like Xpansion sensitivity mode. + # + # Suggested parameters: + # - `exclude_pattern = "output/**/*"`: Default CLI exclusion. + # - `include_pattern = "output/{output_id}/**/*"`: Inclusion for specific output + # for Xpansion sensitivity mode (e.g., `output_id = "20230926-1230adq"`). + # + # Suggested implementation: + # study_files -= set(study_dir.glob(exclude_pattern)) if exclude_pattern else set() + # study_files |= set(study_dir.glob(include_pattern)) if include_pattern else set() - def launch_study(self, study): - self._current_study = study - self._zip_study() - self._upload_zipfile() - self._remove_input_zipfile() - self._submit_job() + # Compress the study directory + with zipfile.ZipFile(zip_path, mode="w", compression=zipfile.ZIP_DEFLATED) as zf: + loading_bar = self.display.generate_progress_bar(sorted(study_files), desc="Compressing files: ") + for study_file in loading_bar: + zf.write(study_file, study_file.relative_to(root_dir)) + + # Upload the ZIP file to the SLURM server. + # If the upload is successful, the `zip_is_sent` attribute is updated accordingly. + # In all cases, the ZIP file is removed from the local machine. + study.zipfile_path = str(zip_path) + try: + self._study_uploader.upload(study) + if not study.zip_is_sent: + raise Exception("ZIP upload failed") + except Exception as e: + self.display.show_error(f'"{study.name}": was not uploaded: {e}', LOG_NAME) + # If the ZIP file is partially uploaded, it must be removed anyway. + self._study_uploader.remove(study) + raise + finally: + zip_path.unlink() + + # Now launch the job on the SLURM server. + # If the launch is successful, the `job_id` attribute is updated accordingly. + # If the launch fails, remove the ZIP file from the remote server. + try: + self._study_submitter.submit_job(study) + if not study.job_id: + raise Exception("Job submission failed") + except Exception: + self._study_uploader.remove(study) + raise + + except Exception as e: + # The exception is not re-raised, but the job is marked as failed with an internal error message. + study.with_error = True + study.job_state = f"Internal error: {e}" + + finally: + # Save the study information after processing. + self.reporter.save_study(study) class LaunchController: def __init__( self, - repo: IDataRepo, + repo: DataRepoTinydb, env: RemoteEnvironmentWithSlurm, - file_manager: FileManager, - display: IDisplay, + display: DisplayTerminal, ): self.repo = repo self.env = env - self.file_manager = file_manager self.display = display - zipper = StudyZipper(file_manager, display) study_uploader = StudyZipfileUploader(env, display) - zipfile_cleaner = StudyZipCleaner(file_manager, display) study_submitter = StudySubmitter(env, display) - self.study_launcher = StudyLauncher( - zipper, - study_uploader, - zipfile_cleaner, - study_submitter, - DataReporter(repo), - ) + self.study_launcher = StudyLauncher(study_uploader, study_submitter, DataReporter(repo), display) def launch_all_studies(self): """Processes all the studies and send them to the server to process the job diff --git a/antareslauncher/use_cases/launch/study_submitter.py b/antareslauncher/use_cases/launch/study_submitter.py index 91c62b9..3dd14e1 100644 --- a/antareslauncher/use_cases/launch/study_submitter.py +++ b/antareslauncher/use_cases/launch/study_submitter.py @@ -1,40 +1,23 @@ -import copy from pathlib import Path -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO - -class FailedSubmissionException(Exception): - pass +LOG_NAME = f"{__name__}.StudySubmitter" class StudySubmitter(object): - def __init__(self, env: RemoteEnvironmentWithSlurm, display: IDisplay): + def __init__(self, env: RemoteEnvironmentWithSlurm, display: DisplayTerminal): self.env = env self.display = display - self._current_study: StudyDTO = None - - def submit_job(self, study: StudyDTO) -> StudyDTO: - self._current_study = copy.deepcopy(study) - if self._current_study.job_id is None: - self._do_submit() - return self._current_study - def _do_submit(self): - job_id = self.env.submit_job(copy.deepcopy(self._current_study)) - if job_id is not None: - self._current_study.job_id = job_id - self.display.show_message( - f'"{Path(self._current_study.path).name}": was submitted', - __name__ + "." + __class__.__name__, - ) + def submit_job(self, study: StudyDTO) -> None: + if study.job_id: + self.display.show_message(f'"{Path(study.path).name}": is already submitted', LOG_NAME) + return + study.job_id = self.env.submit_job(study) # may raise SubmitJobError + if study.job_id: + self.display.show_message(f'"{Path(study.path).name}": was submitted', LOG_NAME) else: - self.display.show_error( - f'"{Path(self._current_study.path).name}": was not submitted', - __name__ + "." + __class__.__name__, - ) - raise FailedSubmissionException + self.display.show_error(f'"{Path(study.path).name}": was not submitted', LOG_NAME) diff --git a/antareslauncher/use_cases/launch/study_zip_cleaner.py b/antareslauncher/use_cases/launch/study_zip_cleaner.py deleted file mode 100644 index 5967240..0000000 --- a/antareslauncher/use_cases/launch/study_zip_cleaner.py +++ /dev/null @@ -1,17 +0,0 @@ -import copy - -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.file_manager.file_manager import FileManager -from antareslauncher.study_dto import StudyDTO - - -class StudyZipCleaner: - def __init__(self, file_manager: FileManager, display: IDisplay): - self.file_manager = file_manager - self.display = display - self._current_study: StudyDTO = StudyDTO("none") - - def remove_input_zipfile(self, study: StudyDTO) -> StudyDTO: - self._current_study = copy.deepcopy(study) - self.file_manager.remove_file(self._current_study.zipfile_path) - return self._current_study diff --git a/antareslauncher/use_cases/launch/study_zip_uploader.py b/antareslauncher/use_cases/launch/study_zip_uploader.py index b98fabe..beb375e 100644 --- a/antareslauncher/use_cases/launch/study_zip_uploader.py +++ b/antareslauncher/use_cases/launch/study_zip_uploader.py @@ -1,53 +1,31 @@ -import copy -from pathlib import Path - -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO +LOG_NAME = f"{__name__}.StudyZipfileUploader" + class StudyZipfileUploader: - def __init__(self, env: RemoteEnvironmentWithSlurm, display: IDisplay): + def __init__(self, env: RemoteEnvironmentWithSlurm, display: DisplayTerminal): self.env = env self.display = display - self._current_study: StudyDTO = None - - def upload(self, study) -> StudyDTO: - self._current_study = copy.deepcopy(study) - if self._current_study.zip_is_sent is False: - self._do_upload() - return self._current_study - def _do_upload(self): - self._display_welcome_message() - success = self.env.upload_file(self._current_study.zipfile_path) - if success: - self._current_study.zip_is_sent = True - self._display_success_message() + def upload(self, study: StudyDTO) -> None: + if study.zip_is_sent: + self.display.show_message(f'"{study.name}": ZIP is already uploaded', LOG_NAME) + return + self.display.show_message(f'"{study.name}": uploading study...', LOG_NAME) + study.zip_is_sent = self.env.upload_file(study.zipfile_path) + if study.zip_is_sent: + self.display.show_message(f'"{study.name}": was uploaded', LOG_NAME) else: - self._display_failure_error() - raise FailedUploadException - - def _display_failure_error(self): - self.display.show_error( - f'"{Path(self._current_study.path).name}": was not uploaded', - __name__ + "." + __class__.__name__, - ) - - def _display_success_message(self): - self.display.show_message( - f'"{Path(self._current_study.path).name}": was uploaded', - __name__ + "." + __class__.__name__, - ) - - def _display_welcome_message(self): - self.display.show_message( - f'"{Path(self._current_study.path).name}": uploading study ...', - __name__ + "." + __class__.__name__, - ) - - -class FailedUploadException(Exception): - pass + self.display.show_error(f'"{study.name}": was NOT uploaded', LOG_NAME) + + def remove(self, study: StudyDTO) -> None: + # The remote ZIP file is always removed even if `zip_is_sent` is `False` + # because the ZIP file may be partially uploaded (before a failure). + study.zip_is_sent = not self.env.remove_input_zipfile(study) + if not study.zip_is_sent: + self.display.show_message(f'"{study.name}": ZIP is removed from remote', LOG_NAME) + else: + self.display.show_error(f'"{study.name}": ZIP is NOT removed from remote', LOG_NAME) diff --git a/antareslauncher/use_cases/launch/study_zipper.py b/antareslauncher/use_cases/launch/study_zipper.py deleted file mode 100644 index c578696..0000000 --- a/antareslauncher/use_cases/launch/study_zipper.py +++ /dev/null @@ -1,46 +0,0 @@ -import copy -import getpass -from dataclasses import dataclass -from pathlib import Path - -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.file_manager.file_manager import FileManager -from antareslauncher.study_dto import StudyDTO - - -@dataclass -class StudyZipper: - def __init__(self, file_manager: FileManager, display: IDisplay): - self.file_manager = file_manager - self.display = display - self._current_study: StudyDTO = None - - def zip(self, study) -> StudyDTO: - self._current_study = copy.deepcopy(study) - - if study.zipfile_path == "": - self._do_zip() - return self._current_study - - def _do_zip(self): - zipfile_path = f"{self._current_study.path}-{getpass.getuser()}.zip" - success = self.file_manager.zip_dir_excluding_subdir( - self._current_study.path, zipfile_path, None - ) - if success is True: - self._current_study.zipfile_path = zipfile_path - self._display_success_message() - else: - self._display_failure_error() - - def _display_failure_error(self): - self.display.show_error( - f'"{Path(self._current_study.path).name}": was not zipped', - f"{__name__}.{__class__.__name__}", - ) - - def _display_success_message(self): - self.display.show_message( - f'"{Path(self._current_study.path).name}": was zipped', - f"{__name__}.{__class__.__name__}", - ) diff --git a/antareslauncher/use_cases/retrieve/clean_remote_server.py b/antareslauncher/use_cases/retrieve/clean_remote_server.py index 744e9f2..05391b4 100644 --- a/antareslauncher/use_cases/retrieve/clean_remote_server.py +++ b/antareslauncher/use_cases/retrieve/clean_remote_server.py @@ -1,61 +1,43 @@ -import copy -from pathlib import Path - -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO - -class RemoteServerNotCleanException(Exception): - pass +LOG_NAME = f"{__name__}.RemoteServerCleaner" class RemoteServerCleaner: def __init__( self, env: RemoteEnvironmentWithSlurm, - display: IDisplay, + display: DisplayTerminal, ): self._display = display self._env = env - self._current_study: StudyDTO = None def clean(self, study: StudyDTO): - self._current_study = copy.copy(study) - if self._should_clean_remote_server(): - self._do_clean_remote_server() - return self._current_study - - def _should_clean_remote_server(self): - return ( - self._current_study.remote_server_is_clean is False - ) and self._final_zip_downloaded() - - def _final_zip_downloaded(self) -> bool: - if isinstance(self._current_study.local_final_zipfile_path, str): - return bool(self._current_study.local_final_zipfile_path) - else: - return False - - def _do_clean_remote_server(self): - success = self._env.clean_remote_server(copy.copy(self._current_study)) - if success is True: - self._current_study.remote_server_is_clean = success - self._display_success_message() - else: - self._display_failure_error() - raise RemoteServerNotCleanException - - def _display_failure_error(self): - self._display.show_error( - f'"{Path(self._current_study.path).name}": Clean remote server failed', - __name__ + "." + __class__.__name__, - ) - - def _display_success_message(self): - self._display.show_message( - f'"{Path(self._current_study.path).name}": Clean remote server finished', - __name__ + "." + __class__.__name__, - ) + if not study.remote_server_is_clean and study.local_final_zipfile_path: + # If the cleanup procedure fails to remove remote files or + # delete the final ZIP, there's no need to raise an exception. + # Instead, it's sufficient to issue a warning to alert the user. + try: + removed = self._env.clean_remote_server(study) + except Exception as exc: + self._display.show_error( + f'"{study.name}": Clean remote server raised: {exc}', + LOG_NAME, + ) + else: + if removed: + self._display.show_message( + f'"{study.name}": Clean remote server finished', + LOG_NAME, + ) + else: + self._display.show_error( + f'"{study.name}": Clean remote server failed', + LOG_NAME, + ) + + # However, in such cases, it's advisable to indicate that the cleanup + # was successful to prevent an infinite loop. + study.remote_server_is_clean = True diff --git a/antareslauncher/use_cases/retrieve/download_final_zip.py b/antareslauncher/use_cases/retrieve/download_final_zip.py index ed5c96a..6179573 100644 --- a/antareslauncher/use_cases/retrieve/download_final_zip.py +++ b/antareslauncher/use_cases/retrieve/download_final_zip.py @@ -1,25 +1,20 @@ -import copy +from pathlib import Path -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO - -class FinalZipNotDownloadedException(Exception): - pass +LOG_NAME = f"{__name__}.FinalZipDownloader" class FinalZipDownloader(object): def __init__( self, env: RemoteEnvironmentWithSlurm, - display: IDisplay, + display: DisplayTerminal, ): self._env = env self._display = display - self._current_study = None def download(self, study: StudyDTO): """ @@ -32,57 +27,23 @@ def download(self, study: StudyDTO): Returns: The updated data transfer object, with its `local_final_zipfile_path` attribute set if the download was successful. - - Raises: - FinalZipNotDownloadedException: If the download fails or no files are found. - """ - self._current_study = copy.copy(study) - if ( - self._current_study.finished - and not self._current_study.with_error - and not self._current_study.local_final_zipfile_path - ): - self._do_download() - return self._current_study - - def _do_download(self): """ - Perform the download of the final ZIP file for the current study, - and update its `local_final_zipfile_path` attribute. - - Raises: - FinalZipNotDownloadedException: If the download fails or no files are found. - - Note: - This function delegates the download operation to the - `_env.download_final_zip` method, which is assumed to return - the path to the downloaded zip file on the local filesystem - or `None` if the download fails or no files are found. - - If the download succeeds, the `local_final_zipfile_path` attribute - of the `_current_study` object is updated with the path to the - downloaded file, and a success message is displayed. - - If the download fails, an error message is displayed and a - `FinalZipNotDownloadedException` exception is raised. - """ - self._display.show_message( - f'"{self._current_study.name}": downloading final ZIP...', - f"{__name__}.{__class__.__name__}", - ) - if local_final_zipfile_path := self._env.download_final_zip( - copy.copy(self._current_study) - ): - self._current_study.local_final_zipfile_path = str(local_final_zipfile_path) + if study.finished and not study.with_error and not study.local_final_zipfile_path: self._display.show_message( - f'"{self._current_study.name}": Final ZIP downloaded', - f"{__name__}.{__class__.__name__}", - ) - else: - self._display.show_error( - f'"{self._current_study.name}": Final ZIP not downloaded', - f"{__name__}.{__class__.__name__}", - ) - raise FinalZipNotDownloadedException( - self._current_study.local_final_zipfile_path + f'"{study.name}": downloading final ZIP...', + LOG_NAME, ) + dst_dir = Path(study.output_dir) + dst_dir.mkdir(parents=True, exist_ok=True) + zip_path = self._env.download_final_zip(study) + study.local_final_zipfile_path = str(zip_path) if zip_path else "" + if study.local_final_zipfile_path: + self._display.show_message( + f'"{study.name}": Final ZIP downloaded', + LOG_NAME, + ) + else: + self._display.show_error( + f'"{study.name}": Final ZIP NOT downloaded', + LOG_NAME, + ) diff --git a/antareslauncher/use_cases/retrieve/final_zip_extractor.py b/antareslauncher/use_cases/retrieve/final_zip_extractor.py index c6e99ed..81d02cb 100644 --- a/antareslauncher/use_cases/retrieve/final_zip_extractor.py +++ b/antareslauncher/use_cases/retrieve/final_zip_extractor.py @@ -1,50 +1,62 @@ +import os.path +import zipfile from pathlib import Path -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.file_manager.file_manager import FileManager +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.study_dto import StudyDTO - -class ResultNotExtractedException(Exception): - pass +LOG_NAME = f"{__name__}.FinalZipDownloader" class FinalZipExtractor: - def __init__(self, file_manager: FileManager, display: IDisplay): - self._file_manager = file_manager + def __init__(self, display: DisplayTerminal): self._display = display - self._current_study: StudyDTO = None - - def extract_final_zip(self, study: StudyDTO) -> StudyDTO: - self._current_study = study - if self._study_final_zip_should_be_extracted(): - self._do_extract() - return self._current_study - - def _do_extract(self): - zipfile_to_extract = self._current_study.local_final_zipfile_path - success = self._file_manager.unzip(zipfile_to_extract) - if success: - self._current_study.final_zip_extracted = success - self._show_success_message() - else: - self._show_failure_error() - raise ResultNotExtractedException - - def _show_failure_error(self): - self._display.show_error( - f'"{Path(self._current_study.path).name}": Final zip not extracted', - __name__ + "." + __class__.__name__, - ) - - def _show_success_message(self): - self._display.show_message( - f'"{Path(self._current_study.path).name}": Final zip extracted', - __name__ + "." + __class__.__name__, - ) - - def _study_final_zip_should_be_extracted(self): - return ( - self._current_study.local_final_zipfile_path - and not self._current_study.final_zip_extracted - ) + + def extract_final_zip(self, study: StudyDTO) -> None: + """ + Extracts the simulation results, which are in the form of a ZIP file, + after it has been downloaded from Antares. + + Args: + study: The current study + """ + if study.finished and not study.with_error and study.local_final_zipfile_path and not study.final_zip_extracted: + zip_path = Path(study.local_final_zipfile_path) + try: + with zipfile.ZipFile(zip_path) as zf: + names = zf.namelist() + if len(names) > 1 and os.path.commonpath(names): + # If all files are in the same directory, we can extract the ZIP + # file directly in the target directory. + target_dir = zip_path.parent + else: + # Otherwise, we need to create a directory to store the results. + # This situation occurs when the ZIP file contains + # only the simulation results and not the entire study. + target_dir = zip_path.with_suffix("") + + progress_bar = self._display.generate_progress_bar( + names, desc="Extracting archive:", total=len(names) + ) + for file in progress_bar: + zf.extract(member=file, path=target_dir) + + except (OSError, zipfile.BadZipFile) as exc: + # If we cannot extract the final ZIP file, either because the file + # doesn't exist or the ZIP file is corrupted, we find ourselves + # in a situation where the results are unusable. + # In such cases, it's best to consider the simulation as failed, + # enabling the user to restart its simulation. + study.final_zip_extracted = False + study.with_error = True + self._display.show_error( + f'"{study.name}": Final zip not extracted: {exc}', + LOG_NAME, + ) + + else: + study.final_zip_extracted = True + self._display.show_message( + f'"{study.name}": Final zip extracted', + LOG_NAME, + ) diff --git a/antareslauncher/use_cases/retrieve/log_downloader.py b/antareslauncher/use_cases/retrieve/log_downloader.py index 22248d9..8339145 100644 --- a/antareslauncher/use_cases/retrieve/log_downloader.py +++ b/antareslauncher/use_cases/retrieve/log_downloader.py @@ -1,63 +1,50 @@ -import copy from pathlib import Path -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.file_manager.file_manager import FileManager -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO +LOG_NAME = f"{__name__}.LogDownloader" + class LogDownloader: def __init__( self, env: RemoteEnvironmentWithSlurm, - file_manager: FileManager, - display: IDisplay, + display: DisplayTerminal, ): self.env = env self.display = display - self.file_manager = file_manager - self._current_study = None - - def _create_logs_subdirectory(self): - self._set_current_study_log_dir_path() - self.file_manager.make_dir(self._current_study.job_log_dir) - - def _set_current_study_log_dir_path(self): - directory_name = self._get_log_dir_name() - if Path(self._current_study.job_log_dir).name != directory_name: - self._current_study.job_log_dir = str( - Path(self._current_study.job_log_dir) / directory_name - ) - def _get_log_dir_name(self): - return ( - Path(self._current_study.path).name + "_" + str(self._current_study.job_id) - ) - - def run(self, study: StudyDTO): - """Downloads slurm logs from the server then save study if the study is running + def run(self, study: StudyDTO) -> None: + """ + Downloads slurm logs from the server then save study if the study is running Args: study: The study data transfer object """ - self._current_study = copy.copy(study) - if self._current_study.started: - self._create_logs_subdirectory() - self._do_download_logs() - return self._current_study - - def _do_download_logs(self): - if self.env.download_logs(copy.copy(self._current_study)): - self._current_study.logs_downloaded = True - self.display.show_message( - f'"{Path(self._current_study.path).name}": Logs downloaded', - f"{__name__}.{__class__.__name__}", - ) - else: - self.display.show_error( - f'"{Path(self._current_study.path).name}": Logs not downloaded', - f"{__name__}.{__class__.__name__}", - ) + if study.started: + # set_current_study_log_dir_path + directory_name = f"{study.name}_{study.job_id}" + job_log_dir = Path(study.job_log_dir) + if job_log_dir.name != directory_name: + job_log_dir = job_log_dir / directory_name + study.job_log_dir = str(job_log_dir) + + # create logs subdirectory + job_log_dir.mkdir(parents=True, exist_ok=True) + + # make an attempt to download logs + downloaded_logs = self.env.download_logs(study) + if downloaded_logs: + study.logs_downloaded = True + self.display.show_message( + f'"{study.name}": Logs downloaded', + LOG_NAME, + ) + else: + # No file to download + self.display.show_error( + f'"{study.name}": Logs NOT downloaded', + LOG_NAME, + ) diff --git a/antareslauncher/use_cases/retrieve/retrieve_controller.py b/antareslauncher/use_cases/retrieve/retrieve_controller.py index ca67f11..a6fab0d 100644 --- a/antareslauncher/use_cases/retrieve/retrieve_controller.py +++ b/antareslauncher/use_cases/retrieve/retrieve_controller.py @@ -1,10 +1,7 @@ +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.data_repo.data_reporter import DataReporter -from antareslauncher.data_repo.idata_repo import IDataRepo -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.file_manager.file_manager import FileManager -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.use_cases.retrieve.clean_remote_server import RemoteServerCleaner from antareslauncher.use_cases.retrieve.download_final_zip import FinalZipDownloader from antareslauncher.use_cases.retrieve.final_zip_extractor import FinalZipExtractor @@ -12,28 +9,25 @@ from antareslauncher.use_cases.retrieve.state_updater import StateUpdater from antareslauncher.use_cases.retrieve.study_retriever import StudyRetriever +LOG_NAME = f"{__name__}.RetrieveController" + class RetrieveController: def __init__( self, - repo: IDataRepo, + repo: DataRepoTinydb, env: RemoteEnvironmentWithSlurm, - file_manager: FileManager, - display: IDisplay, + display: DisplayTerminal, state_updater: StateUpdater, ): self.repo = repo self.env = env - self.file_manager = file_manager self.display = display self.state_updater = state_updater - DataReporter(repo) - logs_downloader = LogDownloader( - env=self.env, file_manager=file_manager, display=self.display - ) + logs_downloader = LogDownloader(env=self.env, display=self.display) final_zip_downloader = FinalZipDownloader(env=self.env, display=self.display) - remote_server_cleaner = RemoteServerCleaner(env, display) - zip_extractor = FinalZipExtractor(file_manager, display) + remote_server_cleaner = RemoteServerCleaner(env=self.env, display=self.display) + zip_extractor = FinalZipExtractor(display=self.display) self.study_retriever = StudyRetriever( state_updater, logs_downloader, @@ -51,10 +45,7 @@ def all_studies_done(self): True if all the studies are done, False otherwise """ studies = self.repo.get_list_of_studies() - for study in studies: - if not study.done: - return False - return True + return all(study.done for study in studies) def retrieve_all_studies(self): """Retrieves all the studies and logs from the environment and process them @@ -67,16 +58,9 @@ def retrieve_all_studies(self): 5. extract result """ studies = self.repo.get_list_of_studies() - self.display.show_message( - "Retrieving all studies", - __name__ + "." + __class__.__name__, - ) + self.display.show_message("Retrieving all studies...", LOG_NAME) for study in studies: - if not study.done: - self.study_retriever.retrieve(study) + self.study_retriever.retrieve(study) if self.all_studies_done: - self.display.show_message( - "Everything is done", - __name__ + "." + __class__.__name__, - ) + self.display.show_message("All retrievals are done.", LOG_NAME) return self.all_studies_done diff --git a/antareslauncher/use_cases/retrieve/state_updater.py b/antareslauncher/use_cases/retrieve/state_updater.py index 41b0677..2c07cf0 100644 --- a/antareslauncher/use_cases/retrieve/state_updater.py +++ b/antareslauncher/use_cases/retrieve/state_updater.py @@ -1,79 +1,67 @@ -from pathlib import Path -from typing import List +import typing as t -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO +LOG_NAME = f"{__name__}.RetrieveController" + class StateUpdater: def __init__( self, env: RemoteEnvironmentWithSlurm, - display: IDisplay, + display: DisplayTerminal, ): self._env = env self._display = display - self._current_study: StudyDTO = None - def _show_job_state_message(self, study: StudyDTO): - if study.done is True: + def _show_job_state_message(self, study: StudyDTO) -> None: + if study.done: + self._display.show_message( + f'"{study.name}": (JOBID={study.job_id}): everything is done', + LOG_NAME, + ) + elif study.job_id: self._display.show_message( - f'"{Path(study.path).name}" (JOBID={study.job_id}): everything is done', - __name__ + "." + __class__.__name__, + f'"{study.name}": (JOBID={study.job_id}): {study.job_state}', + LOG_NAME, ) else: - if study.job_id: - self._display.show_message( - f'"{Path(study.path).name}" (JOBID={study.job_id}): {study.job_state}', - __name__ + "." + __class__.__name__, - ) - else: - self._display.show_error( - f'"{Path(study.path).name}": Job was not submitted', - __name__ + "." + __class__.__name__, - ) + self._display.show_error( + f'"{study.name}": Job was NOT submitted', + LOG_NAME, + ) - def run(self, study: StudyDTO) -> StudyDTO: + def run(self, study: StudyDTO) -> None: """Gets the job state flags from the environment and update the IStudyDTO flags then save study Args: study: The study data transfer object """ - self._current_study = study - if not self._current_study.done: - self._set_current_study_job_state_flags() - self._set_current_study_job_state() - self._show_job_state_message(study) - return study + if not study.done: + # set current study job state flags + if study.job_id: + s, f, e = self._env.get_job_state_flags(study) + else: + s, f, e = False, False, False + study.started = s + study.finished = f + study.with_error = e - def _set_current_study_job_state_flags(self): - if self._current_study.job_id: - s, f, e = self._env.get_job_state_flags(self._current_study) + # set current study job state + if study.with_error: + study.job_state = "Ended with error" + elif study.finished: + study.job_state = "Finished" + elif study.started: + study.job_state = "Running" else: - s, f, e = False, False, False - self._current_study.started = s - self._current_study.finished = f - self._current_study.with_error = e + study.job_state = "Pending" - def _set_current_study_job_state(self): - if self._current_study.with_error: - self._current_study.job_state = "Ended with error" - elif self._current_study.finished: - self._current_study.job_state = "Finished" - elif self._current_study.started: - self._current_study.job_state = "Running" - else: - self._current_study.job_state = "Pending" + self._show_job_state_message(study) - def run_on_list(self, study_list: List[StudyDTO]): - message = "Checking status of the studies:" - self._display.show_message( - message, - __name__ + "." + __class__.__name__, - ) - study_list.sort(key=lambda x: x.done, reverse=True) - for study in study_list: + def run_on_list(self, studies: t.Sequence[StudyDTO]) -> None: + self._display.show_message("Checking status of the studies:", LOG_NAME) + for study in sorted(studies, key=lambda x: x.done, reverse=True): self.run(study) diff --git a/antareslauncher/use_cases/retrieve/study_retriever.py b/antareslauncher/use_cases/retrieve/study_retriever.py index 48fa7c6..8d1ea99 100644 --- a/antareslauncher/use_cases/retrieve/study_retriever.py +++ b/antareslauncher/use_cases/retrieve/study_retriever.py @@ -23,48 +23,26 @@ def __init__( self.remote_server_cleaner = remote_server_cleaner self.zip_extractor = zip_extractor self.reporter = reporter - self._current_study: StudyDTO = None - - def _update_job_state_flags(self): - self._current_study = self.state_updater.run(self._current_study) - self.reporter.save_study(self._current_study) - - def _download_slurm_logs(self): - self._current_study = self.logs_downloader.run(self._current_study) - self.reporter.save_study(self._current_study) - - def _download_final_zip(self): - self._current_study = self.final_zip_downloader.download(self._current_study) - self.reporter.save_study(self._current_study) - - def _clean_remote_server(self): - self._current_study = self.remote_server_cleaner.clean(self._current_study) - self.reporter.save_study(self._current_study) - - def _extract_study_result(self): - self._current_study = self.zip_extractor.extract_final_zip(self._current_study) - self.reporter.save_study(self._current_study) - - def _check_if_done(self): - done = self.check_if_study_is_done(self._current_study) - self._current_study.done = done - self.reporter.save_study(self._current_study) def retrieve(self, study: StudyDTO): - self._current_study = study - if not self._current_study.done: - self._update_job_state_flags() - self._download_slurm_logs() - self._download_final_zip() - self._clean_remote_server() - self._extract_study_result() - self._check_if_done() - - @staticmethod - def check_if_study_is_done(study: StudyDTO): - return study.with_error or ( - study.logs_downloaded - and study.local_final_zipfile_path - and study.remote_server_is_clean - and study.final_zip_extracted - ) + if not study.done: + try: + self.state_updater.run(study) + self.logs_downloader.run(study) + self.final_zip_downloader.download(study) + self.remote_server_cleaner.clean(study) + self.zip_extractor.extract_final_zip(study) + study.done = study.with_error or ( + study.logs_downloaded + and bool(study.local_final_zipfile_path) + and study.remote_server_is_clean + and study.final_zip_extracted + ) + + except Exception as e: + # The exception is not re-raised, but the job is marked as failed + study.with_error = True + study.job_state = f"Internal error: {e}" + + finally: + self.reporter.save_study(study) diff --git a/antareslauncher/use_cases/wait_loop_controller/wait_controller.py b/antareslauncher/use_cases/wait_loop_controller/wait_controller.py index 1d4e2db..c540ddb 100644 --- a/antareslauncher/use_cases/wait_loop_controller/wait_controller.py +++ b/antareslauncher/use_cases/wait_loop_controller/wait_controller.py @@ -1,12 +1,12 @@ import time from dataclasses import dataclass -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal @dataclass class WaitController: - display: IDisplay + display: DisplayTerminal def countdown(self, seconds_to_wait: int): """ @@ -38,8 +38,6 @@ def _wait_loop(self, seconds_to_wait: int) -> None: mins, secs = divmod(seconds_to_wait, 60) formatted_time = "{:02d}:{:02d}".format(mins, secs) - self.display.show_message( - text_4_countdown + formatted_time, __name__, end="\r" - ) + self.display.show_message(text_4_countdown + formatted_time, __name__, end="\r") time.sleep(seconds_between_messages) seconds_to_wait -= seconds_between_messages diff --git a/data/README.md b/data/README.md index 1895066..851382c 100644 --- a/data/README.md +++ b/data/README.md @@ -25,6 +25,8 @@ DB_PRIMARY_KEY : "name" DEFAULT_SSH_CONFIGFILE_NAME: "ssh_config.json" SSH_CONFIG_FILE_IS_REQUIRED : False SLURM_SCRIPT_PATH : "/opt/antares/launchAntares.sh" +PARTITION : "compute1" +QUALITY_OR_SERVICE : "user1_qos" ANTARES_VERSIONS_ON_REMOTE_SERVER : - "610" @@ -51,6 +53,11 @@ Below is a description of the parameters: - `DEFAULT_SSH_CONFIGFILE_NAME`: The default name of the SSH configuration file, it should be "ssh_config.json". - `SSH_CONFIG_FILE_IS_REQUIRED`: A flag indicating whether an SSH configuration file is required. - `SLURM_SCRIPT_PATH`: Path to the SLURM script used to launch studies (a Shell script). +- `PARTITION`: Extra `sbatch` option to request a specific partition for resource allocation. + If not specified, the default behavior is to allow the SLURM controller + to select the default partition as designated by the system administrator. +- `QUALITY_OF_SERVICE`: Extra `sbatch` option to request a quality of service for the job. + QOS values can be defined for each user/cluster/account association in the Slurm database. - `ANTARES_VERSIONS_ON_REMOTE_SERVER`: A list of strings representing the available Antares Solver versions on the remote server. ## SSH Configuration diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e56b84b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[tool.black] +target-version = ["py38"] +line-length = 120 +exclude = "(data/*|docs/*|remote_scripts_templates/*|target/*)" + +[tool.isort] +profile = "black" +line_length = 120 +src_paths = ["antareslauncher", "tests"] +skip_gitignore = true +extend_skip_glob = [ + "data/*", + "doc/*", + "remote_scripts_templates/*", + "target/*", +] diff --git a/tests/integration/test_integration_check_queue_controller.py b/tests/integration/test_integration_check_queue_controller.py index 91e781f..17baf93 100644 --- a/tests/integration/test_integration_check_queue_controller.py +++ b/tests/integration/test_integration_check_queue_controller.py @@ -2,19 +2,11 @@ import pytest -from antareslauncher.data_repo.idata_repo import IDataRepo -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) -from antareslauncher.remote_environnement.slurm_script_features import ( - SlurmScriptFeatures, -) -from antareslauncher.use_cases.check_remote_queue.check_queue_controller import ( - CheckQueueController, -) -from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import ( - SlurmQueueShow, -) +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm +from antareslauncher.remote_environnement.slurm_script_features import SlurmScriptFeatures +from antareslauncher.use_cases.check_remote_queue.check_queue_controller import CheckQueueController +from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import SlurmQueueShow from antareslauncher.use_cases.retrieve.state_updater import StateUpdater @@ -23,7 +15,11 @@ def setup_method(self): self.connection_mock = mock.Mock(home_dir="path/to/home") self.connection_mock.username = "username" self.connection_mock.execute_command = mock.Mock(return_value=("", "")) - slurm_script_features = SlurmScriptFeatures("slurm_script_path") + slurm_script_features = SlurmScriptFeatures( + "slurm_script_path", + partition="fake_partition", + quality_of_service="user1_qos", + ) env_mock = RemoteEnvironmentWithSlurm( _connection=self.connection_mock, slurm_script_features=slurm_script_features, @@ -31,10 +27,8 @@ def setup_method(self): display_mock = mock.Mock() slurm_queue_show = SlurmQueueShow(env_mock, display_mock) state_updater = StateUpdater(env_mock, display_mock) - repo = mock.MagicMock(spec=IDataRepo) - self.check_queue_controller = CheckQueueController( - slurm_queue_show, state_updater, repo - ) + repo = mock.MagicMock(spec=DataRepoTinydb) + self.check_queue_controller = CheckQueueController(slurm_queue_show, state_updater, repo) @pytest.mark.integration_test def test_check_queue_controller_check_queue_calls_connection_execute_command( diff --git a/tests/integration/test_integration_job_kill_controller.py b/tests/integration/test_integration_job_kill_controller.py index e8c8731..861d26a 100644 --- a/tests/integration/test_integration_job_kill_controller.py +++ b/tests/integration/test_integration_job_kill_controller.py @@ -2,20 +2,18 @@ import pytest -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) -from antareslauncher.remote_environnement.slurm_script_features import ( - SlurmScriptFeatures, -) -from antareslauncher.use_cases.kill_job.job_kill_controller import ( - JobKillController, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm +from antareslauncher.remote_environnement.slurm_script_features import SlurmScriptFeatures +from antareslauncher.use_cases.kill_job.job_kill_controller import JobKillController class TestIntegrationJobKilController: def setup_method(self): - slurm_script_features = SlurmScriptFeatures("slurm_script_path") + slurm_script_features = SlurmScriptFeatures( + "slurm_script_path", + partition="fake_partition", + quality_of_service="user1_qos", + ) connection = mock.Mock(home_dir="path/to/home") env = RemoteEnvironmentWithSlurm(connection, slurm_script_features) self.job_kill_controller = JobKillController(env, mock.Mock(), repo=mock.Mock()) @@ -26,12 +24,8 @@ def test_job_kill_controller_kill_job_calls_connection_execute_command( ): # given job_id = 42 - self.job_kill_controller.env.connection.execute_command = mock.Mock( - return_value=("", "") - ) + self.job_kill_controller.env.connection.execute_command = mock.Mock(return_value=("", "")) # when self.job_kill_controller.kill_job(job_id) # then - self.job_kill_controller.env.connection.execute_command.assert_called_once_with( - f"scancel {job_id}" - ) + self.job_kill_controller.env.connection.execute_command.assert_called_once_with(f"scancel {job_id}") diff --git a/tests/integration/test_integration_launch_controller.py b/tests/integration/test_integration_launch_controller.py deleted file mode 100644 index 412e5fa..0000000 --- a/tests/integration/test_integration_launch_controller.py +++ /dev/null @@ -1,138 +0,0 @@ -import getpass -import socket -from pathlib import Path -from unittest import mock - -import pytest - -from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) -from antareslauncher.remote_environnement.slurm_script_features import ( - SlurmScriptFeatures, -) -from antareslauncher.study_dto import StudyDTO -from antareslauncher.use_cases.launch.launch_controller import LaunchController - - -class TestIntegrationLaunchController: - @pytest.fixture(scope="function") - def launch_controller(self): - connection = mock.Mock(home_dir="path/to/home") - slurm_script_features = SlurmScriptFeatures("slurm_script_path") - environment = RemoteEnvironmentWithSlurm(connection, slurm_script_features) - study1 = mock.Mock() - study1.zipfile_path = "filepath" - study1.zip_is_sent = False - study1.path = "path" - study2 = mock.Mock() - study2.zipfile_path = "filepath" - study2.zip_is_sent = False - study2.path = "path" - - data_repo = mock.Mock() - data_repo.get_list_of_studies = mock.Mock(return_value=[study1, study2]) - file_manager = mock.Mock() - display = DisplayTerminal() - launch_controller = LaunchController( - repo=data_repo, - env=environment, - file_manager=file_manager, - display=display, - ) - - return launch_controller - - @pytest.mark.integration_test - def test_upload_file__called_twice(self, launch_controller): - """ - This test function checks if when launching two studies through the controller, - the call to the 'launch_all_studies' method triggers the 'upload_file' method - of the connection twice. - """ - # when - launch_controller.launch_all_studies() - - # then - # noinspection PyUnresolvedReferences - assert launch_controller.env.connection.upload_file.call_count == 2 - - @pytest.mark.integration_test - def test_execute_command__called_with_the_correct_parameters( - self, - ): - """ - This test function checks if when launching a study through the controller, - the call to the `submit_job` method triggers the `execute_command` method - of the connection only once, with the correct command. - """ - # given - connection = mock.Mock() - connection.execute_command = mock.Mock(return_value=["Submitted 42", ""]) - connection.home_dir = "Submitted" - slurm_script_features = SlurmScriptFeatures("slurm_script_path") - environment = RemoteEnvironmentWithSlurm(connection, slurm_script_features) - study1 = StudyDTO( - path="dummy_path", - zipfile_path=str(Path("base_path") / "zip_name"), - zip_is_sent=False, - n_cpu=12, - antares_version="700", - time_limit=120, - ) - home_dir = "Submitted" - - remote_base_path = ( - str(home_dir) + "/REMOTE_" + getpass.getuser() + "_" + socket.gethostname() - ) - - zipfile_name = Path(study1.zipfile_path).name - job_type = "ANTARES" - post_processing = False - other_options = "" - bash_options = ( - f'"{zipfile_name}"' - f" {study1.antares_version}" - f" {job_type}" - f" {post_processing}" - f" '{other_options}'" - ) - command = ( - f"cd {remote_base_path} && " - f'sbatch --job-name="{Path(study1.path).name}"' - f" --time={study1.time_limit // 60}" - f" --cpus-per-task={study1.n_cpu}" - f" {environment.slurm_script_features.solver_script_path}" - f" {bash_options}" - ) - - data_repo = mock.Mock() - data_repo.get_list_of_studies = mock.Mock(return_value=[study1]) - file_manager = mock.Mock() - display = DisplayTerminal() - launch_controller = LaunchController( - repo=data_repo, - env=environment, - file_manager=file_manager, - display=display, - ) - # when - launch_controller.launch_all_studies() # _submit_job(study1) - - # then - connection.execute_command.assert_called_once_with(command) - - @pytest.mark.integration_test - def test_remove_zip_file__called_twice(self, launch_controller): - """ - This test function checks if when executing the `launch_all_studies` with two sent studies, - the `remove_zip_file` method is called twice. - """ - launch_controller.file_manager.remove_file = mock.Mock() - - # when - launch_controller.launch_all_studies() - - # then - assert launch_controller.file_manager.remove_file.call_count == 2 diff --git a/tests/integration/test_integration_study_list_composer.py b/tests/integration/test_integration_study_list_composer.py deleted file mode 100644 index 1155ba1..0000000 --- a/tests/integration/test_integration_study_list_composer.py +++ /dev/null @@ -1,55 +0,0 @@ -from pathlib import Path -from unittest import mock - -import pytest - -from antareslauncher.use_cases.create_list.study_list_composer import ( - StudyListComposer, - StudyListComposerParameters, -) - - -class TestIntegrationStudyListComposer: - def setup_method(self): - self.study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(), - display=mock.Mock(), - parameters=StudyListComposerParameters( - studies_in_dir="studies_in", - time_limit=42, - n_cpu=24, - log_dir="job_log_dir", - xpansion_mode=None, - output_dir="output_dir", - post_processing=False, - antares_versions_on_remote_server=["700"], - other_options="", - ), - ) - - @pytest.mark.integration_test - def test_study_list_composer_get_list_of_studies_calls_repo_get_list_of_studies( - self, - ): - self.study_list_composer.get_list_of_studies() - self.study_list_composer._repo.get_list_of_studies.assert_called_once() - - @pytest.mark.integration_test - def test_study_list_composer_get_antares_version_calls_file_manager_get_config_from_file( - self, - ): - # given - directory_path = "directory_path" - file_path = Path(directory_path) / "study.antares" - self.study_list_composer._file_manager.get_config_from_file = mock.Mock( - return_value={} - ) - # when - self.study_list_composer.get_antares_version(directory_path) - # then - self.study_list_composer._file_manager.get_config_from_file.assert_called_once_with( - file_path - ) - - # TODO: test_update_study_database already in unit tests? diff --git a/tests/unit/assets/__init__.py b/tests/unit/assets/__init__.py new file mode 100644 index 0000000..773f16e --- /dev/null +++ b/tests/unit/assets/__init__.py @@ -0,0 +1,3 @@ +from pathlib import Path + +ASSETS_DIR = Path(__file__).parent.resolve() diff --git a/tests/unit/assets/study_list_composer/studies/013 TS Generation - Solar power/README.md b/tests/unit/assets/study_list_composer/studies/013 TS Generation - Solar power/README.md new file mode 100644 index 0000000..641398f --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/013 TS Generation - Solar power/README.md @@ -0,0 +1,3 @@ +# Description + +This directory contains a study with a valid `version` number and a valide `solver_version` number. \ No newline at end of file diff --git a/tests/unit/assets/study_list_composer/studies/013 TS Generation - Solar power/study.antares b/tests/unit/assets/study_list_composer/studies/013 TS Generation - Solar power/study.antares new file mode 100644 index 0000000..9918bed --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/013 TS Generation - Solar power/study.antares @@ -0,0 +1,7 @@ +[antares] +version = 800 +caption = 013 TS Generation - Solar power +created = 1246524135 +lastsave = 1608213721 +author = Robert SMITH +solver_version = 850 diff --git a/tests/unit/assets/study_list_composer/studies/024 Hurdle costs - 1/README.md b/tests/unit/assets/study_list_composer/studies/024 Hurdle costs - 1/README.md new file mode 100644 index 0000000..4bd9f24 --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/024 Hurdle costs - 1/README.md @@ -0,0 +1,3 @@ +# Description + +This directory contains a classic study with a valid `version` number. diff --git a/tests/unit/assets/study_list_composer/studies/024 Hurdle costs - 1/study.antares b/tests/unit/assets/study_list_composer/studies/024 Hurdle costs - 1/study.antares new file mode 100644 index 0000000..0e69154 --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/024 Hurdle costs - 1/study.antares @@ -0,0 +1,7 @@ +[antares] +version = 840 +caption = 024 Hurdle costs - 1 +created = 1258636851 +lastsave = 1608213740 +author = Pink Floyd + diff --git a/tests/unit/assets/study_list_composer/studies/069 Hydro Reservoir Model/README.md b/tests/unit/assets/study_list_composer/studies/069 Hydro Reservoir Model/README.md new file mode 100644 index 0000000..539264e --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/069 Hydro Reservoir Model/README.md @@ -0,0 +1,3 @@ +# Description + +This directory contains an "old" study: `version` < 800. \ No newline at end of file diff --git a/tests/unit/assets/study_list_composer/studies/069 Hydro Reservoir Model/study.antares b/tests/unit/assets/study_list_composer/studies/069 Hydro Reservoir Model/study.antares new file mode 100644 index 0000000..97ae384 --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/069 Hydro Reservoir Model/study.antares @@ -0,0 +1,7 @@ +[antares] +version = 740 +caption = 069 Hydro Reservoir Model +created = 1293630068 +lastsave = 1608214118 +author = Vercingétorix + diff --git a/tests/unit/assets/study_list_composer/studies/BAD Study Section/README.md b/tests/unit/assets/study_list_composer/studies/BAD Study Section/README.md new file mode 100644 index 0000000..e87f6be --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/BAD Study Section/README.md @@ -0,0 +1,6 @@ +# Description + +This directory contains a BAD study. + +The content of the file [study.antares](study.antares) is wrong because the section is named `[solaris]` instead +of `[antares]`. \ No newline at end of file diff --git a/tests/unit/assets/study_list_composer/studies/BAD Study Section/study.antares b/tests/unit/assets/study_list_composer/studies/BAD Study Section/study.antares new file mode 100644 index 0000000..50d9004 --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/BAD Study Section/study.antares @@ -0,0 +1,6 @@ +[solaris] +version = 820 +caption = BAD Study Section +created = 1258636851 +lastsave = 1608213740 +author = Luc BESSON diff --git a/tests/unit/assets/study_list_composer/studies/MISSING Study version/README.md b/tests/unit/assets/study_list_composer/studies/MISSING Study version/README.md new file mode 100644 index 0000000..d4787fe --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/MISSING Study version/README.md @@ -0,0 +1,5 @@ +# Description + +This directory contains a BAD study. + +The content of the file [study.antares](study.antares) is wrong because the `version` option is missing in the `[antares]` section. \ No newline at end of file diff --git a/tests/unit/assets/study_list_composer/studies/MISSING Study version/study.antares b/tests/unit/assets/study_list_composer/studies/MISSING Study version/study.antares new file mode 100644 index 0000000..9879b83 --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/MISSING Study version/study.antares @@ -0,0 +1,5 @@ +[solaris] +caption = MISSING Study version +created = 1258636851 +lastsave = 1608213740 +author = Lady GAGA diff --git a/tests/unit/assets/study_list_composer/studies/SMTA-case/README.md b/tests/unit/assets/study_list_composer/studies/SMTA-case/README.md new file mode 100644 index 0000000..4a76da0 --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/SMTA-case/README.md @@ -0,0 +1,3 @@ +# Description + +This directory contains a study parametrized for Xpansion. diff --git a/tests/unit/assets/study_list_composer/studies/SMTA-case/study.antares b/tests/unit/assets/study_list_composer/studies/SMTA-case/study.antares new file mode 100644 index 0000000..01ad4a4 --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/SMTA-case/study.antares @@ -0,0 +1,7 @@ +[antares] +version = 810 +caption = SMTA-case +created = 1480683452 +lastsave = 1555333928 +author = John DOE + diff --git a/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/candidates.ini b/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/candidates.ini new file mode 100644 index 0000000..9c514f0 --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/candidates.ini @@ -0,0 +1,535 @@ +[1] +name = gridNEASUPS +link = North-East Asia - UPS +annual-cost-per-mw = 35104 +max-investment = 18750000 + +[2] +name = gridCASNEAS +link = Central Asia - North-East Asia +annual-cost-per-mw = 36454 +max-investment = 18750000 + +[3] +name = gridCASUPS +link = Central Asia - UPS +annual-cost-per-mw = 27003 +max-investment = 18750000 + +[4] +name = gridEURUPS +link = Europe - UPS +annual-cost-per-mw = 36454 +max-investment = 18750000 + +[5] +name = gridEURMEAST +link = Europe - Middle East +annual-cost-per-mw = 51306 +max-investment = 18750000 + +[6] +name = gridEURNAF +link = Europe - North Africa +annual-cost-per-mw = 85585 +max-investment = 18750000 + +[7] +name = gridAFRNAF +link = Africa - North Africa +annual-cost-per-mw = 87086 +max-investment = 18750000 + +[8] +name = gridAFRMEAST +link = Africa - Middle East +annual-cost-per-mw = 52019 +max-investment = 18750000 + +[9] +name = gridMEASTNAF +link = Middle East - North Africa +annual-cost-per-mw = 36454 +max-investment = 18750000 + +[10] +name = gridMEASTUPS +link = Middle East - UPS +annual-cost-per-mw = 55357 +max-investment = 18750000 + +[11] +name = gridCASMEAST +link = Central Asia - Middle East +annual-cost-per-mw = 36454 +max-investment = 18750000 + +[12] +name = gridCASSAS +link = Central Asia - South Asia +annual-cost-per-mw = 10651 +max-investment = 18750000 + +[13] +name = gridNEASSAS +link = North-East Asia - South Asia +annual-cost-per-mw = 27003 +max-investment = 18750000 + +[14] +name = gridSASSEAS +link = South Asia - South-East Asia +annual-cost-per-mw = 29704 +max-investment = 18750000 + +[15] +name = gridNEASSEAS +link = North-East Asia - South-East Asia +annual-cost-per-mw = 10651 +max-investment = 18750000 + +[16] +name = gridOCEASEAS +link = Oceania - South-East Asia +annual-cost-per-mw = 232603 +max-investment = 18750000 + +[17] +name = gridNAMUPS +link = North America - UPS +annual-cost-per-mw = 149943 +max-investment = 18750000 + +[18] +name = gridNAMNAT +link = North America - North Atlantic +annual-cost-per-mw = 123615 +max-investment = 18750000 + +[19] +name = gridEURNAT +link = Europe - North Atlantic +annual-cost-per-mw = 213476 +max-investment = 18750000 + +[20] +name = gridLAMNAM +link = Latin America - North America +annual-cost-per-mw = 48606 +max-investment = 18750000 + +[21] +name = PVAFR +link = Africa - PV_AFR +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaAFR.txt + +[22] +name = PVCAS +link = Central Asia - PV_CAS +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaCAS.txt + +[23] +name = PVEUR +link = Europe - PV_EUR +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaEUR.txt + +[24] +name = PVLAM +link = Latin America - PV_LAM +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaLAM.txt + +[25] +name = PVMEAST +link = Middle East - PV_MEAST +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaMEAST.txt + +[26] +name = PVNAF +link = North Africa - PV_NAF +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaNAF.txt + +[27] +name = PVNAM +link = North America - PV_NAM +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaNAM.txt + +[28] +name = PVNAT +link = North Atlantic - PV_NAT +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaNAT.txt + +[29] +name = PVNEAS +link = North-East Asia - PV_NEAS +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaNEAS.txt + +[30] +name = PVOCEA +link = Oceania - PV_OCEA +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaOCEA.txt + +[31] +name = PVSAS +link = PV_SAS - South Asia +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaSAS.txt + +[32] +name = PVSEAS +link = PV_SEAS - South-East Asia +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaSEAS.txt + +[33] +name = PVUPS +link = PV_UPS - UPS +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaUPS.txt + +[34] +name = windAFR +link = Africa - wind_AFR +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaAFR.txt + +[35] +name = windCAS +link = Central Asia - wind_CAS +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaCAS.txt + +[36] +name = windEUR +link = Europe - wind_EUR +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaEUR.txt + +[37] +name = windLAM +link = Latin America - wind_LAM +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaLAM.txt + +[38] +name = windMEAST +link = Middle East - wind_MEAST +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaMEAST.txt + +[39] +name = windNAF +link = North Africa - wind_NAF +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaNAF.txt + +[40] +name = windNAM +link = North America - wind_NAM +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaNAM.txt + +[41] +name = windNAT +link = North Atlantic - wind_NAT +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaNAT.txt + +[42] +name = windNEAS +link = North-East Asia - wind_NEAS +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaNEAS.txt + +[43] +name = windOCEA +link = Oceania - wind_OCEA +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaOCEA.txt + +[44] +name = windSAS +link = South Asia - wind_SAS +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaSAS.txt + +[45] +name = windSEAS +link = South-East Asia - wind_SEAS +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaSEAS.txt + +[46] +name = windUPS +link = UPS - wind_UPS +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaUPS.txt + +[47] +name = OCGTAFR +link = Africa - OCGT_AFR +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[48] +name = OCGTCAS +link = Central Asia - OCGT_CAS +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[49] +name = OCGTEUR +link = Europe - OCGT_EUR +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[50] +name = OCGTLAM +link = Latin America - OCGT_LAM +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[51] +name = OCGTMEAST +link = Middle East - OCGT_MEAST +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[52] +name = OCGTNAF +link = North Africa - OCGT_NAF +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[53] +name = OCGTNAM +link = North America - OCGT_NAM +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[54] +name = OCGTNAT +link = North Atlantic - OCGT_NAT +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[55] +name = OCGTNEAS +link = North-East Asia - OCGT_NEAS +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[56] +name = OCGTOCEA +link = Oceania - OCGT_OCEA +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[57] +name = OCGTSAS +link = OCGT_SAS - South Asia +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[58] +name = OCGTSEAS +link = OCGT_SEAS - South-East Asia +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[59] +name = OCGTUPS +link = OCGT_UPS - UPS +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[60] +name = CCGTAFR +link = Africa - CCGT_AFR +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[61] +name = CCGTCAS +link = CCGT_CAS - Central Asia +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[62] +name = CCGTEUR +link = CCGT_EUR - Europe +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[63] +name = CCGTLAM +link = CCGT_LAM - Latin America +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[64] +name = CCGTMEAST +link = CCGT_MEAST - Middle East +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[65] +name = CCGTNAF +link = CCGT_NAF - North Africa +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[66] +name = CCGTNAM +link = CCGT_NAM - North America +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[67] +name = CCGTNAT +link = CCGT_NAT - North Atlantic +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[68] +name = CCGTNEAS +link = CCGT_NEAS - North-East Asia +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[69] +name = CCGTOCEA +link = CCGT_OCEA - Oceania +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[70] +name = CCGTSAS +link = CCGT_SAS - South Asia +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[71] +name = CCGTSEAS +link = CCGT_SEAS - South-East Asia +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[72] +name = CCGTUPS +link = CCGT_UPS - UPS +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[73] +name = CCGTCCSAFR +link = Africa - CCGT_CCS_AFR +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[74] +name = CCGTCCSCAS +link = CCGT_CCS_CAS - Central Asia +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[75] +name = CCGTCCSEUR +link = CCGT_CCS_EUR - Europe +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[76] +name = CCGTCCSLAM +link = CCGT_CCS_LAM - Latin America +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[77] +name = CCGTCCSMEAST +link = CCGT_CCS_MEAST - Middle East +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[78] +name = CCGTCCSNAF +link = CCGT_CCS_NAF - North Africa +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[79] +name = CCGTCCSNAM +link = CCGT_CCS_NAM - North America +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[80] +name = CCGTCCSNAT +link = CCGT_CCS_NAT - North Atlantic +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[81] +name = CCGTCCSNEAS +link = CCGT_CCS_NEAS - North-East Asia +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[82] +name = CCGTCCSOCEA +link = CCGT_CCS_OCEA - Oceania +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[83] +name = CCGTCCSSAS +link = CCGT_CCS_SAS - South Asia +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[84] +name = CCGTCCSSEAS +link = CCGT_CCS_SEAS - South-East Asia +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[85] +name = CCGTCCSUPS +link = CCGT_CCS_UPS - UPS +annual-cost-per-mw = 141030 +max-investment = 100000000 \ No newline at end of file diff --git a/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/capa/windcapaSEAS.txt b/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/capa/windcapaSEAS.txt new file mode 100644 index 0000000..a919e8d --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/capa/windcapaSEAS.txt @@ -0,0 +1,8760 @@ +0.301656541 +0.280889889 +0.26076965 +0.174149339 +0.087267559 +0.062949088 +0.037356322 +0.021674556 +0.013776659 +0.009991971 +0.010834289 +0.02074014 +0.085305839 +0.11735713 +0.093565102 +0.071996919 +0.07378086 +0.079094604 +0.077744548 +0.068354918 +0.055847017 +0.047477843 +0.04537514 +0.045599291 +0.046799505 +0.048188513 +0.052764779 +0.056212466 +0.015431623 +0.010158999 +0.012042272 +0.015211695 +0.019042084 +0.021566866 +0.02177806 +0.025170883 +0.07380215 +0.097347945 +0.078933793 +0.065336447 +0.072035577 +0.085529617 +0.093224101 +0.102384691 +0.111280759 +0.112135556 +0.114335693 +0.123510234 +0.14250836 +0.162142216 +0.174039576 +0.145217344 +0.067045734 +0.042715444 +0.028472919 +0.034746016 +0.050325153 +0.066657721 +0.080290075 +0.099624968 +0.228156846 +0.340920954 +0.351521557 +0.348558692 +0.358893371 +0.377465595 +0.414302272 +0.455158011 +0.480378409 +0.491157773 +0.490999362 +0.482550117 +0.464346521 +0.4418994 +0.422989291 +0.262944621 +0.203901305 +0.188783823 +0.162349849 +0.145892277 +0.141317367 +0.141392751 +0.147244384 +0.166551037 +0.281324972 +0.421627432 +0.462191859 +0.48939836 +0.516960916 +0.552184614 +0.594209078 +0.631982129 +0.644070007 +0.632883469 +0.613897034 +0.595012203 +0.571424145 +0.548550847 +0.527491871 +0.347473407 +0.25678261 +0.218433985 +0.189205556 +0.183191114 +0.196117785 +0.220052835 +0.241789925 +0.269367655 +0.38864628 +0.552669425 +0.60121879 +0.617540075 +0.610239611 +0.606742243 +0.606638467 +0.597372051 +0.578919873 +0.553054448 +0.54014727 +0.529208853 +0.507910971 +0.48611561 +0.465756927 +0.272397059 +0.168283028 +0.139500372 +0.11319445 +0.079992618 +0.05460596 +0.049991703 +0.062264313 +0.089746496 +0.1641645 +0.209861502 +0.202766201 +0.182318207 +0.151063998 +0.143521403 +0.154143836 +0.17186721 +0.187416351 +0.185922655 +0.17247527 +0.148662721 +0.132095055 +0.122723327 +0.119000329 +0.084583681 +0.045529562 +0.113560583 +0.205659211 +0.284294894 +0.335348501 +0.347792304 +0.364550165 +0.384227676 +0.438593932 +0.478764326 +0.482683132 +0.474213495 +0.472446092 +0.478168198 +0.494956994 +0.510551206 +0.530382938 +0.517138579 +0.463640209 +0.39843279 +0.319569286 +0.314035659 +0.33021376 +0.311968316 +0.362526997 +0.346535781 +0.299104619 +0.267163556 +0.251406705 +0.249390859 +0.262009332 +0.283544775 +0.356577322 +0.490295182 +0.50730662 +0.509000722 +0.499630596 +0.484993489 +0.472550562 +0.467027467 +0.464546624 +0.470641114 +0.473558563 +0.46973727 +0.455994004 +0.439528831 +0.418297882 +0.295468056 +0.270379941 +0.207782672 +0.148870864 +0.110366091 +0.088392006 +0.085228843 +0.104022784 +0.14046156 +0.241971501 +0.38181499 +0.431977297 +0.474100425 +0.510333579 +0.533534223 +0.540861237 +0.547437807 +0.562487928 +0.597617139 +0.640541027 +0.67775152 +0.700971787 +0.70114487 +0.687500523 +0.571369337 +0.658211783 +0.716465775 +0.720489815 +0.712176987 +0.690183673 +0.65193121 +0.609062919 +0.574626142 +0.610974011 +0.65419273 +0.656200886 +0.661987702 +0.657951209 +0.63279535 +0.595788603 +0.549903966 +0.509360914 +0.471510454 +0.432607693 +0.402801005 +0.385820418 +0.377022749 +0.371149892 +0.233037706 +0.140116143 +0.108446383 +0.061855694 +0.030938388 +0.016537404 +0.011470931 +0.011498035 +0.016340584 +0.052747718 +0.092551091 +0.098092117 +0.096759727 +0.103814682 +0.126250213 +0.156707361 +0.176576958 +0.183280592 +0.179707869 +0.182695596 +0.189486033 +0.195708296 +0.210819187 +0.223266855 +0.191448615 +0.091710995 +0.105736243 +0.128777851 +0.140802489 +0.14826984 +0.139928897 +0.126702336 +0.114912811 +0.143470953 +0.189200661 +0.217386601 +0.243216433 +0.267131838 +0.291628575 +0.315381679 +0.333846873 +0.345073573 +0.338007323 +0.335249453 +0.335682591 +0.33905809 +0.338380546 +0.335432642 +0.190440176 +0.115837533 +0.074609729 +0.048046023 +0.045832066 +0.049719943 +0.050241699 +0.046998246 +0.044267132 +0.089567159 +0.172579591 +0.204917862 +0.207529162 +0.215914263 +0.237489488 +0.262903473 +0.280001527 +0.291668521 +0.299879861 +0.300230042 +0.28315142 +0.256704945 +0.232873054 +0.211070255 +0.101604307 +0.036996815 +0.017102569 +0.00987478 +0.010743691 +0.015559839 +0.023028613 +0.032490805 +0.042073043 +0.091700915 +0.15820233 +0.157618757 +0.138431364 +0.124706338 +0.119205579 +0.123610184 +0.136734587 +0.155627557 +0.177290417 +0.208870977 +0.249295644 +0.289166501 +0.31550976 +0.316567636 +0.176744169 +0.084697742 +0.075245895 +0.057691834 +0.045548968 +0.038337968 +0.037507374 +0.040624152 +0.048025421 +0.131270236 +0.206931175 +0.191994616 +0.152333461 +0.132955331 +0.132938468 +0.131332475 +0.131585162 +0.139339039 +0.139988904 +0.138257797 +0.144237905 +0.150913479 +0.149114813 +0.141351608 +0.084457348 +0.018651619 +0.007877024 +0.008659563 +0.013212713 +0.022746237 +0.039984754 +0.065477802 +0.107256394 +0.243851558 +0.382851922 +0.418748297 +0.423121235 +0.430916605 +0.441081367 +0.460731664 +0.472588801 +0.472408867 +0.483894592 +0.502617133 +0.506834126 +0.500789849 +0.496771345 +0.495611135 +0.389378533 +0.379734739 +0.317152737 +0.247167737 +0.194461092 +0.158238587 +0.147573081 +0.157654435 +0.179730585 +0.261889323 +0.415363085 +0.431518471 +0.39894137 +0.355395537 +0.314829535 +0.284141862 +0.263604257 +0.25614537 +0.272585568 +0.290792976 +0.292205793 +0.279639464 +0.256072932 +0.229035078 +0.116197674 +0.037209078 +0.017611385 +0.013168941 +0.010714588 +0.010433331 +0.012955815 +0.018532895 +0.02782393 +0.090737323 +0.142376668 +0.132458231 +0.103690912 +0.095181079 +0.13473005 +0.206366763 +0.27274793 +0.318172467 +0.340107262 +0.354836192 +0.363252742 +0.369152797 +0.384877539 +0.409792903 +0.286444385 +0.240853613 +0.363787107 +0.416426115 +0.430069056 +0.430771214 +0.423523401 +0.419826142 +0.431017343 +0.523739419 +0.59497638 +0.618673864 +0.627545255 +0.621612962 +0.601236817 +0.552072938 +0.459719497 +0.374818448 +0.285744515 +0.219691093 +0.195468465 +0.19954718 +0.194411803 +0.193174967 +0.140369067 +0.140233229 +0.18841479 +0.204413163 +0.182444773 +0.14315275 +0.1091245 +0.092967218 +0.08347739 +0.122307345 +0.165956202 +0.155569795 +0.126941738 +0.098369052 +0.078013289 +0.058520989 +0.042821083 +0.0335367 +0.029838202 +0.029006883 +0.024745802 +0.021102313 +0.023216613 +0.031848638 +0.021834019 +0.012573264 +0.013181235 +0.014596049 +0.017457453 +0.021976415 +0.026228686 +0.028464361 +0.028799397 +0.042224683 +0.070946197 +0.075013965 +0.069068678 +0.065988476 +0.072345623 +0.087333889 +0.100682268 +0.106139115 +0.108465559 +0.117677821 +0.129295643 +0.138266695 +0.14241556 +0.137676448 +0.05532273 +0.008874675 +0.003923742 +0.003741782 +0.004974094 +0.007868952 +0.011681598 +0.015144078 +0.017927485 +0.043187954 +0.112629297 +0.132622322 +0.119710484 +0.106515649 +0.104434641 +0.113978713 +0.127180814 +0.143123639 +0.164037505 +0.183491805 +0.192483918 +0.195570098 +0.198989198 +0.2096763 +0.124557203 +0.050563529 +0.040399919 +0.030575779 +0.021830088 +0.016484189 +0.014961081 +0.016738124 +0.026553268 +0.100206839 +0.19085191 +0.209994232 +0.203515025 +0.209754414 +0.245849264 +0.294511473 +0.335644284 +0.36929709 +0.386334216 +0.391643047 +0.396360408 +0.405398318 +0.417733797 +0.429854849 +0.299502135 +0.238337657 +0.34184197 +0.381333438 +0.387098989 +0.389701858 +0.390276065 +0.395341772 +0.413932063 +0.490738768 +0.540746656 +0.499201875 +0.459395208 +0.439363467 +0.415332302 +0.396508348 +0.383158196 +0.378930412 +0.369241472 +0.355486069 +0.349039101 +0.349968202 +0.358976191 +0.365799556 +0.230319664 +0.204270806 +0.252815596 +0.267352846 +0.276837999 +0.285747344 +0.283642085 +0.260666724 +0.220957497 +0.225417611 +0.252178654 +0.282293204 +0.319580444 +0.361327778 +0.387097374 +0.395250557 +0.393310203 +0.378978891 +0.337418333 +0.291026475 +0.252912907 +0.217398094 +0.184619176 +0.1575418 +0.088995603 +0.015220362 +0.002810943 +0.002587044 +0.004486207 +0.007057156 +0.010716439 +0.01520322 +0.019343018 +0.053559329 +0.095623618 +0.086536321 +0.06069513 +0.047970769 +0.055581956 +0.073941633 +0.090246337 +0.098419951 +0.101644324 +0.105171357 +0.112773794 +0.131485486 +0.15522618 +0.175688117 +0.131713445 +0.032649166 +0.022547817 +0.023456803 +0.025000889 +0.027019018 +0.029201786 +0.033753366 +0.045339218 +0.112247742 +0.202753779 +0.217777556 +0.194444566 +0.188373123 +0.21100859 +0.262302126 +0.304878161 +0.321204433 +0.310755466 +0.289761124 +0.268077333 +0.244691023 +0.217436747 +0.19037988 +0.1025622 +0.021601474 +0.020033818 +0.026060888 +0.040236291 +0.057957478 +0.073976932 +0.085422567 +0.091906581 +0.137723762 +0.170800839 +0.135248481 +0.09957827 +0.093697858 +0.113017944 +0.148181092 +0.180967524 +0.205716618 +0.225357705 +0.239563472 +0.255811566 +0.265710409 +0.263255754 +0.259613309 +0.17997995 +0.079167115 +0.107223125 +0.117438034 +0.124644385 +0.123667897 +0.116715554 +0.114634454 +0.127164506 +0.209864648 +0.312170634 +0.3389659 +0.336159461 +0.341635668 +0.362730009 +0.389056059 +0.402682156 +0.402735964 +0.390882964 +0.37159746 +0.350637228 +0.330335225 +0.310785365 +0.285172856 +0.159735683 +0.078745154 +0.085562684 +0.098314254 +0.116747419 +0.133753893 +0.142570789 +0.143886159 +0.149269239 +0.224367224 +0.336207509 +0.353755753 +0.352852552 +0.35224236 +0.370174498 +0.381711245 +0.392125122 +0.407486182 +0.417768434 +0.426882484 +0.434912899 +0.436800378 +0.438795056 +0.438692455 +0.236314796 +0.18521249 +0.204490515 +0.196143064 +0.195512783 +0.205247843 +0.223038628 +0.251642113 +0.288904962 +0.391749063 +0.522524244 +0.559270804 +0.581182219 +0.601105764 +0.610333065 +0.589596496 +0.552078145 +0.496855586 +0.42534896 +0.375642383 +0.343697582 +0.325108605 +0.318339927 +0.319266245 +0.222190618 +0.307458337 +0.361172076 +0.40195794 +0.451114982 +0.484887066 +0.492972154 +0.479561017 +0.443909121 +0.402309781 +0.453966275 +0.446029425 +0.441567225 +0.438021632 +0.429869077 +0.410128563 +0.391807755 +0.367701956 +0.333949037 +0.308725922 +0.285571452 +0.261594553 +0.238250835 +0.21355905 +0.166193846 +0.162218245 +0.199115857 +0.258647385 +0.307729791 +0.335267951 +0.320577786 +0.277867341 +0.2242127 +0.194981464 +0.195591195 +0.198967086 +0.211761035 +0.235576132 +0.266216234 +0.298126329 +0.333119603 +0.369203878 +0.390502695 +0.393241755 +0.387888499 +0.384798753 +0.392433698 +0.402763945 +0.396218322 +0.355209301 +0.291375484 +0.234900555 +0.205410961 +0.217546886 +0.245667525 +0.26341461 +0.277595316 +0.303326538 +0.375568961 +0.394705955 +0.399570521 +0.400349515 +0.409625256 +0.398643974 +0.42216132 +0.443242306 +0.483066642 +0.487073342 +0.427344015 +0.369627428 +0.317502778 +0.270560315 +0.179008152 +0.108846209 +0.09534561 +0.134203005 +0.147716565 +0.157961189 +0.173920973 +0.193267247 +0.193998549 +0.186629359 +0.191322038 +0.169657052 +0.152026129 +0.155601817 +0.174356173 +0.21692505 +0.236809441 +0.228833734 +0.2340744 +0.246475549 +0.257493789 +0.267381283 +0.277905264 +0.242065912 +0.164014336 +0.119882396 +0.082486053 +0.047432019 +0.042479923 +0.057909127 +0.097220315 +0.174199725 +0.195910859 +0.177986826 +0.193249712 +0.190107457 +0.18254231 +0.195886657 +0.26971967 +0.370795212 +0.444698597 +0.511229333 +0.54938606 +0.565430478 +0.56875787 +0.570257499 +0.581628034 +0.563434758 +0.44396319 +0.550951036 +0.667773132 +0.715060343 +0.731282263 +0.716162699 +0.690908784 +0.661193945 +0.615955839 +0.58723485 +0.638646925 +0.629466881 +0.608761459 +0.612655116 +0.611878192 +0.611171437 +0.609707323 +0.604328035 +0.608847939 +0.610652094 +0.604885617 +0.60643646 +0.599696923 +0.553797421 +0.512128678 +0.544781089 +0.496095597 +0.45647772 +0.433169652 +0.423137853 +0.414808036 +0.41695076 +0.436626034 +0.478728834 +0.616548815 +0.652474548 +0.668397867 +0.679925052 +0.68370589 +0.671073408 +0.643223293 +0.617755041 +0.590415743 +0.5624862 +0.548075076 +0.542343427 +0.544762422 +0.522324921 +0.404309273 +0.443561946 +0.423489625 +0.402007224 +0.395341412 +0.390704925 +0.385713771 +0.397557458 +0.425093834 +0.493079573 +0.650268582 +0.705873795 +0.708972138 +0.675536634 +0.637272947 +0.608665951 +0.597710798 +0.591135475 +0.596127909 +0.605735082 +0.608054465 +0.606961383 +0.599827254 +0.554182998 +0.328057011 +0.29540221 +0.258481874 +0.212167477 +0.18897094 +0.17249344 +0.161800718 +0.164368001 +0.181601543 +0.244015602 +0.36362171 +0.367685608 +0.33792588 +0.307008584 +0.315954185 +0.354580798 +0.392084722 +0.420548452 +0.451711545 +0.493113296 +0.517576385 +0.539930916 +0.545794716 +0.502343677 +0.354669253 +0.431027939 +0.450094301 +0.464620902 +0.486245636 +0.509994729 +0.532601539 +0.551776894 +0.563245555 +0.587886578 +0.699648333 +0.724443174 +0.71325425 +0.693642367 +0.678642074 +0.660418473 +0.63952753 +0.611204406 +0.576960184 +0.548679755 +0.520198675 +0.49097222 +0.462171536 +0.426178761 +0.238747249 +0.190317535 +0.151828443 +0.119198785 +0.094006362 +0.078949551 +0.075143941 +0.075863489 +0.07889944 +0.11879184 +0.221817369 +0.240308825 +0.219538298 +0.200676073 +0.198920201 +0.203182433 +0.212356592 +0.2233708 +0.227690051 +0.222154638 +0.211629425 +0.200274753 +0.190205359 +0.19208658 +0.090582623 +0.03205209 +0.022452965 +0.020296196 +0.021742005 +0.025396795 +0.024436573 +0.02223476 +0.026531438 +0.065160683 +0.172726958 +0.174088287 +0.126208944 +0.099156938 +0.086283537 +0.08038493 +0.082650586 +0.084013285 +0.090691567 +0.105039931 +0.124684893 +0.151969055 +0.189485221 +0.238457833 +0.158347565 +0.108545512 +0.084784596 +0.070497647 +0.069388712 +0.082521508 +0.108280739 +0.139271056 +0.174842339 +0.256812201 +0.450735145 +0.531394294 +0.565424441 +0.580759707 +0.586869537 +0.587413748 +0.593040928 +0.607411861 +0.630349291 +0.640820427 +0.635216534 +0.617740785 +0.593080581 +0.533025718 +0.329576236 +0.352403861 +0.335203272 +0.32920772 +0.340757452 +0.364914189 +0.396295296 +0.445277061 +0.508143729 +0.595943459 +0.737706603 +0.778882309 +0.797296157 +0.809145908 +0.797880121 +0.76879572 +0.745503879 +0.731696723 +0.726747781 +0.7241174 +0.720759851 +0.716090994 +0.70678631 +0.65416525 +0.489516816 +0.601135716 +0.601634665 +0.601568117 +0.607543351 +0.614297721 +0.614979142 +0.60703931 +0.593449857 +0.596872269 +0.67744134 +0.680204739 +0.667282038 +0.648960424 +0.640805799 +0.630966021 +0.619116289 +0.601616939 +0.57984531 +0.54843003 +0.5012074 +0.44352816 +0.385079688 +0.332825042 +0.16984718 +0.096927058 +0.067252234 +0.038338214 +0.019203681 +0.010098833 +0.008161297 +0.009934459 +0.015355273 +0.057482895 +0.149973197 +0.163772224 +0.150428918 +0.146127303 +0.15349405 +0.164923742 +0.171299275 +0.169634755 +0.171291804 +0.182510717 +0.197634116 +0.204201373 +0.204188125 +0.205575325 +0.129369002 +0.054795393 +0.072007491 +0.090428131 +0.112952887 +0.139250574 +0.166906046 +0.191374779 +0.216938179 +0.297647277 +0.427372528 +0.47230524 +0.497015342 +0.514248249 +0.521986834 +0.518652611 +0.5055599 +0.489539907 +0.451584903 +0.413493486 +0.383549783 +0.353385661 +0.327783671 +0.294499066 +0.114344341 +0.059816314 +0.053916171 +0.069875155 +0.119203646 +0.190202572 +0.264779062 +0.327144774 +0.372363811 +0.440895025 +0.49439514 +0.491845162 +0.480254209 +0.451498463 +0.425598297 +0.382584967 +0.343187197 +0.327325503 +0.349744894 +0.380838393 +0.38002832 +0.362182789 +0.336864859 +0.275501416 +0.129072567 +0.092942817 +0.045941283 +0.028490159 +0.023859922 +0.023441417 +0.024354668 +0.025949153 +0.029545805 +0.048398192 +0.158852426 +0.223857688 +0.236564044 +0.233402454 +0.23654789 +0.252165935 +0.277440561 +0.302908109 +0.319747529 +0.324740636 +0.321831631 +0.316302017 +0.303679854 +0.272542934 +0.096840549 +0.037990877 +0.015359055 +0.007863246 +0.007866057 +0.013599868 +0.025942672 +0.046887679 +0.080756412 +0.1591855 +0.395117181 +0.52324593 +0.573768494 +0.590273485 +0.595560863 +0.60530684 +0.610142123 +0.602285226 +0.614025617 +0.632983329 +0.626192372 +0.596984448 +0.556477326 +0.464146451 +0.296973778 +0.302420548 +0.230850885 +0.171205931 +0.144142807 +0.133237553 +0.128195694 +0.123562604 +0.118932373 +0.13814327 +0.260381732 +0.311534812 +0.308580337 +0.285840269 +0.260771478 +0.238217368 +0.225395958 +0.221993816 +0.229576484 +0.239936882 +0.243326989 +0.240917472 +0.237485725 +0.22502445 +0.087116728 +0.023614002 +0.013876716 +0.012494949 +0.011048133 +0.009199525 +0.008219262 +0.008945399 +0.013444563 +0.043315892 +0.144555034 +0.17850758 +0.16810406 +0.157490607 +0.168622686 +0.212530069 +0.263275551 +0.298691377 +0.316266053 +0.319278576 +0.317358785 +0.313595113 +0.311834641 +0.311304955 +0.159783833 +0.135246623 +0.136473259 +0.113440712 +0.098017745 +0.09086238 +0.096082007 +0.107760083 +0.126352296 +0.183835809 +0.36233126 +0.463695304 +0.518157804 +0.546509639 +0.559484864 +0.56199477 +0.526649338 +0.471916858 +0.408005007 +0.351683877 +0.307634774 +0.268290061 +0.228005755 +0.192852539 +0.063614747 +0.043682657 +0.053979156 +0.059656821 +0.064268469 +0.068371984 +0.067205871 +0.06218879 +0.058592733 +0.074710664 +0.170996154 +0.236922436 +0.264727366 +0.266433219 +0.274775208 +0.277799897 +0.276246997 +0.271265724 +0.263198807 +0.266021781 +0.277467611 +0.298821012 +0.326729973 +0.305534776 +0.175701466 +0.116595639 +0.078099986 +0.056190308 +0.050839795 +0.055791922 +0.066328907 +0.074720614 +0.078926535 +0.097615949 +0.213832933 +0.272322012 +0.273047288 +0.261547207 +0.263021935 +0.286070302 +0.326144413 +0.374179275 +0.430981728 +0.471409262 +0.483245706 +0.475207101 +0.457520562 +0.381982946 +0.217513266 +0.174492808 +0.129736377 +0.110830216 +0.123135001 +0.155902205 +0.200150651 +0.246437675 +0.289404415 +0.338102223 +0.527714144 +0.585984402 +0.564942845 +0.542063773 +0.529507317 +0.519062084 +0.506791663 +0.499374545 +0.50895475 +0.517068894 +0.51598438 +0.510225939 +0.502217266 +0.4370106 +0.264311335 +0.244018161 +0.202445971 +0.171158049 +0.157442825 +0.156584058 +0.177620409 +0.218653532 +0.260701566 +0.308287778 +0.47293901 +0.517099831 +0.509504651 +0.495231801 +0.482018645 +0.465097695 +0.455400305 +0.447804195 +0.437241888 +0.418154724 +0.387736498 +0.353804476 +0.320585265 +0.276830055 +0.110708275 +0.071083307 +0.056086813 +0.052314442 +0.052361891 +0.054021772 +0.0602327 +0.067611453 +0.070838902 +0.081886143 +0.160713815 +0.154966851 +0.12280521 +0.113391749 +0.129294371 +0.148902825 +0.161902813 +0.173732558 +0.185351499 +0.203876025 +0.231154536 +0.258244344 +0.28480259 +0.296373495 +0.152500306 +0.167233221 +0.164182037 +0.147454626 +0.135201486 +0.13529809 +0.149982139 +0.161237331 +0.164382557 +0.174432865 +0.270930986 +0.359732553 +0.389363732 +0.414692989 +0.419544234 +0.414035614 +0.415645786 +0.426327366 +0.429977378 +0.427248408 +0.418861115 +0.394812973 +0.37276078 +0.305523013 +0.160477364 +0.181517052 +0.19386614 +0.204017958 +0.224958717 +0.255902278 +0.299377024 +0.337573184 +0.365963287 +0.416449662 +0.533050932 +0.597671077 +0.58790896 +0.554974502 +0.513967493 +0.475671711 +0.456950622 +0.43704247 +0.425223488 +0.427153248 +0.427993214 +0.423192679 +0.402508712 +0.317146619 +0.148985105 +0.098744592 +0.077621686 +0.076581335 +0.086227265 +0.093039432 +0.086971453 +0.07117301 +0.058212968 +0.07575835 +0.190610411 +0.262458533 +0.278452968 +0.266023514 +0.242473898 +0.207679023 +0.18733611 +0.180082324 +0.172253846 +0.170679072 +0.177984313 +0.181627766 +0.171483471 +0.149893009 +0.072545822 +0.075102224 +0.078095987 +0.075714933 +0.077025263 +0.08532315 +0.099562742 +0.107148165 +0.11285188 +0.129097314 +0.22040349 +0.296614514 +0.342358509 +0.390700349 +0.400525528 +0.374255513 +0.342334931 +0.321261605 +0.301869028 +0.284904734 +0.270986315 +0.260665765 +0.255418249 +0.205595259 +0.092853004 +0.074237837 +0.070282619 +0.084536436 +0.094242461 +0.095105071 +0.094618455 +0.091907088 +0.089833253 +0.100738033 +0.192432635 +0.30031954 +0.371992312 +0.435211624 +0.479204784 +0.503188655 +0.512513644 +0.500909487 +0.479119348 +0.449915758 +0.411215253 +0.370946327 +0.33701554 +0.235737903 +0.121528076 +0.102851143 +0.083395581 +0.079374696 +0.083861028 +0.093093502 +0.100855471 +0.103840366 +0.105879533 +0.129207522 +0.25779736 +0.350706453 +0.388544991 +0.411750666 +0.437384362 +0.472468758 +0.518186765 +0.5235034 +0.494152287 +0.456675824 +0.422900275 +0.396200826 +0.377405388 +0.282315156 +0.142635217 +0.155439471 +0.143873546 +0.133332834 +0.125149444 +0.119876715 +0.120100739 +0.121694887 +0.122343485 +0.139499614 +0.248388375 +0.325811161 +0.388598398 +0.439600767 +0.478058297 +0.521036659 +0.552749289 +0.554434794 +0.521402883 +0.454011535 +0.400338659 +0.355520409 +0.326347801 +0.268882708 +0.133707938 +0.176291457 +0.1771035 +0.187158335 +0.210833065 +0.239567858 +0.275315562 +0.317549149 +0.369041386 +0.429580163 +0.548008486 +0.553830032 +0.45826722 +0.332887606 +0.244963543 +0.186106079 +0.153837316 +0.142742954 +0.130008332 +0.123558049 +0.136306531 +0.168887546 +0.210347112 +0.194781924 +0.114505455 +0.088437728 +0.070841821 +0.072122169 +0.091269321 +0.125117348 +0.168831296 +0.212933945 +0.251788732 +0.292003545 +0.436841359 +0.517943538 +0.520359958 +0.514511677 +0.512075943 +0.53234958 +0.570090859 +0.614380094 +0.646238618 +0.658454054 +0.658991709 +0.666403213 +0.670077661 +0.629821132 +0.659411908 +0.730576508 +0.746375884 +0.768910225 +0.801505093 +0.829011256 +0.850361961 +0.863606755 +0.866484218 +0.855705352 +0.871486105 +0.848286567 +0.814154646 +0.791905684 +0.783961778 +0.774713407 +0.759923825 +0.74353828 +0.713486154 +0.69037127 +0.678084206 +0.655174325 +0.622166585 +0.500861433 +0.328008047 +0.364368943 +0.300828511 +0.234240937 +0.199592181 +0.19032438 +0.193571597 +0.189289969 +0.170379412 +0.153811364 +0.195372041 +0.184869353 +0.168478525 +0.177514165 +0.216415203 +0.272228023 +0.319601163 +0.338101949 +0.315730302 +0.274939456 +0.247044471 +0.231058357 +0.216587632 +0.179295672 +0.098991494 +0.076689661 +0.065171458 +0.060096262 +0.070247598 +0.089742475 +0.121259698 +0.164966372 +0.214300496 +0.279220769 +0.447471276 +0.575304552 +0.661061924 +0.69437889 +0.685279012 +0.668724379 +0.64992167 +0.624249891 +0.586338985 +0.557138245 +0.541654321 +0.538065514 +0.54338261 +0.476230289 +0.395579556 +0.530668457 +0.541753024 +0.519643657 +0.505393564 +0.508640789 +0.533399816 +0.563628345 +0.587992544 +0.622777033 +0.708461437 +0.720003226 +0.696433993 +0.670816455 +0.634084475 +0.587231335 +0.533464394 +0.475670169 +0.402040993 +0.319893858 +0.271841105 +0.276750919 +0.30969824 +0.29640414 +0.21124932 +0.383003737 +0.495054577 +0.538657459 +0.568249195 +0.599811127 +0.637985762 +0.677083965 +0.690399785 +0.653453285 +0.632557092 +0.563409797 +0.49177022 +0.451700825 +0.418236292 +0.38337746 +0.364610196 +0.356078583 +0.313801869 +0.202689329 +0.108018198 +0.081247857 +0.071960963 +0.066622466 +0.06058942 +0.210798767 +0.41092253 +0.601600115 +0.735673901 +0.805932437 +0.820566915 +0.802076646 +0.757275371 +0.677555265 +0.655702422 +0.576427585 +0.532688265 +0.544711354 +0.562868893 +0.557389726 +0.53255789 +0.497435309 +0.485821176 +0.453621541 +0.367274856 +0.299665248 +0.27389263 +0.22490771 +0.19695605 +0.317003763 +0.513291984 +0.707479642 +0.84421364 +0.909520257 +0.933858025 +0.943930741 +0.946302818 +0.944583537 +0.946785771 +0.926402045 +0.892220703 +0.858524862 +0.829380366 +0.816834793 +0.7925402 +0.76999917 +0.744761559 +0.704945903 +0.645705351 +0.571559011 +0.521915023 +0.337941513 +0.295948391 +0.3337634 +0.337840874 +0.332971751 +0.325163351 +0.318855955 +0.312994537 +0.302734226 +0.287419484 +0.27345492 +0.382127934 +0.404017408 +0.378809632 +0.351422987 +0.337860117 +0.347780457 +0.370328045 +0.397891673 +0.426166541 +0.449901312 +0.46994324 +0.491169824 +0.507247197 +0.44025482 +0.268244393 +0.304844066 +0.25012212 +0.201948524 +0.15453173 +0.118338096 +0.099698445 +0.091160428 +0.090154565 +0.109418246 +0.238288053 +0.295564338 +0.319024517 +0.332975119 +0.348427121 +0.361829423 +0.372924176 +0.377262256 +0.375528698 +0.36023536 +0.338757606 +0.314932633 +0.282090139 +0.220264247 +0.070321627 +0.044765623 +0.04816424 +0.055284459 +0.065102137 +0.075374803 +0.083286846 +0.092864287 +0.110134978 +0.149707655 +0.308059534 +0.363966656 +0.380931715 +0.401564744 +0.430874024 +0.454843533 +0.463934317 +0.455351371 +0.420163497 +0.384390862 +0.368310728 +0.352666935 +0.332856557 +0.287128344 +0.11901203 +0.165004015 +0.222115634 +0.2802216 +0.333944631 +0.377809353 +0.411890279 +0.444801526 +0.479957989 +0.517497756 +0.589672338 +0.569465684 +0.520090668 +0.475210319 +0.457944024 +0.457826497 +0.438713213 +0.426173026 +0.41498722 +0.418044243 +0.423860669 +0.420194225 +0.408343141 +0.258644224 +0.105506085 +0.123698234 +0.090253781 +0.081659163 +0.091621722 +0.116948237 +0.1629569 +0.226039202 +0.277366767 +0.309805518 +0.416700098 +0.45614208 +0.436148044 +0.399898444 +0.36103875 +0.328157305 +0.317293078 +0.323163881 +0.311278215 +0.290541458 +0.274691838 +0.589079678 +0.589585409 +0.47961074 +0.43753545 +0.381070061 +0.336272721 +0.352761668 +0.402752206 +0.433790273 +0.451997615 +0.479734754 +0.495282729 +0.479151815 +0.430137559 +0.386858541 +0.362205103 +0.367692947 +0.390301668 +0.427108498 +0.467863188 +0.498473128 +0.529092868 +0.557120272 +0.575481626 +0.588291703 +0.583990589 +0.506164159 +0.540665281 +0.440417093 +0.340795882 +0.268149783 +0.213550682 +0.180153362 +0.170592034 +0.173282153 +0.179843885 +0.198454673 +0.340988267 +0.546368715 +0.594436304 +0.616784123 +0.611187693 +0.603235373 +0.600420553 +0.598384093 +0.604760674 +0.606452729 +0.590034461 +0.560102603 +0.524948687 +0.337340195 +0.253270068 +0.218583654 +0.143026503 +0.088462527 +0.069354261 +0.064370165 +0.070281661 +0.086142942 +0.10465749 +0.130245728 +0.208726831 +0.290162389 +0.32051747 +0.333236998 +0.343699367 +0.357868234 +0.354449145 +0.338830223 +0.35426178 +0.365580385 +0.367384257 +0.355034598 +0.334354486 +0.246158553 +0.100998462 +0.057902791 +0.034531619 +0.024239642 +0.024176055 +0.027260334 +0.032079062 +0.039948517 +0.061246564 +0.09126973 +0.141848687 +0.149122356 +0.126326508 +0.115858385 +0.114251212 +0.100332315 +0.067529845 +0.042878506 +0.039597264 +0.048968979 +0.06731901 +0.102032197 +0.097732962 +0.062708123 +0.025660978 +0.027066136 +0.050671487 +0.095877606 +0.151596696 +0.208300909 +0.260311365 +0.3015507 +0.321219559 +0.303580021 +0.284160768 +0.257244861 +0.215805228 +0.18526137 +0.157890257 +0.130625998 +0.103896249 +0.08583417 +0.083972192 +0.094135279 +0.098779934 +0.10659343 +0.12050804 +0.062403214 +0.031947661 +0.02051491 +0.016398885 +0.021348331 +0.036320888 +0.057963554 +0.076431419 +0.085780408 +0.086723166 +0.085822437 +0.137024274 +0.218022277 +0.215166105 +0.184204016 +0.158604202 +0.153832187 +0.158242131 +0.154833747 +0.156447908 +0.168514319 +0.189098335 +0.206720707 +0.216608868 +0.13718781 +0.045167465 +0.027524853 +0.017993711 +0.014966849 +0.014573609 +0.016541083 +0.022321553 +0.032643449 +0.047387309 +0.070732804 +0.171864441 +0.296100354 +0.335195308 +0.350408948 +0.364696166 +0.386903469 +0.399810586 +0.401914687 +0.393211597 +0.375514508 +0.339554006 +0.296529056 +0.265608842 +0.145188288 +0.031431859 +0.030243475 +0.057195315 +0.092333306 +0.12669148 +0.157515559 +0.177229848 +0.201410728 +0.237050081 +0.277458738 +0.383887297 +0.471339808 +0.502909753 +0.532167751 +0.564383374 +0.593551759 +0.602774436 +0.586416592 +0.538872102 +0.487878352 +0.441315346 +0.397876258 +0.372274144 +0.231485901 +0.188936546 +0.244504348 +0.249634023 +0.253702171 +0.266392738 +0.2902188 +0.333396939 +0.384484378 +0.424296041 +0.456955901 +0.55079827 +0.601686152 +0.631740877 +0.655558957 +0.679633139 +0.695325979 +0.708867704 +0.721387164 +0.70139348 +0.630932758 +0.540722046 +0.485591424 +0.459364004 +0.262989852 +0.278462341 +0.555703817 +0.624217761 +0.671376869 +0.724246269 +0.781255404 +0.839925998 +0.884963346 +0.90852156 +0.898365839 +0.882901326 +0.854304352 +0.772880987 +0.684906958 +0.608508637 +0.555395894 +0.507151239 +0.46222757 +0.460950492 +0.466835336 +0.457674604 +0.421660958 +0.382565229 +0.207768199 +0.117671235 +0.206752939 +0.249698607 +0.265942263 +0.253921889 +0.234591968 +0.229839533 +0.246677518 +0.26316534 +0.272571925 +0.311675977 +0.29110391 +0.253731526 +0.219053223 +0.190742228 +0.1653909 +0.164989452 +0.194110183 +0.228079499 +0.246539823 +0.274815498 +0.336209847 +0.413655652 +0.400046511 +0.294893699 +0.44442173 +0.575957059 +0.654659956 +0.711808861 +0.751947892 +0.775442658 +0.777611367 +0.746293957 +0.686115146 +0.657263436 +0.648528475 +0.616850081 +0.577287288 +0.550113745 +0.521229781 +0.510043004 +0.478621697 +0.437766744 +0.409308517 +0.376645801 +0.340947773 +0.30886395 +0.151930769 +0.107918239 +0.175828437 +0.182519135 +0.203265325 +0.244569001 +0.31652385 +0.425785734 +0.553169089 +0.652314037 +0.702571917 +0.727514647 +0.664170452 +0.53210959 +0.386330733 +0.26650993 +0.195156697 +0.173816012 +0.16992625 +0.159384672 +0.164548845 +0.192259097 +0.215488736 +0.215736696 +0.109032768 +0.176636927 +0.197575914 +0.201573551 +0.238325352 +0.298309681 +0.367740029 +0.450874027 +0.538414067 +0.60850421 +0.650150041 +0.712081352 +0.728304078 +0.714857033 +0.722698116 +0.729923113 +0.735378282 +0.699829001 +0.670152026 +0.658662166 +0.668562953 +0.684682229 +0.685558319 +0.636999783 +0.458771537 +0.50076119 +0.4259826 +0.372928473 +0.366369015 +0.380307482 +0.407420124 +0.455982718 +0.517489895 +0.576412934 +0.632562471 +0.720806207 +0.830495167 +0.825414206 +0.818557368 +0.803542079 +0.778448571 +0.754822485 +0.743153577 +0.741546726 +0.745170972 +0.740342377 +0.720741455 +0.672235289 +0.441129949 +0.446747712 +0.468153745 +0.383135606 +0.28697817 +0.206389664 +0.15718736 +0.142510409 +0.154725611 +0.182167372 +0.230177327 +0.402474494 +0.603734052 +0.656187856 +0.684626064 +0.704685513 +0.727380845 +0.744777279 +0.747820473 +0.742758193 +0.73347479 +0.716353205 +0.690049842 +0.650450569 +0.385229461 +0.314209581 +0.293189095 +0.246770708 +0.221657707 +0.209021515 +0.202720217 +0.199910288 +0.195220206 +0.188091502 +0.185361975 +0.274209181 +0.383687543 +0.394974541 +0.39790209 +0.398701951 +0.397304455 +0.391990562 +0.384673047 +0.381353768 +0.377596528 +0.368266846 +0.346690138 +0.325505058 +0.199279344 +0.091184033 +0.084451662 +0.066940786 +0.051391845 +0.040094474 +0.034078772 +0.034307504 +0.040662641 +0.05671514 +0.092054373 +0.210535482 +0.314872346 +0.333704494 +0.351146656 +0.383062786 +0.429245376 +0.468903406 +0.48290311 +0.466315058 +0.442939782 +0.4236566 +0.402264667 +0.377595745 +0.244030393 +0.076066851 +0.053852559 +0.038161854 +0.038170777 +0.044400802 +0.052920611 +0.062998262 +0.073239824 +0.086366381 +0.117490605 +0.242019325 +0.34925903 +0.357699229 +0.336141509 +0.332786174 +0.362491354 +0.38626617 +0.383253178 +0.371567604 +0.365704576 +0.366112883 +0.36034136 +0.356817025 +0.213086363 +0.086746788 +0.075638808 +0.055820079 +0.046323028 +0.047750522 +0.058177405 +0.082760566 +0.118976988 +0.155015623 +0.201285759 +0.301218496 +0.357806903 +0.36077454 +0.359376301 +0.384223794 +0.399519735 +0.419748101 +0.418162001 +0.416841902 +0.405403093 +0.403999621 +0.386382448 +0.365424708 +0.187498729 +0.077436347 +0.077678001 +0.081255408 +0.088771351 +0.09843012 +0.114951289 +0.14133527 +0.166167805 +0.191196042 +0.240385576 +0.391478864 +0.516791944 +0.507950046 +0.496270865 +0.495334393 +0.511434364 +0.531268143 +0.538397119 +0.54253361 +0.54716288 +0.570175042 +0.579127121 +0.567777436 +0.397535933 +0.377226504 +0.346753738 +0.286009567 +0.223291508 +0.173285619 +0.147910009 +0.153860098 +0.178719739 +0.200987587 +0.216352194 +0.294793543 +0.452196256 +0.47859299 +0.493992999 +0.494966835 +0.487291109 +0.472587447 +0.456551525 +0.429981865 +0.407009942 +0.397538908 +0.382189114 +0.353494837 +0.191398858 +0.119711038 +0.068662852 +0.032947324 +0.023510858 +0.02162882 +0.023030656 +0.026945083 +0.032808847 +0.043889178 +0.067927307 +0.142587616 +0.223768661 +0.215311047 +0.206231369 +0.245100325 +0.297425325 +0.328393205 +0.325853191 +0.305515136 +0.281975644 +0.28047806 +0.291994096 +0.288380099 +0.14997096 +0.123976474 +0.153779548 +0.192578529 +0.245223044 +0.284604115 +0.295695074 +0.288421715 +0.265693203 +0.250296556 +0.247451978 +0.294797622 +0.368217895 +0.361780017 +0.345579662 +0.328406064 +0.30204201 +0.294742658 +0.297172463 +0.29649378 +0.311863836 +0.360348812 +0.38759562 +0.380833738 +0.203323039 +0.207563812 +0.164162258 +0.083683474 +0.050372827 +0.062190122 +0.095079068 +0.135616591 +0.170177671 +0.19427883 +0.209226766 +0.236154789 +0.293787141 +0.28413517 +0.277278534 +0.262984728 +0.248906062 +0.248276497 +0.250623512 +0.254438084 +0.255526524 +0.257366797 +0.265001728 +0.280092192 +0.173464868 +0.17254821 +0.194312224 +0.165873313 +0.115365508 +0.093949311 +0.095449804 +0.101216553 +0.091314448 +0.07242901 +0.071920313 +0.104864197 +0.221831666 +0.251196684 +0.237681667 +0.230786988 +0.252138927 +0.305675313 +0.376412435 +0.442173758 +0.485902923 +0.513801319 +0.512948733 +0.472373921 +0.395398542 +0.368095719 +0.322490832 +0.292788115 +0.282255024 +0.28867594 +0.312907549 +0.352468382 +0.401759339 +0.450793664 +0.493602451 +0.540461492 +0.657434678 +0.640905657 +0.586163204 +0.5229702 +0.474104686 +0.442692202 +0.438058497 +0.443848374 +0.450659522 +0.454886568 +0.44675856 +0.422240294 +0.222159397 +0.195314068 +0.175168977 +0.143284536 +0.123853755 +0.116719791 +0.116835652 +0.122205939 +0.125987804 +0.124450368 +0.122423238 +0.161479326 +0.184312158 +0.130586826 +0.087984546 +0.083173596 +0.103010136 +0.132190782 +0.158859216 +0.18010637 +0.19221856 +0.194180867 +0.187671307 +0.189639691 +0.133521014 +0.067178931 +0.064933207 +0.069805351 +0.086963746 +0.11898926 +0.165591304 +0.214584165 +0.265025668 +0.323411508 +0.402120697 +0.500873779 +0.551093505 +0.47811796 +0.418599573 +0.36230972 +0.308935096 +0.281131313 +0.275433015 +0.25320983 +0.220107803 +0.204676981 +0.200492567 +0.193086256 +0.112242378 +0.090713269 +0.075163888 +0.06654595 +0.068912693 +0.078236838 +0.090506783 +0.102723346 +0.10762282 +0.109984957 +0.117827736 +0.154038495 +0.237174588 +0.272459111 +0.288921004 +0.296011544 +0.312610834 +0.313597768 +0.307734313 +0.320454582 +0.338852482 +0.339998713 +0.331001718 +0.285407497 +0.123243921 +0.097868584 +0.087382052 +0.077261023 +0.078424196 +0.085424359 +0.09213745 +0.097747622 +0.10164926 +0.104814161 +0.110134209 +0.137394221 +0.210153405 +0.255986737 +0.306130444 +0.33886894 +0.357989633 +0.366363342 +0.371446757 +0.376626595 +0.381680028 +0.376903579 +0.356301659 +0.307723031 +0.131638643 +0.083108992 +0.049000959 +0.033057809 +0.033961722 +0.045001378 +0.069084097 +0.107410113 +0.151865815 +0.20043385 +0.267123306 +0.365564711 +0.49086681 +0.527634453 +0.548667674 +0.554443309 +0.551238369 +0.540236895 +0.531845788 +0.528351914 +0.518239211 +0.484901154 +0.4511967 +0.383394889 +0.206332012 +0.273021792 +0.185015342 +0.10988972 +0.079067197 +0.07456077 +0.085079412 +0.10580385 +0.124120232 +0.138784993 +0.150585347 +0.188188188 +0.270655137 +0.293987021 +0.319680514 +0.349142134 +0.380291843 +0.416484482 +0.451439846 +0.455411774 +0.435975776 +0.418147196 +0.40214303 +0.369137759 +0.17977299 +0.163445692 +0.167137967 +0.12517954 +0.100747812 +0.095562896 +0.099212597 +0.107135743 +0.116505299 +0.130791901 +0.155170633 +0.221315049 +0.331239456 +0.359853623 +0.364044137 +0.354989212 +0.374344847 +0.41442493 +0.44797039 +0.442558372 +0.417286707 +0.405034194 +0.404185346 +0.391850143 +0.3040741 +0.277160184 +0.23452677 +0.176848509 +0.134938148 +0.116779325 +0.112702884 +0.11822021 +0.127462797 +0.135195404 +0.141073971 +0.183844932 +0.333496727 +0.362143656 +0.359861103 +0.353046146 +0.346495692 +0.34805502 +0.355955507 +0.367886498 +0.373942216 +0.361307397 +0.331630657 +0.293066828 +0.115898654 +0.04953087 +0.042230374 +0.048028605 +0.058835589 +0.071614881 +0.085396298 +0.09665443 +0.105658728 +0.115034351 +0.127405032 +0.184477839 +0.2670421 +0.253514694 +0.225691829 +0.209304366 +0.222143733 +0.261900043 +0.301539725 +0.328652944 +0.345621285 +0.35042352 +0.345046168 +0.33609904 +0.172542359 +0.07571684 +0.067796376 +0.05024709 +0.047883614 +0.049150909 +0.052612629 +0.06258582 +0.07716697 +0.096316142 +0.123934226 +0.199273012 +0.292980182 +0.29005963 +0.262227931 +0.260653578 +0.289898166 +0.326346034 +0.351543293 +0.354051111 +0.333868148 +0.305366479 +0.273903223 +0.244424298 +0.111637341 +0.05393802 +0.056874501 +0.041989242 +0.033282492 +0.030697762 +0.032714484 +0.040231003 +0.053706362 +0.074211698 +0.104014116 +0.170107079 +0.245090602 +0.240700041 +0.240850308 +0.288351623 +0.354513491 +0.41799187 +0.475588046 +0.502464138 +0.502477408 +0.480290176 +0.447876607 +0.42449471 +0.203976456 +0.178770662 +0.110968125 +0.040855388 +0.018495925 +0.018039834 +0.027045645 +0.04203688 +0.062806097 +0.092104238 +0.137354426 +0.216471445 +0.346429741 +0.425586957 +0.459409 +0.486040481 +0.505961805 +0.51919711 +0.521749291 +0.50896215 +0.475912661 +0.444954968 +0.420987906 +0.380937618 +0.163468921 +0.103870116 +0.060269125 +0.03588277 +0.035617073 +0.050427017 +0.075476217 +0.113596953 +0.16238059 +0.214539971 +0.268067106 +0.334732333 +0.421287548 +0.446177267 +0.464796072 +0.510515106 +0.557909712 +0.596954122 +0.640805506 +0.667436103 +0.66607919 +0.62894552 +0.572168623 +0.473513019 +0.189197314 +0.203786819 +0.12300316 +0.067452869 +0.056350058 +0.067702824 +0.090224133 +0.123739126 +0.160490844 +0.194259369 +0.228911793 +0.285271211 +0.397671952 +0.443449147 +0.446912269 +0.44268476 +0.42810724 +0.417831515 +0.407674208 +0.374044312 +0.341447003 +0.307063146 +0.269242961 +0.213680038 +0.094043262 +0.056732559 +0.02078436 +0.008309845 +0.009393504 +0.01952414 +0.038153929 +0.063432474 +0.091478126 +0.114904603 +0.132919076 +0.155036561 +0.204637316 +0.19520309 +0.173193131 +0.165757036 +0.186097038 +0.212174893 +0.237205936 +0.249497564 +0.25178853 +0.248570069 +0.251726898 +0.253122349 +0.11137128 +0.120655798 +0.09591883 +0.074645764 +0.065771415 +0.068185285 +0.081556406 +0.101786636 +0.128030705 +0.15678971 +0.187659241 +0.223808507 +0.29125069 +0.334106574 +0.377460983 +0.414779514 +0.459980352 +0.500378899 +0.532128219 +0.54324072 +0.54529251 +0.531458619 +0.472746873 +0.371211977 +0.139891229 +0.189936696 +0.223653014 +0.24930266 +0.290291566 +0.336390707 +0.377793337 +0.406118641 +0.426264474 +0.4411667 +0.453723641 +0.474897519 +0.501012033 +0.486292531 +0.514613377 +0.551298603 +0.598948086 +0.657533902 +0.696467784 +0.720784518 +0.700613278 +0.65111547 +0.580778856 +0.488682481 +0.287305179 +0.304813886 +0.203063748 +0.157258443 +0.161915658 +0.187206525 +0.21431478 +0.242797539 +0.270904056 +0.290058324 +0.293739256 +0.291262636 +0.362338126 +0.3661472 +0.364492684 +0.378817105 +0.401524573 +0.41460864 +0.403073268 +0.372686022 +0.344205171 +0.317359503 +0.29454121 +0.23990639 +0.109403477 +0.078783886 +0.035739261 +0.027518996 +0.033156259 +0.048130578 +0.069120358 +0.092191475 +0.109370734 +0.12018194 +0.134737177 +0.168420845 +0.263848607 +0.308869651 +0.335574075 +0.358786842 +0.370973033 +0.384052317 +0.386794591 +0.383377345 +0.377427197 +0.374459567 +0.360080436 +0.294552051 +0.129048241 +0.138824257 +0.114055207 +0.099583816 +0.10208138 +0.116789288 +0.14109329 +0.175006526 +0.214292697 +0.252060846 +0.291953928 +0.346578931 +0.458147359 +0.478648489 +0.482395356 +0.488015283 +0.48782261 +0.475387901 +0.44828402 +0.436325099 +0.426752289 +0.408848438 +0.379337696 +0.27762059 +0.20931425 +0.190687728 +0.158780386 +0.14823005 +0.146053492 +0.152885542 +0.170360205 +0.190804508 +0.20736593 +0.217995917 +0.225549919 +0.231565189 +0.290839416 +0.309305729 +0.293582928 +0.272455807 +0.260181068 +0.273273295 +0.299119265 +0.324310912 +0.350578378 +0.370937509 +0.38585493 +0.374884215 +0.226908455 +0.234305235 +0.222966061 +0.218785415 +0.21687155 +0.218093353 +0.22302588 +0.238663176 +0.258808469 +0.274732427 +0.281112397 +0.28771775 +0.395756215 +0.404364664 +0.357259658 +0.309276565 +0.26619158 +0.226889715 +0.20336686 +0.180784228 +0.149639611 +0.123523582 +0.107371621 +0.103350166 +0.041942985 +0.029670005 +0.040743846 +0.05698061 +0.080410061 +0.107722801 +0.136325242 +0.166050595 +0.196513378 +0.224282879 +0.248337174 +0.272813478 +0.356887382 +0.341682117 +0.289372442 +0.25114419 +0.24867182 +0.272386649 +0.324181459 +0.374456646 +0.414554058 +0.435854334 +0.432248947 +0.368002461 +0.205608325 +0.203293505 +0.197867827 +0.215925252 +0.264721707 +0.340936698 +0.43678585 +0.537548446 +0.622987868 +0.677027715 +0.703831761 +0.712283521 +0.775824671 +0.751946641 +0.68431986 +0.617164457 +0.575085793 +0.560390314 +0.566444817 +0.569955252 +0.560133447 +0.551122318 +0.537187595 +0.456820762 +0.231937235 +0.29866021 +0.34288699 +0.357617781 +0.361950234 +0.363238019 +0.363262538 +0.369056969 +0.370680166 +0.367016831 +0.354081844 +0.353991352 +0.390946012 +0.336518619 +0.281325473 +0.249252833 +0.245183926 +0.25400638 +0.268047569 +0.272258347 +0.260563112 +0.247755469 +0.235959947 +0.191553516 +0.102401699 +0.086426043 +0.120675263 +0.186632189 +0.26655571 +0.345285981 +0.420050324 +0.475009281 +0.503367796 +0.517474122 +0.517377982 +0.501684418 +0.520919236 +0.544105472 +0.572007099 +0.588269737 +0.601366722 +0.607569373 +0.621122863 +0.62709989 +0.602004462 +0.57760622 +0.55770653 +0.486755347 +0.426133743 +0.509646132 +0.61177524 +0.655577772 +0.670672257 +0.680000584 +0.690434216 +0.705586769 +0.724845004 +0.744061557 +0.766955016 +0.805772601 +0.852998189 +0.862522461 +0.831021851 +0.76234837 +0.679300322 +0.62180363 +0.584427115 +0.537647989 +0.49748316 +0.472550443 +0.463281947 +0.379976666 +0.387449019 +0.464913062 +0.477187129 +0.537252859 +0.608077535 +0.662513511 +0.703951457 +0.730276411 +0.75356178 +0.767730713 +0.774703173 +0.7524746 +0.7274995 +0.639654032 +0.52302516 +0.428030819 +0.392135437 +0.379166466 +0.355270397 +0.303354161 +0.230390984 +0.17446127 +0.132464 +0.074622797 +0.046427761 +0.035716064 +0.023640642 +0.018215926 +0.019976042 +0.02988226 +0.046476133 +0.072996425 +0.106497079 +0.137661659 +0.162147923 +0.180330196 +0.218618301 +0.243982496 +0.240551082 +0.225425864 +0.195384092 +0.162696602 +0.133110115 +0.109225911 +0.098172278 +0.093931487 +0.092548162 +0.06666671 +0.043323577 +0.035606844 +0.033050202 +0.042877833 +0.059670726 +0.081376187 +0.104275992 +0.119670871 +0.127313573 +0.133065665 +0.139351571 +0.150276265 +0.20347446 +0.250458042 +0.266760858 +0.254182691 +0.228943454 +0.221248469 +0.217648484 +0.202941153 +0.190337529 +0.175192644 +0.16027958 +0.111385472 +0.06358879 +0.050137115 +0.04418596 +0.047363912 +0.056268215 +0.072099532 +0.093288367 +0.121556567 +0.1516196 +0.178264038 +0.199274372 +0.223152562 +0.332717603 +0.364731517 +0.344879119 +0.318980938 +0.289517904 +0.267468428 +0.253154977 +0.247997277 +0.25829239 +0.269617502 +0.270834149 +0.223838492 +0.10513709 +0.095292393 +0.05936802 +0.046292982 +0.046409527 +0.051871405 +0.059654707 +0.063969449 +0.069089458 +0.07712319 +0.086158831 +0.102456625 +0.170672252 +0.177216673 +0.164387983 +0.163886099 +0.194737522 +0.240970623 +0.281709475 +0.317820723 +0.337494292 +0.341580624 +0.337036505 +0.295830842 +0.120658265 +0.101552975 +0.075661 +0.069442905 +0.074215601 +0.079513427 +0.082370809 +0.081884505 +0.078021961 +0.075405136 +0.076284121 +0.093290181 +0.160963033 +0.16183597 +0.153293939 +0.171620848 +0.204065467 +0.250745316 +0.294423547 +0.32470342 +0.343592809 +0.352093859 +0.357686693 +0.344763068 +0.214442195 +0.257097825 +0.277979897 +0.27127473 +0.276522354 +0.301874465 +0.340321849 +0.376120591 +0.405544038 +0.427573997 +0.444359317 +0.4742685 +0.611200692 +0.646977782 +0.663189292 +0.681054052 +0.693730845 +0.703635107 +0.707620557 +0.698708947 +0.677205968 +0.661167487 +0.6535618 +0.580815285 +0.49215863 +0.554933759 +0.531374699 +0.496933383 +0.491443013 +0.515287043 +0.553343615 +0.59262274 +0.618319438 +0.62783445 +0.62910704 +0.629858564 +0.738704346 +0.74949095 +0.699147152 +0.635440081 +0.58084432 +0.562458532 +0.572300165 +0.587312211 +0.589094383 +0.579899567 +0.566885024 +0.49186018 +0.317349602 +0.353272014 +0.365501622 +0.408689856 +0.471607419 +0.542273533 +0.615640477 +0.680142887 +0.719337295 +0.727926858 +0.716421275 +0.696606918 +0.723795338 +0.670567723 +0.60979586 +0.576419509 +0.569844372 +0.581477093 +0.59661937 +0.604173706 +0.598139126 +0.579566805 +0.554567133 +0.509024045 +0.316153432 +0.300794341 +0.27975019 +0.245897174 +0.235486772 +0.247358115 +0.270969129 +0.29345087 +0.313805749 +0.32789173 +0.33143966 +0.341290847 +0.460449549 +0.466929844 +0.444087399 +0.417762654 +0.404258206 +0.391233683 +0.378905753 +0.372887066 +0.367929886 +0.361098931 +0.355678916 +0.345506 +0.147203771 +0.094697526 +0.079834474 +0.076624137 +0.077703348 +0.079820694 +0.083792869 +0.088446655 +0.096013756 +0.103488326 +0.109405529 +0.125652689 +0.183720526 +0.149658104 +0.108807817 +0.087544834 +0.082100222 +0.08373742 +0.086796115 +0.088286761 +0.089676894 +0.089179532 +0.087338083 +0.090442671 +0.031684615 +0.015212168 +0.015351683 +0.013392737 +0.01394919 +0.018343567 +0.026625204 +0.040680502 +0.058539672 +0.078174491 +0.102109648 +0.134310442 +0.230985608 +0.25989311 +0.278147284 +0.300698752 +0.329511548 +0.353854431 +0.361107588 +0.364320939 +0.365382945 +0.367825657 +0.369231977 +0.328505819 +0.171784423 +0.201923745 +0.198420441 +0.173265074 +0.166354586 +0.174907536 +0.194848814 +0.225063201 +0.260204497 +0.294017516 +0.325295742 +0.359055788 +0.453256363 +0.484767613 +0.502573549 +0.525798133 +0.521756506 +0.529617085 +0.538811848 +0.545615043 +0.542532024 +0.526977277 +0.509451256 +0.403120292 +0.176944867 +0.172735834 +0.166328534 +0.149011351 +0.145256583 +0.157666638 +0.181487389 +0.210785019 +0.24207271 +0.275740874 +0.313135703 +0.359526621 +0.426765052 +0.469528179 +0.515581515 +0.560899378 +0.61293537 +0.661077569 +0.674188512 +0.624133972 +0.568012558 +0.528693995 +0.508118699 +0.414906976 +0.201215245 +0.226103267 +0.195938957 +0.162318932 +0.150334952 +0.16091093 +0.188400971 +0.237271578 +0.300990359 +0.369248259 +0.441752339 +0.517734845 +0.648282701 +0.662688792 +0.623385497 +0.592978466 +0.54444034 +0.513212308 +0.487949705 +0.488250963 +0.512560922 +0.544167313 +0.560834322 +0.45722911 +0.430782408 +0.530777092 +0.464462303 +0.379891864 +0.322148823 +0.294781037 +0.286538652 +0.288222977 +0.295681625 +0.308480223 +0.32174025 +0.337299834 +0.484663293 +0.531805598 +0.489382395 +0.439126667 +0.405414136 +0.388542315 +0.379753903 +0.386440374 +0.41333601 +0.42764327 +0.421588351 +0.347746309 +0.164313308 +0.15833343 +0.107788571 +0.08709271 +0.082837202 +0.085345849 +0.090285452 +0.100410149 +0.109546331 +0.112585882 +0.110318698 +0.111722077 +0.191603912 +0.228322236 +0.250471873 +0.246657175 +0.226929452 +0.197518211 +0.16417268 +0.136165939 +0.113655143 +0.103436779 +0.104969213 +0.104035973 +0.045162725 +0.060049567 +0.061665092 +0.052255365 +0.04637958 +0.046523617 +0.051668844 +0.058965291 +0.066165344 +0.073493778 +0.085696043 +0.108586027 +0.175267789 +0.229434011 +0.254159917 +0.27648087 +0.303377973 +0.328516494 +0.346825434 +0.366319606 +0.393307193 +0.414610447 +0.429139184 +0.372534738 +0.17955506 +0.253862869 +0.206611629 +0.151075217 +0.126168722 +0.120376278 +0.125128191 +0.132971188 +0.145109389 +0.15921738 +0.179546638 +0.212260995 +0.273393871 +0.300514834 +0.321324122 +0.367158095 +0.439849609 +0.489923482 +0.52660336 +0.536498829 +0.518507183 +0.501462792 +0.491274673 +0.380626962 +0.177772217 +0.183082816 +0.129119338 +0.097682156 +0.08677591 +0.087136085 +0.096993518 +0.123809227 +0.16415728 +0.208558109 +0.252451663 +0.295370185 +0.379162804 +0.381661091 +0.358726148 +0.35071504 +0.364808879 +0.398347883 +0.433509926 +0.461809744 +0.455868967 +0.428773662 +0.3885917 +0.314009648 +0.112553085 +0.095585044 +0.073604128 +0.062678737 +0.072750141 +0.095326384 +0.122992991 +0.149509454 +0.1782648 +0.207983777 +0.241287304 +0.296752884 +0.441221666 +0.512974065 +0.555671112 +0.580667461 +0.598997904 +0.62491146 +0.645215283 +0.638566152 +0.635770067 +0.636045192 +0.642334136 +0.517434609 +0.297377675 +0.411802383 +0.310748707 +0.207588157 +0.151335311 +0.126763638 +0.120260054 +0.125988662 +0.135437856 +0.149574371 +0.172156625 +0.221486686 +0.371563343 +0.421852434 +0.424632294 +0.43031301 +0.458548298 +0.491755432 +0.538370588 +0.571316352 +0.572470803 +0.568054146 +0.568114607 +0.49134258 +0.307844088 +0.392148435 +0.372156442 +0.341546029 +0.340433245 +0.362262091 +0.401683388 +0.460507174 +0.53002136 +0.596985044 +0.655971339 +0.700983044 +0.81637902 +0.831288368 +0.804515472 +0.767237768 +0.722133006 +0.70123266 +0.70539223 +0.720834926 +0.739904671 +0.752577862 +0.76893686 +0.72091718 +0.574369239 +0.785145332 +0.84891957 +0.844847707 +0.82933872 +0.813641804 +0.803666619 +0.800391939 +0.803273207 +0.811891972 +0.822317477 +0.83045634 +0.861813076 +0.861693286 +0.850033432 +0.828287082 +0.803103534 +0.786263587 +0.769644477 +0.757808503 +0.741043256 +0.714084268 +0.685723059 +0.629572886 +0.450669251 +0.451795521 +0.425869112 +0.403279367 +0.391989612 +0.382785241 +0.381244806 +0.395781698 +0.431644561 +0.481173737 +0.534213725 +0.590873897 +0.682996217 +0.661166499 +0.635418276 +0.612292168 +0.589535884 +0.564625697 +0.537788288 +0.524356532 +0.530377344 +0.543605711 +0.55882102 +0.548081728 +0.392091146 +0.396597264 +0.398125932 +0.417817877 +0.45264118 +0.485462107 +0.521059187 +0.561670013 +0.603897775 +0.641172933 +0.669355682 +0.693403541 +0.754729463 +0.737730341 +0.722238685 +0.702389384 +0.681379452 +0.668340087 +0.660155325 +0.65951098 +0.659687351 +0.64825934 +0.633013902 +0.591153647 +0.388315887 +0.404894572 +0.399911396 +0.388854015 +0.384249163 +0.395593106 +0.426668109 +0.470928732 +0.52371599 +0.578213206 +0.618966986 +0.641579482 +0.700114853 +0.632557321 +0.548268841 +0.46872033 +0.420620941 +0.395285857 +0.377571139 +0.369085886 +0.36204785 +0.353270175 +0.346365302 +0.326874192 +0.134989641 +0.092872332 +0.077593113 +0.072951191 +0.077516646 +0.083877883 +0.09683251 +0.114426152 +0.127283419 +0.1360115 +0.14821097 +0.184001973 +0.262324728 +0.315081471 +0.344844499 +0.31991458 +0.313395277 +0.308583934 +0.340866872 +0.384234851 +0.419553516 +0.458443059 +0.469046344 +0.405460327 +0.339105333 +0.290197278 +0.216223793 +0.146963778 +0.094214052 +0.061733975 +0.045367776 +0.04142396 +0.047560653 +0.065649906 +0.101758299 +0.185227676 +0.400821517 +0.534473572 +0.609761949 +0.635085638 +0.637206004 +0.627231843 +0.589971491 +0.541227339 +0.509859581 +0.4803372 +0.459040287 +0.397161471 +0.367436885 +0.312326126 +0.219643642 +0.13481283 +0.080535322 +0.052645036 +0.040870383 +0.040577132 +0.047818096 +0.065108351 +0.102066707 +0.173583502 +0.30780813 +0.377844212 +0.389779896 +0.411348212 +0.43654063 +0.460174569 +0.465850256 +0.450643016 +0.42897698 +0.420475997 +0.428471413 +0.370461655 +0.325454184 +0.346416885 +0.334006048 +0.320359182 +0.315668159 +0.318739371 +0.326120306 +0.333062473 +0.34472667 +0.371399015 +0.416065392 +0.485066349 +0.610248567 +0.643292857 +0.632064734 +0.617298199 +0.606022889 +0.599744365 +0.600576949 +0.607049616 +0.605246394 +0.589006501 +0.564729632 +0.498195145 +0.458091328 +0.461086414 +0.422539482 +0.390099735 +0.373334263 +0.377483807 +0.406568116 +0.450839413 +0.503946973 +0.56453727 +0.629693323 +0.689399579 +0.750582028 +0.739982586 +0.690949587 +0.646849469 +0.615008832 +0.576812097 +0.552494342 +0.549147304 +0.555176394 +0.564824991 +0.578250949 +0.493972419 +0.482025795 +0.578044026 +0.594768587 +0.602109359 +0.627382186 +0.670688865 +0.722078965 +0.772226374 +0.806566236 +0.821858908 +0.824024705 +0.817104858 +0.845640541 +0.821530278 +0.806551254 +0.804196237 +0.802638158 +0.798670267 +0.785462509 +0.762440357 +0.737181718 +0.720277708 +0.706624795 +0.616139226 +0.51850691 +0.555317664 +0.544799556 +0.534564167 +0.533569239 +0.540485804 +0.551257017 +0.56884949 +0.589461355 +0.606644519 +0.625429258 +0.653830751 +0.722161806 +0.710543874 +0.680794611 +0.662369497 +0.656720475 +0.643812548 +0.625556108 +0.605676723 +0.586731232 +0.578338105 +0.575562585 +0.529036189 +0.39780837 +0.411843139 +0.411174002 +0.41248744 +0.417677592 +0.431969128 +0.451040982 +0.476428518 +0.501031411 +0.522030804 +0.541794606 +0.563196271 +0.623494149 +0.621463059 +0.581647168 +0.534060742 +0.491517791 +0.468192637 +0.471295796 +0.490646187 +0.515129682 +0.537254033 +0.553613601 +0.524204853 +0.325034445 +0.341828894 +0.362895559 +0.385877872 +0.40756753 +0.440742201 +0.48620579 +0.531768542 +0.574043228 +0.611205093 +0.640899117 +0.66966552 +0.724082332 +0.720202172 +0.704148706 +0.681856979 +0.652579001 +0.626315017 +0.619513053 +0.636438303 +0.667828929 +0.698099334 +0.714661382 +0.662591675 +0.442011282 +0.555888388 +0.606887112 +0.634112381 +0.655961756 +0.685512084 +0.72194952 +0.752381585 +0.772781768 +0.782873148 +0.789877974 +0.796559801 +0.847558386 +0.837475412 +0.795425603 +0.730213544 +0.670586028 +0.632943127 +0.616714488 +0.606233904 +0.591107743 +0.572249792 +0.558906406 +0.495975025 +0.276918152 +0.35495924 +0.394176796 +0.456261588 +0.515731286 +0.562505051 +0.595714805 +0.618591424 +0.634734037 +0.638663399 +0.628057923 +0.602826264 +0.675371017 +0.650435897 +0.591019377 +0.535323465 +0.510794274 +0.501255601 +0.500207914 +0.498835106 +0.493996024 +0.489773952 +0.483430533 +0.452145614 +0.222863574 +0.216673728 +0.250003572 +0.297691063 +0.354937898 +0.417851781 +0.479652093 +0.519189216 +0.539573982 +0.54938846 +0.550343824 +0.555675369 +0.672193602 +0.645771367 +0.599910296 +0.569428726 +0.546295555 +0.526633031 +0.504653612 +0.493607443 +0.495213708 +0.495858965 +0.49445916 +0.440354183 +0.266714711 +0.278650539 +0.27565958 +0.283357325 +0.30938851 +0.34879191 +0.39351357 +0.435850453 +0.47424188 +0.510752947 +0.54316318 +0.57674077 +0.697123972 +0.708811975 +0.663939891 +0.59823602 +0.530891975 +0.501049307 +0.50114868 +0.496994298 +0.489148301 +0.480415091 +0.469799775 +0.400577987 +0.21135753 +0.197872852 +0.176772626 +0.181164581 +0.188348477 +0.192227382 +0.192964032 +0.196408939 +0.204012697 +0.216910723 +0.230148298 +0.256079846 +0.373339918 +0.36863017 +0.318778957 +0.283465318 +0.267390571 +0.282944248 +0.297052488 +0.315053091 +0.325033001 +0.323623082 +0.312239146 +0.252303983 +0.135608195 +0.089349421 +0.051144861 +0.030042701 +0.025058835 +0.02692934 +0.032497966 +0.043213995 +0.062999984 +0.093846493 +0.142075005 +0.20594021 +0.31017201 +0.30937848 +0.279657874 +0.272676801 +0.286528048 +0.316573208 +0.327235526 +0.325579645 +0.318723322 +0.310154733 +0.293708949 +0.225426806 +0.158603716 +0.107868367 +0.067519388 +0.043406652 +0.032861641 +0.032448978 +0.039653981 +0.057063675 +0.086129656 +0.120046395 +0.157420978 +0.200012008 +0.290108189 +0.372341498 +0.433825187 +0.494965286 +0.535354051 +0.560796566 +0.568214157 +0.569459171 +0.541129172 +0.498642632 +0.453118839 +0.353211169 +0.28636595 +0.228098022 +0.155747248 +0.102483021 +0.078140339 +0.073809502 +0.083066698 +0.101558411 +0.126630544 +0.155996822 +0.195449799 +0.253637597 +0.348417213 +0.402373394 +0.425903477 +0.444242093 +0.455649604 +0.468015534 +0.47589101 +0.473915861 +0.460223905 +0.439855923 +0.421672129 +0.358396788 +0.266810441 +0.239860532 +0.20365822 +0.173276395 +0.160037005 +0.164986522 +0.189828503 +0.222959151 +0.259735875 +0.294645324 +0.330243216 +0.379022752 +0.519798007 +0.586081939 +0.603335999 +0.593322484 +0.586828105 +0.583883269 +0.590550662 +0.606293853 +0.608220373 +0.582840749 +0.544808073 +0.462999025 +0.382863806 +0.340327983 +0.280250205 +0.238424994 +0.226504966 +0.245239952 +0.289817007 +0.337646124 +0.377411906 +0.411729358 +0.441209185 +0.480257155 +0.590073589 +0.61699982 +0.630541858 +0.633525632 +0.619184075 +0.600022427 +0.588036074 +0.584183488 +0.58468523 +0.578454864 +0.565567379 +0.479874515 +0.390176609 +0.377246069 +0.340701386 +0.325628403 +0.316361174 +0.306891679 +0.296030733 +0.286336752 +0.279366743 +0.27364228 +0.271319789 +0.291764897 +0.369398558 +0.374440487 +0.404935537 +0.444792725 +0.484463008 +0.508446942 +0.515411502 +0.502260358 +0.480714076 +0.459275037 +0.438000681 +0.393003142 +0.34947577 +0.322813236 +0.274316998 +0.230729928 +0.198648989 +0.182183168 +0.179013213 +0.192776707 +0.217177952 +0.245793761 +0.288215362 +0.361852976 +0.511118674 +0.565662181 +0.55889197 +0.547341086 +0.536162175 +0.517519512 +0.49307182 +0.478012217 +0.463836747 +0.443000961 +0.425750704 +0.350779775 +0.351175234 +0.351926086 +0.338562547 +0.329576877 +0.3274394 +0.327847921 +0.326150058 +0.327046518 +0.331340507 +0.340278292 +0.359505111 +0.393716183 +0.501815863 +0.487241643 +0.419631778 +0.362130816 +0.332778327 +0.329982637 +0.331584635 +0.337968459 +0.352413835 +0.360978466 +0.366216072 +0.311835614 +0.251050274 +0.28719991 +0.299196153 +0.314806735 +0.322133227 +0.320412383 +0.313530838 +0.310680279 +0.311670617 +0.316798083 +0.33030221 +0.362707922 +0.478557264 +0.470551226 +0.418448404 +0.385364137 +0.38267366 +0.395440835 +0.401493709 +0.396646878 +0.383129239 +0.37624104 +0.376098131 +0.311571586 +0.213946812 +0.244104719 +0.227448586 +0.211133333 +0.207886579 +0.215009358 +0.227210109 +0.248666586 +0.273025642 +0.296324975 +0.321202895 +0.355431076 +0.484931665 +0.479361897 +0.400644407 +0.346212945 +0.32216303 +0.319644115 +0.330942336 +0.346019324 +0.358700797 +0.36310538 +0.35962831 +0.30797259 +0.154521643 +0.165389365 +0.187986561 +0.20727888 +0.221259201 +0.23338233 +0.244363876 +0.251586882 +0.258305336 +0.27149577 +0.288909684 +0.32205392 +0.448215328 +0.47428562 +0.449475607 +0.421213899 +0.396115664 +0.367570626 +0.350069681 +0.339917924 +0.339220719 +0.339680985 +0.334512399 +0.288101326 +0.14798042 +0.151822887 +0.169294693 +0.190520395 +0.210855092 +0.232120233 +0.256786752 +0.276891118 +0.291709936 +0.306013774 +0.319121387 +0.332211849 +0.423461301 +0.408287871 +0.365210442 +0.345668297 +0.34809155 +0.365318545 +0.385493046 +0.398467364 +0.39588278 +0.387163723 +0.374263812 +0.301989155 +0.178425552 +0.138086876 +0.1167562 +0.119501059 +0.135191759 +0.160194095 +0.19202665 +0.226462347 +0.258360067 +0.282987757 +0.305570061 +0.332434684 +0.426859237 +0.41347903 +0.372880171 +0.364382869 +0.375896311 +0.404787824 +0.406759437 +0.390559147 +0.369674061 +0.3541844 +0.340665582 +0.277749121 +0.162520456 +0.0995789 +0.06095635 +0.04356781 +0.043758193 +0.053280419 +0.0708714 +0.095364535 +0.125893563 +0.161348118 +0.202588475 +0.25080931 +0.357643952 +0.360563902 +0.317386689 +0.293921938 +0.287973152 +0.298729527 +0.313536291 +0.32558627 +0.333156674 +0.32635312 +0.311696401 +0.248437688 +0.12438653 +0.083098155 +0.055373812 +0.042196458 +0.040905474 +0.049155107 +0.065671526 +0.087273981 +0.111137479 +0.137462306 +0.166801053 +0.20464428 +0.303382151 +0.329022494 +0.348244916 +0.368041721 +0.36354335 +0.346777568 +0.33900496 +0.329681029 +0.319130874 +0.317943869 +0.313430921 +0.261943837 +0.159166533 +0.105154663 +0.066180895 +0.049683842 +0.062022336 +0.09054373 +0.119343626 +0.147494626 +0.179677173 +0.214058917 +0.252731524 +0.304204207 +0.394902461 +0.418158908 +0.427281283 +0.446374118 +0.457486168 +0.44397908 +0.422197506 +0.398717422 +0.388251242 +0.380143658 +0.366174926 +0.324292307 +0.26151709 +0.242904297 +0.21011176 +0.189764497 +0.180418352 +0.179989774 +0.18129229 +0.184199248 +0.186366596 +0.19039471 +0.202976414 +0.240240423 +0.321798883 +0.312137438 +0.283068345 +0.310262822 +0.367819997 +0.417365684 +0.439512366 +0.426899634 +0.402780555 +0.372727263 +0.344579959 +0.274742581 +0.230216718 +0.204775762 +0.167066781 +0.140696136 +0.122468897 +0.113210964 +0.11227376 +0.113697288 +0.116865137 +0.12364739 +0.141182574 +0.183249981 +0.288375546 +0.358914148 +0.383092212 +0.418211178 +0.44031086 +0.443249586 +0.422236048 +0.373127226 +0.3155899 +0.271698955 +0.245408574 +0.199387389 +0.169414073 +0.166862464 +0.143912645 +0.11863488 +0.095893449 +0.078811972 +0.067724555 +0.063121054 +0.065519606 +0.075682166 +0.102747907 +0.156265925 +0.26173247 +0.309347028 +0.309864492 +0.313046972 +0.318151759 +0.324131699 +0.318329821 +0.304952 +0.276509346 +0.255232834 +0.257910046 +0.243198154 +0.178985594 +0.195979647 +0.201034673 +0.209599717 +0.223608693 +0.237657526 +0.256069299 +0.275428435 +0.292729613 +0.309444014 +0.329227559 +0.357542547 +0.473325267 +0.471330273 +0.40406337 +0.336281743 +0.288039089 +0.272939507 +0.285611295 +0.322632164 +0.373181854 +0.413837427 +0.449576219 +0.426070952 +0.363101017 +0.363807963 +0.320092171 +0.272555592 +0.241177457 +0.229216878 +0.235391504 +0.251724351 +0.27199528 +0.296567721 +0.322848422 +0.359793053 +0.446281022 +0.41997836 +0.403694152 +0.419540501 +0.462965305 +0.509289016 +0.53059424 +0.5264104 +0.516341108 +0.504974702 +0.492784726 +0.427605622 +0.35089081 +0.346666305 +0.310948824 +0.292033662 +0.289427102 +0.309103368 +0.351696296 +0.410667247 +0.470644938 +0.526202414 +0.571250722 +0.604892017 +0.64816284 +0.613944467 +0.559406274 +0.506773716 +0.459718053 +0.424294536 +0.400482193 +0.38439933 +0.380583765 +0.383376257 +0.389450845 +0.339357702 +0.314671664 +0.331795686 +0.317821292 +0.298475496 +0.28569499 +0.283054819 +0.283433972 +0.283065424 +0.275917534 +0.267758357 +0.268157443 +0.301487521 +0.415981303 +0.379979903 +0.326549805 +0.302349627 +0.299043106 +0.311236922 +0.332691201 +0.347899613 +0.353275296 +0.359348005 +0.365966034 +0.346495488 +0.173648067 +0.147043527 +0.143767886 +0.154716937 +0.170811363 +0.189763707 +0.204772817 +0.212979213 +0.214177381 +0.217560602 +0.227324084 +0.257930148 +0.358635582 +0.338523486 +0.281149738 +0.248783787 +0.241160866 +0.251249144 +0.262427465 +0.257986702 +0.243302397 +0.218985022 +0.1938323 +0.169326452 +0.068306301 +0.038709407 +0.031313572 +0.030120419 +0.031953637 +0.035893442 +0.041218417 +0.049163893 +0.059547937 +0.074471289 +0.100348256 +0.154424602 +0.238196849 +0.194663245 +0.157875504 +0.163758377 +0.195328664 +0.234576034 +0.263500042 +0.267721701 +0.252995295 +0.229535071 +0.210652469 +0.174194914 +0.10044699 +0.060948959 +0.034945147 +0.016480582 +0.012759864 +0.015437807 +0.02119836 +0.029294427 +0.041648021 +0.061424746 +0.09368438 +0.144132814 +0.218909199 +0.207389041 +0.18886901 +0.198816734 +0.250095364 +0.30495282 +0.338854348 +0.352297564 +0.338094141 +0.314351502 +0.288041073 +0.228858814 +0.114706172 +0.061783092 +0.035797894 +0.020789913 +0.015680523 +0.018009916 +0.024180581 +0.033409616 +0.047119047 +0.067824594 +0.102833059 +0.165261554 +0.261601707 +0.266839244 +0.266015967 +0.313846903 +0.381831058 +0.412537821 +0.399514442 +0.365929405 +0.333101629 +0.309108317 +0.298217498 +0.254876155 +0.146987113 +0.107063057 +0.070866621 +0.04360491 +0.031243578 +0.030614267 +0.033997437 +0.041684525 +0.052565831 +0.069267648 +0.102215096 +0.155805062 +0.244427184 +0.265703929 +0.278470566 +0.323892242 +0.367401953 +0.390496302 +0.389868872 +0.388415335 +0.373491062 +0.354901139 +0.332014289 +0.272281619 +0.232731353 +0.217934629 +0.177539708 +0.140928259 +0.117924976 +0.1087315 +0.106588146 +0.100145425 +0.098213069 +0.104899303 +0.125282615 +0.179922256 +0.330840236 +0.401992554 +0.414683375 +0.436019317 +0.469875359 +0.466794078 +0.427643138 +0.370538119 +0.312295067 +0.26418949 +0.240208741 +0.204559481 +0.184095566 +0.192495026 +0.183450975 +0.174838847 +0.169484019 +0.167373991 +0.162254277 +0.15425696 +0.152478366 +0.172121492 +0.213864155 +0.2825903 +0.418647752 +0.443247959 +0.420741536 +0.395023106 +0.372449969 +0.34836968 +0.30607117 +0.266585588 +0.224445367 +0.195821636 +0.179136934 +0.144947247 +0.097144011 +0.081590651 +0.064303556 +0.05356066 +0.047842606 +0.047080563 +0.050076613 +0.052686921 +0.055203311 +0.061538083 +0.075442456 +0.11862651 +0.252874909 +0.320026402 +0.32626807 +0.310474883 +0.281376751 +0.268409488 +0.276395071 +0.275222113 +0.261833281 +0.229577685 +0.197284739 +0.155524061 +0.110279113 +0.081209331 +0.059155133 +0.041790916 +0.034255851 +0.032746843 +0.036095216 +0.043904762 +0.057759543 +0.077337906 +0.107362159 +0.16227108 +0.264205548 +0.298349905 +0.314227991 +0.306978355 +0.31928942 +0.340048329 +0.379838011 +0.389196282 +0.361610346 +0.329107449 +0.292621014 +0.228982804 +0.119566713 +0.077233464 +0.047764651 +0.027579751 +0.020362525 +0.020140759 +0.025912729 +0.036722185 +0.053834035 +0.078400113 +0.115200797 +0.166016267 +0.237913943 +0.242483631 +0.261778334 +0.302940614 +0.345921508 +0.386551367 +0.410356143 +0.40427176 +0.383173584 +0.35028292 +0.319202539 +0.252009177 +0.155724993 +0.127068835 +0.087864946 +0.061799378 +0.051641456 +0.052458346 +0.06010675 +0.072334889 +0.09253685 +0.123411069 +0.166834234 +0.216799338 +0.303766221 +0.326442468 +0.347917081 +0.392312311 +0.415891017 +0.406456799 +0.374110414 +0.329878417 +0.28193644 +0.239679805 +0.212942772 +0.16914131 +0.158805577 +0.146358533 +0.120303778 +0.102537554 +0.094098105 +0.0942609 +0.102299678 +0.118266795 +0.137891413 +0.158975249 +0.199885795 +0.266391878 +0.35557081 +0.356668192 +0.319301637 +0.302663216 +0.31269667 +0.333591214 +0.32787458 +0.300942285 +0.267509978 +0.224962 +0.198973592 +0.160807015 +0.155748537 +0.166335304 +0.167733366 +0.166082695 +0.163984663 +0.167023112 +0.179378636 +0.198883991 +0.219683661 +0.236680619 +0.249100434 +0.27276674 +0.346508908 +0.382907437 +0.401576482 +0.411858038 +0.406625571 +0.382932186 +0.351147267 +0.319503587 +0.297139472 +0.281967561 +0.281606455 +0.259685645 +0.247808469 +0.26684961 +0.262234517 +0.250942075 +0.237817507 +0.242979011 +0.261254044 +0.28820516 +0.322534044 +0.360725631 +0.388838652 +0.413987117 +0.485538157 +0.503277367 +0.50477147 +0.491718619 +0.466406464 +0.437493156 +0.404845469 +0.393574207 +0.385081878 +0.383109253 +0.388647187 +0.35673277 +0.334650573 +0.372132527 +0.367190457 +0.364593019 +0.377617584 +0.402249773 +0.434283997 +0.466904304 +0.496254913 +0.521437291 +0.540510565 +0.5581314 +0.570610466 +0.541440738 +0.516016379 +0.496253121 +0.481955656 +0.466088384 +0.446838101 +0.428992784 +0.418995771 +0.413151538 +0.405486465 +0.369421264 +0.293022403 +0.31737108 +0.282345255 +0.257626155 +0.253815072 +0.259860382 +0.268077639 +0.279664786 +0.290806103 +0.299851389 +0.309617899 +0.335565059 +0.388652607 +0.364111082 +0.336328166 +0.326732835 +0.334492874 +0.353515656 +0.371161568 +0.385768226 +0.394009893 +0.392146564 +0.389120392 +0.367386665 +0.197869465 +0.180489732 +0.143760106 +0.118851877 +0.115025769 +0.122937296 +0.134907968 +0.154114621 +0.178999754 +0.204444875 +0.229392844 +0.268147641 +0.314589121 +0.242237179 +0.190368345 +0.178639039 +0.200082712 +0.238033353 +0.257585877 +0.261007141 +0.26097006 +0.25421048 +0.245777293 +0.234366126 +0.12586337 +0.093801833 +0.079065911 +0.071974341 +0.068720275 +0.066619671 +0.06586125 +0.064458106 +0.065721383 +0.069336414 +0.074000991 +0.098687326 +0.149059439 +0.132181418 +0.124878916 +0.134664508 +0.181655131 +0.211106631 +0.216465476 +0.193583464 +0.166555562 +0.157824321 +0.146929288 +0.132218344 +0.073544385 +0.046718401 +0.032769184 +0.027241607 +0.029318026 +0.037088581 +0.048544702 +0.060797092 +0.075854261 +0.092962788 +0.114911597 +0.158701491 +0.241344405 +0.270659206 +0.29491401 +0.305654097 +0.308251025 +0.305039352 +0.301685034 +0.298464634 +0.293635576 +0.303745987 +0.316342646 +0.295085594 +0.20752556 +0.196486999 +0.185757669 +0.171223188 +0.17433175 +0.190433942 +0.218500844 +0.254419572 +0.293622344 +0.333231667 +0.373045012 +0.433911488 +0.507025605 +0.501303565 +0.495585404 +0.473037698 +0.46004734 +0.456841588 +0.447581797 +0.437011528 +0.424950605 +0.399370793 +0.381485682 +0.324306433 +0.209516444 +0.185480755 +0.158159699 +0.138015515 +0.119916664 +0.10601889 +0.09814419 +0.094032088 +0.094819174 +0.101095203 +0.109181269 +0.140408681 +0.205005557 +0.228867886 +0.240071448 +0.290438278 +0.32711791 +0.344229434 +0.344453791 +0.328459931 +0.303514035 +0.268995045 +0.236154566 +0.189729774 +0.120341381 +0.095952629 +0.078020831 +0.067793708 +0.061134066 +0.057373711 +0.056782938 +0.054976643 +0.055540505 +0.060205408 +0.072897265 +0.110989211 +0.194655096 +0.250702047 +0.287190691 +0.298389676 +0.312813307 +0.331494693 +0.313688544 +0.26674599 +0.218598028 +0.185588444 +0.176526552 +0.163318352 +0.111751862 +0.092976373 +0.068999958 +0.053124054 +0.04737099 +0.048521403 +0.055161741 +0.063063321 +0.070085086 +0.078278184 +0.097605239 +0.146495229 +0.244535527 +0.267799619 +0.270374057 +0.288902254 +0.320990037 +0.352630151 +0.372233493 +0.361268586 +0.342010468 +0.323260645 +0.305037769 +0.273688822 +0.185003077 +0.124740874 +0.098030233 +0.089357449 +0.086821821 +0.089231168 +0.097262264 +0.110715495 +0.126096617 +0.143351942 +0.172407225 +0.240468436 +0.342286319 +0.362832275 +0.358050931 +0.364733683 +0.391541397 +0.414329827 +0.425773705 +0.404015429 +0.369773282 +0.337360568 +0.310326963 +0.268201974 +0.196106202 +0.152734891 +0.120928533 +0.095950988 +0.079151723 +0.070400121 +0.068510517 +0.070958572 +0.078796445 +0.094455948 +0.123373912 +0.186103666 +0.282348077 +0.298602706 +0.287399994 +0.289079894 +0.325717818 +0.37251549 +0.415282424 +0.417748392 +0.389073282 +0.358655766 +0.328756566 +0.283068789 +0.193425747 +0.138523297 +0.090271882 +0.056679468 +0.039056739 +0.031631564 +0.030706967 +0.035651729 +0.050833367 +0.07964962 +0.125665091 +0.195612875 +0.298631861 +0.289219312 +0.255404313 +0.243246801 +0.250716427 +0.276400072 +0.299508902 +0.317784898 +0.32807519 +0.321538536 +0.305759154 +0.277922513 +0.193491914 +0.135198884 +0.096382379 +0.075330199 +0.074156448 +0.078104478 +0.084972014 +0.092947128 +0.102768445 +0.123751851 +0.162733539 +0.225053516 +0.292167224 +0.268525901 +0.266587959 +0.311429717 +0.349171624 +0.380440792 +0.410736568 +0.433017097 +0.450801717 +0.456349344 +0.467198025 +0.480721275 +0.388536462 +0.396165278 +0.38836836 +0.377548112 +0.38174205 +0.392708832 +0.409026755 +0.439274305 +0.478068575 +0.523473935 +0.581080804 +0.655511677 +0.714071681 +0.722807897 +0.722166371 +0.721505914 +0.717000216 +0.684678988 +0.658770085 +0.648108921 +0.645321685 +0.652528166 +0.663943989 +0.643922185 +0.611955473 +0.632280942 +0.585762426 +0.546075007 +0.516499379 +0.494754915 +0.47803282 +0.472177538 +0.469996992 +0.460782053 +0.444867448 +0.436144006 +0.50192403 +0.480881659 +0.439730188 +0.400925883 +0.382593191 +0.375058255 +0.364439412 +0.347738363 +0.336576372 +0.330612568 +0.326868209 +0.310544046 +0.184148651 +0.130685003 +0.093923926 +0.088197231 +0.089386736 +0.094021934 +0.09731108 +0.09349923 +0.086053949 +0.084933446 +0.099027203 +0.138457362 +0.200684312 +0.172886976 +0.131460943 +0.129138022 +0.1555186 +0.187742756 +0.216874738 +0.231943538 +0.236941829 +0.235447134 +0.228430206 +0.203183013 +0.089191177 +0.041709412 +0.020331429 +0.012042267 +0.012030479 +0.016887843 +0.025729257 +0.037113591 +0.049018619 +0.061833272 +0.086497585 +0.143520312 +0.227908378 +0.200168473 +0.167420389 +0.177705622 +0.212685756 +0.232092783 +0.227563762 +0.217054659 +0.203047203 +0.185219471 +0.164698827 +0.132915258 +0.056406025 +0.025234161 +0.011802632 +0.005958392 +0.005624915 +0.009169624 +0.0160002 +0.025600968 +0.039906582 +0.064004956 +0.107757128 +0.181494392 +0.271722054 +0.250700569 +0.227072054 +0.218976056 +0.226559493 +0.238437452 +0.231209725 +0.22766773 +0.229605077 +0.229571513 +0.223614544 +0.193384993 +0.107805079 +0.059433043 +0.032949097 +0.021772716 +0.026077537 +0.038700895 +0.057426517 +0.084244685 +0.115273049 +0.149184729 +0.19125403 +0.250532958 +0.328914588 +0.27944445 +0.233592991 +0.224311354 +0.248896106 +0.283147609 +0.302390461 +0.296736934 +0.280900119 +0.264585557 +0.25418783 +0.242662069 +0.143572591 +0.110600474 +0.112147154 +0.118792363 +0.134417332 +0.154963028 +0.180306057 +0.211443157 +0.240805342 +0.266287129 +0.29460145 +0.350278134 +0.437250127 +0.415664322 +0.381798082 +0.347936672 +0.321547081 +0.301854479 +0.286288278 +0.272594068 +0.256163679 +0.236526088 +0.219058276 +0.202831201 +0.108853574 +0.086312564 +0.098292444 +0.107859224 +0.118168436 +0.129318077 +0.140493535 +0.157839045 +0.177873608 +0.197871826 +0.226344943 +0.29059287 +0.372812267 +0.333450108 +0.27710844 +0.251925011 +0.259049802 +0.272020629 +0.272953863 +0.274662063 +0.279841406 +0.271826593 +0.262782439 +0.243977719 +0.125274386 +0.079821688 +0.055118047 +0.038798311 +0.036247933 +0.040292341 +0.045327505 +0.054664473 +0.069651522 +0.089852062 +0.122215115 +0.196407775 +0.286174462 +0.279559506 +0.274103308 +0.303825983 +0.347130429 +0.366628542 +0.377128605 +0.37168687 +0.350649012 +0.323649196 +0.29819821 +0.252778497 +0.147621389 +0.0887292 +0.049219161 +0.030043571 +0.028107607 +0.034592942 +0.043240716 +0.054864231 +0.06727016 +0.082993742 +0.108069564 +0.169942828 +0.279546726 +0.284822911 +0.258967738 +0.236720405 +0.242713706 +0.260066498 +0.273977584 +0.273222318 +0.266772053 +0.257061126 +0.240912803 +0.219333425 +0.149257707 +0.102750114 +0.070381903 +0.053633585 +0.053752787 +0.061040346 +0.070285235 +0.080419286 +0.091734683 +0.107958298 +0.138053688 +0.207825096 +0.308770207 +0.345294487 +0.340054516 +0.326401684 +0.341049552 +0.348709571 +0.353837277 +0.345493531 +0.33361194 +0.322556311 +0.318050905 +0.298847656 +0.177076432 +0.130329893 +0.107440286 +0.101392403 +0.101416104 +0.105063501 +0.112055046 +0.127572567 +0.146983901 +0.16950226 +0.208614734 +0.292879788 +0.400467237 +0.380606462 +0.338011201 +0.30457871 +0.288732132 +0.289510418 +0.296379142 +0.301391967 +0.307175758 +0.314036486 +0.316627695 +0.308034468 +0.192692615 +0.146567295 +0.124844546 +0.103627601 +0.095572942 +0.089711903 +0.082266197 +0.080609775 +0.082953574 +0.086680195 +0.099817221 +0.165369592 +0.302310342 +0.303891976 +0.277990423 +0.2662932 +0.270131094 +0.275082821 +0.270189763 +0.250915132 +0.224317111 +0.200641212 +0.18902719 +0.187508156 +0.100208138 +0.05611345 +0.037137967 +0.033061387 +0.033936804 +0.035532592 +0.037649037 +0.040148879 +0.044199338 +0.050943678 +0.062439004 +0.100714259 +0.179044235 +0.197014164 +0.185912394 +0.18744689 +0.186872302 +0.205651056 +0.217217126 +0.239257719 +0.252457692 +0.247636767 +0.237532209 +0.209270753 +0.115203185 +0.089817557 +0.064393721 +0.045941016 +0.038163803 +0.038902246 +0.045427223 +0.055908136 +0.068304307 +0.081326405 +0.095973032 +0.141994513 +0.234499579 +0.255034696 +0.244550216 +0.240527684 +0.244095724 +0.242874907 +0.241926167 +0.234126249 +0.219809066 +0.204944715 +0.190024133 +0.173513295 +0.084791478 +0.055725043 +0.042773571 +0.035305253 +0.038693629 +0.050755262 +0.071125712 +0.097061364 +0.123306937 +0.14902592 +0.17945056 +0.255441353 +0.351954948 +0.332786766 +0.286196781 +0.243161448 +0.22317218 +0.231771114 +0.25207401 +0.25910513 +0.244693302 +0.22143389 +0.202052218 +0.196899722 +0.123380127 +0.083912064 +0.062322674 +0.052434151 +0.060654943 +0.083172163 +0.117843129 +0.154566337 +0.179474831 +0.1962373 +0.215378241 +0.308340302 +0.410457437 +0.398812589 +0.365420492 +0.330674595 +0.293560268 +0.264449387 +0.247768654 +0.238735884 +0.239259767 +0.256224945 +0.286467975 +0.314377554 +0.212625779 +0.126658809 +0.08883398 +0.067116112 +0.058400424 +0.061567763 +0.070564786 +0.082628422 +0.094155692 +0.106648473 +0.125783023 +0.203091222 +0.293528376 +0.267611603 +0.210209427 +0.165001652 +0.156071948 +0.188860635 +0.238032652 +0.276081628 +0.309564987 +0.328439201 +0.327491981 +0.310973697 +0.166103572 +0.117840406 +0.093495431 +0.070956397 +0.060786585 +0.062779127 +0.073366179 +0.090199375 +0.108407302 +0.130319876 +0.161569495 +0.262429912 +0.378814644 +0.351400677 +0.311042136 +0.302360867 +0.314683141 +0.343007878 +0.387354291 +0.421644511 +0.437388795 +0.433120342 +0.422776367 +0.405926217 +0.267240678 +0.194395865 +0.150909593 +0.111480553 +0.088126715 +0.081849148 +0.085065281 +0.096575661 +0.113297792 +0.137390781 +0.168688984 +0.245372176 +0.338447015 +0.340898599 +0.33542074 +0.339936736 +0.346476132 +0.351390553 +0.37164536 +0.399988971 +0.422874315 +0.436312624 +0.432695102 +0.423576433 +0.290462739 +0.23722735 +0.211662829 +0.180655322 +0.159981528 +0.159456372 +0.168406269 +0.188166453 +0.208670764 +0.23101321 +0.265082524 +0.365766018 +0.487663077 +0.494503915 +0.479931868 +0.466363168 +0.452587678 +0.4456225 +0.450289838 +0.451173177 +0.437762038 +0.426355386 +0.400800495 +0.364512747 +0.197747969 +0.130991555 +0.115277375 +0.113824942 +0.133499999 +0.161736504 +0.188777771 +0.216731833 +0.238703972 +0.252706317 +0.26610898 +0.338941983 +0.38779231 +0.30799201 +0.227665529 +0.182948263 +0.167344833 +0.166642672 +0.173927287 +0.176117744 +0.17230379 +0.166796375 +0.163110339 +0.158523711 +0.112730526 +0.056938328 +0.049849552 +0.052119603 +0.057523237 +0.065185596 +0.074615564 +0.093335522 +0.113683922 +0.128851782 +0.14404852 +0.217963701 +0.240324181 +0.180987213 +0.135607447 +0.120620527 +0.14088015 +0.177927773 +0.220800059 +0.237170246 +0.233662493 +0.225478866 +0.213385905 +0.205884772 +0.155278186 +0.10312709 +0.073186122 +0.053343951 +0.038277125 +0.035833621 +0.044005201 +0.055232807 +0.065339686 +0.077246384 +0.101272141 +0.189729304 +0.231497347 +0.185582756 +0.154132716 +0.188399614 +0.278375252 +0.338214763 +0.359174141 +0.359306327 +0.352449471 +0.344551245 +0.333603871 +0.312185344 +0.156182063 +0.063794246 +0.037054361 +0.023787204 +0.021142869 +0.024923382 +0.037811708 +0.060949497 +0.092808756 +0.128938315 +0.166251048 +0.24840535 +0.307103825 +0.290717921 +0.28850616 +0.306944073 +0.355302689 +0.387262938 +0.381662764 +0.372240017 +0.37531919 +0.389852158 +0.404082788 +0.415925081 +0.285721777 +0.18140466 +0.172766994 +0.1663511 +0.172849118 +0.194363081 +0.23156126 +0.291383112 +0.360492656 +0.421251474 +0.474509544 +0.57060312 +0.63637323 +0.556423913 +0.459558353 +0.368398675 +0.304310492 +0.263443006 +0.246441807 +0.24008367 +0.24250923 +0.253282969 +0.26498849 +0.273243163 +0.207450229 +0.153317173 +0.172534124 +0.184028168 +0.214562721 +0.258311652 +0.307198124 +0.353634343 +0.385785544 +0.406980049 +0.424125881 +0.500128131 +0.517544014 +0.430393719 +0.333644461 +0.292853441 +0.288485412 +0.300440049 +0.314522443 +0.323894096 +0.323104172 +0.318076126 +0.307499062 +0.284375622 +0.159313173 +0.104857912 +0.088963062 +0.080839609 +0.080260422 +0.089956174 +0.107576064 +0.131151104 +0.156575925 +0.182100025 +0.209420833 +0.309509018 +0.360234401 +0.3293185 +0.296120536 +0.276438847 +0.271772957 +0.277326208 +0.282218842 +0.287624932 +0.285181312 +0.275436246 +0.258433625 +0.236905483 +0.158996836 +0.099651685 +0.076646177 +0.062875263 +0.061013028 +0.069556549 +0.084463352 +0.110304259 +0.146156209 +0.180635837 +0.22217247 +0.349389194 +0.412034234 +0.370294036 +0.335990137 +0.31740238 +0.308361549 +0.311312809 +0.331754949 +0.347144554 +0.350140129 +0.34391825 +0.322452945 +0.293436397 +0.180482217 +0.101537848 +0.078389893 +0.065642113 +0.06232099 +0.06926229 +0.083330057 +0.105214799 +0.128189206 +0.150112465 +0.177841658 +0.30003283 +0.376511627 +0.336063536 +0.285382833 +0.248329453 +0.23141899 +0.229197169 +0.235171745 +0.236414449 +0.239119077 +0.247561525 +0.255595116 +0.25654706 +0.189752717 +0.090116262 +0.066938124 +0.049232912 +0.042654849 +0.045790691 +0.056583981 +0.070831758 +0.08358742 +0.09584955 +0.116609963 +0.222408158 +0.273744704 +0.212077498 +0.156815612 +0.132440027 +0.125681294 +0.125878258 +0.134504658 +0.14433062 +0.155142138 +0.171428178 +0.192320119 +0.212303607 +0.167652809 +0.075030751 +0.046201786 +0.025518137 +0.01608412 +0.012897932 +0.014169476 +0.019107383 +0.028293733 +0.041570563 +0.064616923 +0.165580407 +0.24015659 +0.197851082 +0.14518942 +0.12177574 +0.116706107 +0.122834513 +0.137412287 +0.148347931 +0.155809385 +0.156562087 +0.147411696 +0.130457178 +0.078802661 +0.023320696 +0.00922726 +0.005479887 +0.005423689 +0.006887666 +0.00964683 +0.014795937 +0.022336001 +0.034054846 +0.061135889 +0.161554532 +0.194415197 +0.131638258 +0.092697081 +0.091259531 +0.125269509 +0.169314394 +0.189705055 +0.193281024 +0.187269744 +0.180128053 +0.173579242 +0.168244178 +0.110870449 +0.035927471 +0.017214114 +0.008467599 +0.007366435 +0.009447434 +0.013963093 +0.021948006 +0.033825678 +0.051472058 +0.086337975 +0.19341884 +0.226904565 +0.163265024 +0.113244196 +0.106894623 +0.127549227 +0.163674818 +0.190176828 +0.193242203 +0.18920606 +0.180569566 +0.168264253 +0.157119998 +0.108695414 +0.047108495 +0.041854014 +0.037029905 +0.03867119 +0.048821406 +0.070308458 +0.101263451 +0.144552731 +0.198900099 +0.264345946 +0.396255266 +0.433910368 +0.365834564 +0.300934923 +0.269489741 +0.257302181 +0.246566719 +0.228451122 +0.222201174 +0.230415382 +0.24586866 +0.264945314 +0.278721256 +0.22359776 +0.194985042 +0.205333944 +0.205142401 +0.215761951 +0.242638377 +0.273124794 +0.300311291 +0.323783141 +0.34570648 +0.37566599 +0.456097753 +0.47044967 +0.439320445 +0.416389726 +0.409863524 +0.410311707 +0.413638636 +0.415958334 +0.414073553 +0.413896264 +0.415826972 +0.417976621 +0.420628969 +0.319348676 +0.258254479 +0.219419272 +0.167699154 +0.136274691 +0.120865384 +0.11796312 +0.124390511 +0.139639114 +0.167003604 +0.217746839 +0.381809835 +0.457525121 +0.418417026 +0.376937286 +0.356222923 +0.343119758 +0.32830243 +0.315518918 +0.307770689 +0.313410507 +0.325235323 +0.339158381 +0.348207148 +0.243726453 +0.135686206 +0.093539096 +0.060221664 +0.044279051 +0.04054642 +0.045788773 +0.05688684 +0.067312035 +0.078188717 +0.104535305 +0.210948521 +0.208908784 +0.121825349 +0.068914895 +0.056488465 +0.074053841 +0.105870217 +0.129919473 +0.137995744 +0.133212209 +0.120068064 +0.107269319 +0.096994181 +0.06449948 +0.021518488 +0.010203605 +0.007417424 +0.007796416 +0.009179311 +0.01115683 +0.015544985 +0.024296547 +0.042828318 +0.090071456 +0.214379916 +0.246899715 +0.186067682 +0.147873367 +0.158043439 +0.185423824 +0.216426924 +0.225233852 +0.229154785 +0.233293499 +0.230397852 +0.2252037 +0.21920133 +0.1500533 +0.069241447 +0.043378001 +0.028531389 +0.018628243 +0.016025048 +0.017318712 +0.021498733 +0.030029314 +0.048684513 +0.095025674 +0.196780305 +0.223466953 +0.197725333 +0.185941771 +0.211800209 +0.247523208 +0.265511427 +0.275899862 +0.284715559 +0.287642333 +0.281562396 +0.273344605 +0.265322378 +0.18789843 +0.08872563 +0.056556879 +0.039236487 +0.027670089 +0.023299719 +0.022648439 +0.026189656 +0.036714687 +0.059943173 +0.111633787 +0.218388287 +0.24203982 +0.206902369 +0.187896241 +0.211817513 +0.23665054 +0.237183076 +0.227056823 +0.219015407 +0.213669602 +0.208406291 +0.196708467 +0.177746278 +0.114134981 +0.065256725 +0.051423885 +0.041277039 +0.032173736 +0.032258217 +0.039534382 +0.049952132 +0.062334407 +0.084408835 +0.134814838 +0.251949423 +0.315316317 +0.309078197 +0.297210257 +0.317748112 +0.329477288 +0.339485885 +0.356725086 +0.361954806 +0.367138995 +0.372694357 +0.363176331 +0.3435956 +0.235768809 +0.17960865 +0.15347296 +0.124365291 +0.103169135 +0.094836544 +0.093857745 +0.099231267 +0.108863301 +0.12561171 +0.162405527 +0.278245671 +0.370847919 +0.40038514 +0.416926188 +0.435506519 +0.441463378 +0.422924849 +0.400959877 +0.378490264 +0.351120816 +0.325921408 +0.303776189 +0.286380704 +0.221761149 +0.181230037 +0.11718628 +0.062391348 +0.036947134 +0.029773875 +0.032229184 +0.039065639 +0.048676835 +0.061936294 +0.09122281 +0.180447029 +0.279425241 +0.334905927 +0.357786755 +0.348056724 +0.318184752 +0.282686025 +0.248839788 +0.242456374 +0.242218164 +0.228025559 +0.206150302 +0.184584816 +0.10431975 +0.051085265 +0.044101406 +0.039147681 +0.038766211 +0.03962501 +0.041283684 +0.044543776 +0.050905744 +0.060587545 +0.077643136 +0.142455013 +0.160755648 +0.133603332 +0.114529002 +0.114791794 +0.135766015 +0.152259217 +0.161346742 +0.161159174 +0.155296283 +0.150185749 +0.148998042 +0.150605237 +0.096685952 +0.043138683 +0.025302886 +0.016729187 +0.016642193 +0.021106634 +0.02979872 +0.04720351 +0.073210416 +0.099602194 +0.12846909 +0.223260408 +0.243881961 +0.209126265 +0.177729177 +0.157173547 +0.138875346 +0.121069239 +0.111650016 +0.110229223 +0.113661867 +0.115437142 +0.11152105 +0.102258045 +0.091686781 +0.025517687 +0.022727766 +0.02961244 +0.039567722 +0.054826346 +0.074144052 +0.098667989 +0.128995056 +0.1623752 +0.203865299 +0.329413898 +0.333200916 +0.29274426 +0.272318215 +0.271387956 +0.264389005 +0.240542647 +0.224954661 +0.214063526 +0.208184286 +0.211120686 +0.214826587 +0.213073065 +0.179239586 +0.103413194 +0.08792461 +0.068580446 +0.051652593 +0.041047732 +0.039623425 +0.048670612 +0.06299599 +0.08041932 +0.118852906 +0.258140518 +0.279434698 +0.226108337 +0.202351208 +0.208219102 +0.230824669 +0.245796045 +0.256375973 +0.263156211 +0.261384472 +0.248616011 +0.230996331 +0.215460531 +0.156251995 +0.056259544 +0.030002825 +0.018684873 +0.01443994 +0.016913226 +0.024693728 +0.036925851 +0.051237571 +0.066585674 +0.095785894 +0.209099436 +0.237447493 +0.196469283 +0.160332353 +0.1467477 +0.15290117 +0.164242381 +0.166431875 +0.166083295 +0.160734482 +0.15247341 +0.142956408 +0.133705781 +0.099557182 +0.035278821 +0.016823622 +0.008354819 +0.006731307 +0.01068742 +0.018803633 +0.029369008 +0.039606459 +0.052184594 +0.079452598 +0.172994443 +0.168661936 +0.1162814 +0.082155137 +0.072377284 +0.083310301 +0.114553279 +0.156075627 +0.192990035 +0.221254888 +0.234747032 +0.23139905 +0.223633448 +0.175773778 +0.093123322 +0.080753712 +0.066426114 +0.058118406 +0.057141715 +0.064342743 +0.079003991 +0.102192259 +0.134916741 +0.192874049 +0.336251948 +0.366752347 +0.341622733 +0.319238906 +0.306616052 +0.297534269 +0.294301935 +0.29525999 +0.286302118 +0.270638588 +0.254524444 +0.240036896 +0.222531119 +0.15455368 +0.058231897 +0.048095223 +0.045142507 +0.045291149 +0.053347907 +0.066771514 +0.080026254 +0.09327167 +0.110829072 +0.14540971 +0.246167243 +0.23321628 +0.180946306 +0.15481618 +0.152129971 +0.151317981 +0.146830972 +0.145135022 +0.136275946 +0.12784708 +0.121935206 +0.111796101 +0.097752326 +0.086307418 +0.025652587 +0.015102107 +0.018135149 +0.022892986 +0.031689912 +0.046255657 +0.070290934 +0.101489445 +0.140577366 +0.20542075 +0.382081353 +0.429619654 +0.39006632 +0.371555806 +0.379795741 +0.378464877 +0.36514153 +0.350737114 +0.34160148 +0.338888395 +0.338420151 +0.337930376 +0.33454599 +0.282040417 +0.16574647 +0.151026048 +0.122413542 +0.110342056 +0.113075004 +0.130047552 +0.164638874 +0.204097569 +0.241475435 +0.28414005 +0.460191365 +0.525122965 +0.503474487 +0.453275085 +0.391383819 +0.334073439 +0.290368105 +0.265657942 +0.242614051 +0.220565806 +0.207991167 +0.195662145 +0.184379824 +0.171769503 +0.057364123 +0.065003975 +0.064468599 +0.056040928 +0.050772961 +0.049424767 +0.051642938 +0.060199885 +0.075961285 +0.11320308 +0.267399449 +0.346624783 +0.347990297 +0.338493663 +0.347391035 +0.377374276 +0.420357935 +0.445460278 +0.443432983 +0.4387782 +0.436843265 +0.436250456 +0.42801173 +0.388612647 +0.342901547 +0.311605967 +0.268266131 +0.23389965 +0.212972322 +0.205162826 +0.21012865 +0.218582028 +0.233480686 +0.266627955 +0.411416653 +0.484201313 +0.479572098 +0.468092558 +0.464008468 +0.466340489 +0.469000612 +0.467873878 +0.466833688 +0.459299378 +0.452929682 +0.44674543 +0.438939283 +0.35521042 +0.26614828 +0.24036662 +0.208889436 +0.194491568 +0.193667695 +0.207634482 +0.241813708 +0.289763866 +0.343199611 +0.409007715 +0.585887161 +0.644817043 +0.642163657 +0.632668608 +0.624437407 +0.622258862 +0.625743703 +0.617842282 +0.594222776 +0.571463942 +0.553255698 +0.539822427 +0.529840493 +0.455162916 +0.308969692 +0.331750257 +0.316714186 +0.298507734 +0.300277122 +0.321580011 +0.354146077 +0.386582156 +0.419978316 +0.471733246 +0.659089596 +0.711393405 +0.694701328 +0.674311897 +0.657494828 +0.64331717 +0.622419338 +0.58892845 +0.554626227 +0.519224603 +0.479259928 +0.444525268 +0.416772121 +0.350986567 +0.214458783 +0.166837715 +0.121409081 +0.100505939 +0.103172009 +0.129303883 +0.163393118 +0.201921477 +0.249487428 +0.325117785 +0.521081748 +0.58501163 +0.586472805 +0.57386957 +0.57155295 +0.553684771 +0.533086282 +0.528625712 +0.546145688 +0.574950659 +0.60141864 +0.593364455 +0.571367481 +0.506085617 +0.499924527 +0.51089255 +0.509480992 +0.500432623 +0.499823105 +0.515882781 +0.551088381 +0.586995185 +0.617264099 +0.653896495 +0.76903348 +0.79773008 +0.772483353 +0.734401041 +0.702406318 +0.672515823 +0.646229449 +0.634136711 +0.634608939 +0.631899917 +0.619940614 +0.600476264 +0.573126484 +0.462312119 +0.292818928 +0.278404321 +0.211359993 +0.163342063 +0.132105252 +0.116421191 +0.118290192 +0.131783548 +0.149529802 +0.178318065 +0.316772932 +0.346596398 +0.317036007 +0.277138657 +0.244668577 +0.230493358 +0.226384001 +0.220681917 +0.217988238 +0.216085117 +0.213681133 +0.208066705 +0.199769008 +0.190536443 +0.061107185 +0.030369911 +0.026446773 +0.028078743 +0.036315643 +0.048845351 +0.066417286 +0.087251025 +0.107506445 +0.138722716 +0.271756546 +0.291598651 +0.248489154 +0.197709961 +0.161329523 +0.135128318 +0.119751697 +0.113979234 +0.121093934 +0.135217905 +0.144144675 +0.142668494 +0.13327053 +0.125797494 +0.035600966 +0.005250511 +0.003307637 +0.005261896 +0.010696941 +0.01851005 +0.028562925 +0.038730764 +0.047703146 +0.06612868 +0.16151748 +0.161806506 +0.108292475 +0.067351485 +0.050418942 +0.046521779 +0.049814223 +0.057703028 +0.069368496 +0.080235787 +0.086170466 +0.090581709 +0.096144022 +0.107405177 +0.044055285 +0.016702341 +0.013276611 +0.01437833 +0.020094636 +0.027623446 +0.033386568 +0.03742645 +0.041783167 +0.066239741 +0.182562773 +0.227554243 +0.234162035 +0.256249133 +0.297309458 +0.341537997 +0.374713484 +0.394334864 +0.413263227 +0.429488551 +0.440397409 +0.446072984 +0.446758515 +0.422716024 +0.212298067 +0.19226747 +0.201389256 +0.19583173 +0.19541038 +0.204299994 +0.222157504 +0.242041014 +0.262548452 +0.318805037 +0.480725335 +0.514673757 +0.514653446 +0.534244946 +0.584855982 +0.600401183 +0.587089155 +0.567889061 +0.551012338 +0.529619014 +0.509120129 +0.490535704 +0.475646536 +0.410855339 +0.270100883 +0.316579847 +0.300009839 +0.288384415 +0.299533672 +0.323251551 +0.347957966 +0.365394448 +0.374621667 +0.398006603 +0.54733779 +0.566639125 +0.543477571 +0.522666243 +0.504404682 +0.48682249 +0.467857417 +0.438892399 +0.407366644 +0.371193248 +0.325636239 +0.27763935 +0.2257444 +0.174680242 +0.046858353 +0.01745591 +0.016156174 +0.024608119 +0.034688401 +0.04174936 +0.041339486 +0.036736889 +0.034596895 +0.047676364 +0.168635824 +0.257627695 +0.264189081 +0.240283242 +0.215517974 +0.195516453 +0.181528658 +0.16812674 +0.151192299 +0.135103614 +0.121199598 +0.111622249 +0.104751312 +0.093333017 +0.028454669 +0.010919505 +0.012678521 +0.020278454 +0.028761885 +0.03668453 +0.04230027 +0.044573152 +0.049407656 +0.072503273 +0.228257059 +0.331081034 +0.366184343 +0.369600469 +0.358123001 +0.367727369 +0.39006265 +0.406870469 +0.408458502 +0.395630177 +0.383591381 +0.36950027 +0.354190102 +0.323447446 +0.160086361 +0.19896628 +0.240798667 +0.259246666 +0.271739047 +0.278264266 +0.271950651 +0.257393215 +0.251455307 +0.30081882 +0.551175209 +0.652081556 +0.673539705 +0.661275038 +0.63695921 +0.607234786 +0.571700714 +0.534154209 +0.492098401 +0.459436788 +0.42614151 +0.385362507 +0.345472155 +0.284567649 +0.100241977 +0.086806706 +0.067464593 +0.052854278 +0.044804849 +0.038766977 +0.035287933 +0.038185596 +0.048203301 +0.082106649 +0.220645369 +0.30810016 +0.31894774 +0.297214412 +0.266821547 +0.249705375 +0.236326412 +0.21999482 +0.210203119 +0.210558866 +0.220125155 +0.234089716 +0.244044104 +0.222752089 +0.082635086 +0.039518469 +0.019059151 +0.009396743 +0.006950686 +0.009346047 +0.01408303 +0.01872422 +0.023471288 +0.043379937 +0.142167175 +0.175619185 +0.152395336 +0.121523582 +0.103796565 +0.100112823 +0.11094929 +0.134738439 +0.153692615 +0.15814206 +0.149177998 +0.131345395 +0.114294219 +0.108114768 +0.04730233 +0.020970143 +0.021859359 +0.022773109 +0.024386106 +0.02891063 +0.033461253 +0.038675176 +0.052102575 +0.105829304 +0.290465317 +0.366549241 +0.379368693 +0.381152405 +0.397342876 +0.421618992 +0.452855655 +0.464788318 +0.429258975 +0.377340022 +0.336082873 +0.306678261 +0.286646318 +0.263490205 +0.105934359 +0.078530441 +0.0586725 +0.037560187 +0.027722118 +0.023914221 +0.023570699 +0.026125693 +0.032283349 +0.061596533 +0.182948376 +0.268256946 +0.303990597 +0.308893288 +0.309784051 +0.315293172 +0.328298518 +0.333069292 +0.326180388 +0.311934867 +0.295695904 +0.275569666 +0.254252673 +0.231022004 +0.105177953 +0.081225943 +0.075296793 +0.065854265 +0.060336705 +0.058801449 +0.063359251 +0.072705203 +0.086688074 +0.133583711 +0.275708195 +0.342074318 +0.345471756 +0.316738287 +0.300176021 +0.30335406 +0.313749705 +0.331611555 +0.349424855 +0.344011599 +0.336267534 +0.336463267 +0.335279093 +0.319771162 +0.242331516 +0.189865792 +0.146357533 +0.121811787 +0.111066017 +0.107334548 +0.113960974 +0.128900483 +0.15101826 +0.216129381 +0.405746703 +0.480438492 +0.4849656 +0.462938695 +0.452689715 +0.450204619 +0.442760707 +0.437228395 +0.437913912 +0.447421283 +0.453241623 +0.444193008 +0.432444778 +0.395468799 +0.288357627 +0.251273705 +0.193958941 +0.146484532 +0.125137728 +0.113774202 +0.121048945 +0.14215655 +0.178325024 +0.275485412 +0.505325043 +0.565888159 +0.556285011 +0.529521114 +0.510695865 +0.500595483 +0.478469982 +0.449764286 +0.43514402 +0.432099259 +0.43331913 +0.437778413 +0.435406252 +0.393544996 +0.308198887 +0.26734684 +0.211323609 +0.169561029 +0.140363767 +0.12574827 +0.128113993 +0.147314384 +0.18450885 +0.274096731 +0.463056329 +0.493599867 +0.476343945 +0.442787255 +0.400745065 +0.373399197 +0.351296081 +0.326965232 +0.327638582 +0.338518894 +0.346334212 +0.356345544 +0.36112971 +0.331897739 +0.188645648 +0.133829789 +0.083241236 +0.056843757 +0.03933749 +0.035291928 +0.038140541 +0.041852328 +0.048450177 +0.085026267 +0.192984538 +0.19905986 +0.168477319 +0.152419378 +0.152467742 +0.160597842 +0.167658225 +0.16307121 +0.141196111 +0.118611174 +0.105981078 +0.101525709 +0.102965889 +0.10926451 +0.055736266 +0.026834598 +0.040184433 +0.051194625 +0.05941181 +0.065953856 +0.071871442 +0.079522276 +0.095735368 +0.149216603 +0.293951819 +0.364467251 +0.404294712 +0.443400863 +0.48445169 +0.515944324 +0.528096436 +0.521769811 +0.490288064 +0.443686272 +0.396735806 +0.363090269 +0.35253054 +0.335745174 +0.184115057 +0.280185931 +0.319953051 +0.329013912 +0.337530717 +0.355466534 +0.376597155 +0.391051202 +0.40135873 +0.434621861 +0.575367058 +0.616489509 +0.632123761 +0.619011451 +0.600482328 +0.5863436 +0.56009394 +0.525086939 +0.483817797 +0.436364649 +0.396470009 +0.373888257 +0.361634968 +0.326376089 +0.192182055 +0.228330146 +0.224830706 +0.201714937 +0.202823986 +0.219910496 +0.244079079 +0.262574424 +0.272544949 +0.309796843 +0.398697699 +0.40512531 +0.402356498 +0.395364494 +0.396405396 +0.404282709 +0.411239443 +0.403266491 +0.384843391 +0.360188353 +0.335474584 +0.314289595 +0.295438693 +0.25009706 +0.104922174 +0.066659991 +0.035685558 +0.029068835 +0.038486625 +0.057597303 +0.084837915 +0.115109143 +0.144639473 +0.188739205 +0.278965566 +0.284375837 +0.277763234 +0.275383001 +0.324841507 +0.39295356 +0.449209647 +0.470876527 +0.463923403 +0.446636178 +0.435248956 +0.434621076 +0.438466006 +0.421292884 +0.226244218 +0.214383108 +0.32926286 +0.358785462 +0.358728519 +0.360449636 +0.377626506 +0.391277865 +0.397476095 +0.45657563 +0.549428079 +0.547576781 +0.53795954 +0.546639838 +0.577917505 +0.598712107 +0.592769301 +0.559671071 +0.522715701 +0.492797742 +0.472312719 +0.473819398 +0.49120101 +0.480921025 +0.315803671 +0.50952443 +0.600693743 +0.569066284 +0.547469905 +0.538906123 +0.549189987 +0.563237306 +0.573808138 +0.601979003 +0.647580752 +0.636636339 +0.625988506 +0.614937282 +0.595262937 +0.585375513 +0.574336265 +0.555547081 +0.558781932 +0.574733689 +0.586281945 +0.589880653 +0.581979473 +0.551194343 +0.532881985 +0.539775437 +0.488758733 +0.427437912 +0.385799732 +0.370743641 +0.37286395 +0.384545959 +0.413851395 +0.495638827 +0.65732551 +0.732878641 +0.762582162 +0.760400858 +0.747106888 +0.726714731 +0.710386664 +0.698190471 +0.702869374 +0.720032046 +0.721529566 +0.708777325 +0.699478482 +0.656940652 +0.481517443 +0.565093648 +0.553200304 +0.521258012 +0.500854677 +0.489870459 +0.481505846 +0.480666628 +0.493380065 +0.558440313 +0.688615184 +0.703308219 +0.701641287 +0.687416539 +0.665021201 +0.638491667 +0.613889458 +0.580112018 +0.549603521 +0.517233789 +0.487364734 +0.472020345 +0.453154306 +0.418772284 +0.347624727 +0.316420821 +0.255371496 +0.18574535 +0.136837688 +0.106288119 +0.097144481 +0.097292219 +0.102445396 +0.138136754 +0.240016943 +0.26215132 +0.2668242 +0.286382997 +0.326347665 +0.367810992 +0.398958572 +0.404940529 +0.42029566 +0.448037434 +0.468767004 +0.48016987 +0.481503683 +0.449943892 +0.261820168 +0.192236846 +0.132402046 +0.083911504 +0.051708398 +0.035254608 +0.032580304 +0.037874768 +0.050763175 +0.109570075 +0.281280849 +0.346530111 +0.375441544 +0.402630191 +0.422984651 +0.431573217 +0.434284242 +0.436937883 +0.443536675 +0.448199903 +0.443306375 +0.433668068 +0.418293794 +0.383722727 +0.188268315 +0.099007042 +0.055427626 +0.027690166 +0.014643697 +0.011856301 +0.01229654 +0.016282337 +0.028827627 +0.104635167 +0.254775773 +0.303524163 +0.305231162 +0.308908838 +0.323433245 +0.332294227 +0.328083265 +0.316037111 +0.311493114 +0.31629792 +0.323924813 +0.327099456 +0.313412808 +0.289746808 +0.142595207 +0.068037298 +0.033571531 +0.021759298 +0.023889117 +0.030294508 +0.033122849 +0.032641998 +0.033160163 +0.073129522 +0.162748084 +0.160078869 +0.131126496 +0.121407401 +0.130524972 +0.150077235 +0.17629983 +0.2048491 +0.236899373 +0.268849411 +0.290774902 +0.300830319 +0.304706645 +0.296249897 +0.151051599 +0.06656589 +0.03705351 +0.025563327 +0.026069275 +0.03283495 +0.040506185 +0.049575658 +0.067066208 +0.178522206 +0.405407346 +0.483651682 +0.515187493 +0.522680928 +0.519054615 +0.51699158 +0.504537682 +0.487907279 +0.480224651 +0.468671205 +0.450865238 +0.436502622 +0.425107613 +0.410476685 +0.216986657 +0.130251918 +0.092339089 +0.072016796 +0.070322525 +0.080864283 +0.099360712 +0.118602346 +0.145873182 +0.259732173 +0.468305776 +0.536739698 +0.55958721 +0.571631443 +0.579037846 +0.584286193 +0.571928669 +0.544581627 +0.536637575 +0.536147544 +0.526923874 +0.514901195 +0.512844758 +0.505760072 +0.305439176 +0.231879819 +0.214724891 +0.206955242 +0.213942959 +0.227340878 +0.247677686 +0.268936244 +0.291835478 +0.380467814 +0.514459457 +0.548740496 +0.546104503 +0.530495999 +0.515221162 +0.501917749 +0.489297827 +0.476866423 +0.47241524 +0.474659209 +0.475571233 +0.473332207 +0.462828497 +0.444734411 +0.290979412 +0.194247337 +0.16990439 +0.140749009 +0.123877041 +0.115774922 +0.109125796 +0.107370387 +0.114210943 +0.195799039 +0.287670856 +0.29596129 +0.289699511 +0.289689688 +0.2919194 +0.285601085 +0.271153726 +0.254692116 +0.243020787 +0.239834742 +0.233350611 +0.21088894 +0.179725362 +0.163342085 +0.105055112 +0.040048077 +0.016721849 +0.005825543 +0.003030779 +0.004030063 +0.00604126 +0.008913207 +0.016763879 +0.079811869 +0.123962901 +0.099827853 +0.066678218 +0.052331396 +0.058140716 +0.07265054 +0.079784198 +0.074039953 +0.061478782 +0.054624799 +0.056183526 +0.060983418 +0.066086741 +0.071306988 +0.051562297 +0.00826556 +0.003662431 +0.008957828 +0.014309088 +0.019404279 +0.022759841 +0.023960804 +0.02873317 +0.090198988 +0.139743404 +0.133179112 +0.105961912 +0.082109786 +0.072505488 +0.076895828 +0.084898792 +0.092063003 +0.094790955 +0.093792311 +0.091480894 +0.087955617 +0.083061307 +0.077622804 +0.063195904 +0.012562628 +0.014842484 +0.020606525 +0.028727725 +0.03737225 +0.04248864 +0.043568665 +0.04750549 +0.109773669 +0.189624638 +0.191237077 +0.183218077 +0.192404912 +0.213934364 +0.240083109 +0.270140884 +0.296345047 +0.316669761 +0.331501336 +0.339656506 +0.341907947 +0.341257836 +0.344881425 +0.215825269 +0.133296015 +0.117399276 +0.108664441 +0.110038568 +0.119853545 +0.131261733 +0.142563435 +0.160678494 +0.262589333 +0.434791071 +0.47693066 +0.502407884 +0.529063271 +0.554664497 +0.58301148 +0.603435954 +0.607091275 +0.598630994 +0.582270052 +0.576450714 +0.57650207 +0.565614335 +0.540504675 +0.317390372 +0.246444414 +0.189573153 +0.132972351 +0.103599408 +0.091327607 +0.100986712 +0.127247184 +0.165560973 +0.286374937 +0.482655226 +0.534844979 +0.545203741 +0.546537322 +0.551784134 +0.573368763 +0.588503144 +0.587428173 +0.582715882 +0.580339023 +0.568801091 +0.555196685 +0.541457847 +0.521124342 +0.307227748 +0.259941983 +0.238594097 +0.20880656 +0.189960098 +0.176173747 +0.173494895 +0.176883312 +0.182105908 +0.242138273 +0.320044568 +0.312501546 +0.286236741 +0.268457824 +0.269476295 +0.279734167 +0.289217929 +0.291898436 +0.29727719 +0.29903368 +0.292507346 +0.285220108 +0.276988322 +0.268628923 +0.161327641 +0.050283086 +0.026729658 +0.014789428 +0.009261639 +0.007196884 +0.008540927 +0.013522939 +0.026932882 +0.102779916 +0.171123961 +0.197415254 +0.226256294 +0.285291578 +0.353752598 +0.395988623 +0.420961401 +0.425710258 +0.417319875 +0.396634133 +0.370999987 +0.356167905 +0.35272423 +0.351682618 +0.232107996 +0.131160774 +0.154122065 +0.139956454 +0.13216032 +0.13093921 +0.136594415 +0.140581659 +0.13932545 +0.186288731 +0.258416861 +0.276139989 +0.264407703 +0.249363586 +0.249275552 +0.249035408 +0.241401072 +0.232223984 +0.218758321 +0.200612651 +0.180103652 +0.161304088 +0.146967032 +0.139665684 +0.081561998 +0.029225284 +0.016385406 +0.013360476 +0.017183818 +0.026067123 +0.035397168 +0.039662197 +0.043080294 +0.097651929 +0.159214425 +0.151160298 +0.124843346 +0.107768685 +0.095850277 +0.091843174 +0.093149009 +0.094693904 +0.09415403 +0.095090685 +0.093995402 +0.086376372 +0.074888488 +0.072833338 +0.04813036 +0.010265771 +0.018837678 +0.050657565 +0.105957268 +0.174464658 +0.237480748 +0.273981767 +0.284690996 +0.333982588 +0.360477451 +0.322137338 +0.277901474 +0.250017656 +0.245596148 +0.251138158 +0.26401222 +0.288777248 +0.329402333 +0.361505154 +0.373539599 +0.380480218 +0.390353854 +0.394473887 +0.249375217 +0.202566448 +0.116759918 +0.075622972 +0.062832733 +0.062034957 +0.059125318 +0.053213186 +0.052196872 +0.111739414 +0.262127724 +0.322215582 +0.353478842 +0.376787707 +0.380484123 +0.375243555 +0.373053975 +0.367423234 +0.361633408 +0.353739397 +0.345706168 +0.337259037 +0.328937015 +0.310800901 +0.168397642 +0.077953887 +0.03806993 +0.024689343 +0.020995316 +0.021891771 +0.025408239 +0.03236104 +0.043895944 +0.115899042 +0.224689069 +0.237367037 +0.230468725 +0.225123195 +0.234614165 +0.242835078 +0.240879341 +0.22769092 +0.210194148 +0.187376596 +0.161914632 +0.141436381 +0.126877561 +0.115564323 +0.072051411 +0.020161234 +0.017848837 +0.033495069 +0.05621788 +0.080848431 +0.107818037 +0.130548227 +0.138835132 +0.191728273 +0.213420214 +0.179350265 +0.145307967 +0.130013643 +0.139866501 +0.154711598 +0.159455308 +0.163702362 +0.158984393 +0.151300364 +0.146857333 +0.139698717 +0.130616826 +0.122882169 +0.069531435 +0.032726373 +0.039827848 +0.051497543 +0.064405569 +0.072601176 +0.079075393 +0.083297875 +0.087163099 +0.154675987 +0.231282885 +0.239719812 +0.226917719 +0.21930865 +0.214605286 +0.212684879 +0.215018595 +0.223497783 +0.231726536 +0.23709993 +0.239900882 +0.240799858 +0.236283503 +0.228632675 +0.122353203 +0.045496065 +0.03966065 +0.040399441 +0.042433764 +0.043677847 +0.047218117 +0.052505619 +0.059785965 +0.122037238 +0.182314319 +0.190036545 +0.181051797 +0.170617584 +0.172175977 +0.187523845 +0.197662474 +0.202280822 +0.197547198 +0.192357739 +0.192477091 +0.196564366 +0.202334899 +0.210177177 +0.154246653 +0.069619562 +0.111595512 +0.142676727 +0.177243633 +0.214802848 +0.242824792 +0.255238995 +0.258264323 +0.315120243 +0.365043056 +0.368311361 +0.370112679 +0.361151976 +0.354547388 +0.359128934 +0.372228922 +0.393911902 +0.412078372 +0.42314917 +0.436256587 +0.441805627 +0.431179369 +0.408646001 +0.27702816 +0.177532146 +0.153396857 +0.114448362 +0.092094904 +0.084488944 +0.086478451 +0.098272663 +0.125054001 +0.26550045 +0.409650181 +0.441731295 +0.447793384 +0.438179554 +0.411036398 +0.387439738 +0.375094629 +0.363342843 +0.343982582 +0.328946242 +0.328512555 +0.335761154 +0.336506028 +0.334125605 +0.248638321 +0.116937821 +0.101785039 +0.095183103 +0.106779003 +0.132489378 +0.166573121 +0.220850779 +0.312885599 +0.532190385 +0.696152993 +0.745503618 +0.782307119 +0.784955968 +0.759111296 +0.734752285 +0.701977834 +0.663243652 +0.629229172 +0.597382146 +0.569017255 +0.542256932 +0.518756087 +0.496040206 +0.368671548 +0.30173267 +0.241029107 +0.179995375 +0.127795485 +0.094616063 +0.079223233 +0.074892945 +0.083533547 +0.173490117 +0.294912917 +0.317544798 +0.319943421 +0.3234739 +0.321753414 +0.320735698 +0.313928371 +0.299258249 +0.286787761 +0.274295074 +0.261226015 +0.247393529 +0.235143063 +0.222091089 +0.139068309 +0.057901965 +0.028864835 +0.016822746 +0.013822643 +0.015399311 +0.020346488 +0.028968771 +0.044714798 +0.148503525 +0.241845201 +0.242476828 +0.236442271 +0.250724553 +0.279338501 +0.313576146 +0.343638789 +0.369214964 +0.388965528 +0.411427918 +0.438579495 +0.460447768 +0.47564347 +0.490083102 +0.36228411 +0.298002034 +0.320842648 +0.329887776 +0.340932451 +0.362767604 +0.404784429 +0.466985306 +0.548645118 +0.689611777 +0.776463702 +0.810612439 +0.831429731 +0.837034224 +0.826567351 +0.803715467 +0.776037805 +0.747514829 +0.735673098 +0.737736634 +0.736180039 +0.730222664 +0.721025649 +0.703642026 +0.601315963 +0.600806563 +0.613184782 +0.586478758 +0.563699147 +0.546012453 +0.534019674 +0.527836416 +0.532767351 +0.60197791 +0.680023008 +0.70482725 +0.720092473 +0.728240813 +0.725458029 +0.720058233 +0.712618997 +0.699276469 +0.688205053 +0.670903451 +0.645092239 +0.613574923 +0.580038356 +0.545149025 +0.42777349 +0.345183587 +0.355748444 +0.354277055 +0.359825856 +0.366238677 +0.374846498 +0.386405598 +0.400993249 +0.469709808 +0.52409231 +0.527641479 +0.540346987 +0.554228049 +0.562705352 +0.570386216 +0.571967269 +0.569207399 +0.563296718 +0.551931816 +0.531940472 +0.501371242 +0.469347073 +0.439245331 +0.323269135 +0.197356926 +0.186801931 +0.169522526 +0.163490583 +0.16659152 +0.172365363 +0.177259564 +0.187295118 +0.266476071 +0.311264011 +0.293143964 +0.277474033 +0.271792264 +0.276146891 +0.287171098 +0.291237846 +0.27966999 +0.268618428 +0.267478637 +0.269312129 +0.266118992 +0.255646782 +0.24277117 +0.17199644 +0.064182699 +0.034791609 +0.016272939 +0.010521297 +0.011958522 +0.014385164 +0.016793813 +0.024606211 +0.100428597 +0.138267814 +0.105162846 +0.080758828 +0.075597695 +0.076443967 +0.076420637 +0.073749756 +0.072403387 +0.075315719 +0.081857171 +0.094975128 +0.121518847 +0.161830078 +0.213344663 +0.2131347 +0.112282373 +0.098578042 +0.102972465 +0.115656793 +0.131426552 +0.153013109 +0.184795333 +0.237414615 +0.383417455 +0.519423633 +0.553318248 +0.574040301 +0.587874728 +0.59308363 +0.588738513 +0.578444037 +0.568963088 +0.564166196 +0.563138182 +0.559099114 +0.550927606 +0.535978733 +0.51605114 +0.400850468 +0.368565903 +0.361514285 +0.348615697 +0.343507094 +0.359658772 +0.38415304 +0.411138719 +0.439600769 +0.505095341 +0.568839459 +0.574713233 +0.579520486 +0.596752741 +0.619535427 +0.638407539 +0.650876555 +0.646507226 +0.637949857 +0.632129321 +0.621506117 +0.599878066 +0.567680737 +0.529409682 +0.391907324 +0.267966529 +0.292526197 +0.297566734 +0.325380742 +0.365090922 +0.398612954 +0.413462597 +0.411984278 +0.460168488 +0.497154871 +0.503486091 +0.515147591 +0.519411266 +0.515612183 +0.512144325 +0.50827198 +0.499382745 +0.482212072 +0.451560104 +0.403255342 +0.350386806 +0.305044729 +0.269975636 +0.218159503 +0.092527897 +0.062668034 +0.04619032 +0.043597247 +0.053499682 +0.072823109 +0.096956081 +0.127994189 +0.22543143 +0.258918023 +0.248568796 +0.247500415 +0.255883795 +0.264968333 +0.268480084 +0.266826853 +0.26664711 +0.270526632 +0.275349906 +0.276170422 +0.269618203 +0.25160902 +0.228138869 +0.181894753 +0.056067115 +0.027254056 +0.016544879 +0.011667873 +0.012914632 +0.016858483 +0.021237572 +0.038946465 +0.103628595 +0.089594351 +0.049506833 +0.028666721 +0.025800528 +0.030870106 +0.038194925 +0.046148053 +0.057148492 +0.068937313 +0.078841749 +0.088580598 +0.09624241 +0.100780643 +0.100111107 +0.099300213 +0.025891106 +0.008046497 +0.006222382 +0.005811331 +0.006733674 +0.009656944 +0.014919214 +0.031314712 +0.108623277 +0.126870076 +0.093948963 +0.080429034 +0.093184717 +0.128916513 +0.169794729 +0.196756257 +0.207149553 +0.199167115 +0.184484888 +0.171483039 +0.159977857 +0.154095782 +0.15698377 +0.147609507 +0.053010916 +0.039645897 +0.047767051 +0.065574839 +0.087922699 +0.101535514 +0.104171022 +0.113830483 +0.199362148 +0.235540762 +0.198085382 +0.172884347 +0.169557413 +0.17860568 +0.194174549 +0.217797857 +0.252203377 +0.293582182 +0.3372576 +0.359743251 +0.362121002 +0.358763906 +0.355922964 +0.244292555 +0.141368139 +0.119144728 +0.088848509 +0.061389883 +0.04605964 +0.046589576 +0.05878252 +0.082836376 +0.181653587 +0.237778682 +0.212988968 +0.184381293 +0.175307491 +0.174047135 +0.172386093 +0.170829424 +0.165813287 +0.170698003 +0.187313499 +0.207290559 +0.220006326 +0.220807085 +0.212738717 +0.145147123 +0.039009798 +0.015139343 +0.007942954 +0.006679769 +0.009033012 +0.013265041 +0.017888441 +0.025274651 +0.086618326 +0.112939533 +0.08863618 +0.062963146 +0.055831543 +0.062916513 +0.070753086 +0.077725939 +0.085479467 +0.094579735 +0.106762332 +0.12302547 +0.138611732 +0.153578556 +0.175058517 +0.180867666 +0.074630816 +0.106373785 +0.146455225 +0.186606725 +0.224917385 +0.262846461 +0.293031233 +0.310956579 +0.395506731 +0.454332974 +0.465054124 +0.485800054 +0.5212467 +0.549182667 +0.556887292 +0.547681827 +0.528758803 +0.499764336 +0.463155041 +0.428959676 +0.406927449 +0.389994118 +0.374543238 +0.276468067 +0.289246952 +0.341920266 +0.364478489 +0.39776454 +0.445398119 +0.478325039 +0.47297595 +0.426796025 +0.452154875 +0.456718472 +0.387122973 +0.335959387 +0.286721158 +0.23289955 +0.193534688 +0.171926169 +0.167774267 +0.179180455 +0.200368742 +0.22172076 +0.238964851 +0.247246449 +0.244508769 +0.122555766 +0.055303227 +0.023047514 +0.006562033 +0.00477441 +0.008081218 +0.011462537 +0.013684312 +0.018756531 +0.07071961 +0.157317759 +0.186327904 +0.20563281 +0.259488752 +0.353079188 +0.436748265 +0.491130588 +0.517859955 +0.526842883 +0.529105477 +0.540329424 +0.550022041 +0.563119251 +0.57524359 +0.432800626 +0.414785972 +0.552247941 +0.546284614 +0.510853249 +0.480474392 +0.451972782 +0.416168218 +0.362275435 +0.374028959 +0.413141385 +0.398081132 +0.385966675 +0.36690286 +0.34266162 +0.317132108 +0.289334603 +0.273363711 +0.268249783 +0.258214951 +0.245927096 +0.240349823 +0.23382862 +0.225392349 +0.151401509 +0.082431147 +0.085826913 +0.109633762 +0.151901348 +0.212016322 +0.278090276 +0.347565289 +0.419211514 +0.571237972 +0.712959336 +0.759011011 +0.775745993 +0.783463775 +0.778819452 +0.763084652 +0.738147076 +0.697429009 +0.655032534 +0.617411599 +0.584483526 +0.558875598 +0.537893972 +0.519845857 +0.358766006 +0.262573516 +0.244576892 +0.229222121 +0.213614662 +0.209549909 +0.212313851 +0.214076685 +0.226398929 +0.338563184 +0.459984561 +0.488806159 +0.487996624 +0.484171079 +0.480355942 +0.472924018 +0.464380728 +0.455762363 +0.452385694 +0.45079121 +0.438857098 +0.412820111 +0.382383688 +0.360898687 +0.26677038 +0.144606521 +0.125718822 +0.103348697 +0.087225874 +0.080808742 +0.084348675 +0.098321965 +0.126023173 +0.21853189 +0.277579971 +0.2710324 +0.258999878 +0.2587627 +0.259099068 +0.254893896 +0.255198262 +0.2609865 +0.267136642 +0.270162548 +0.268101026 +0.259351606 +0.24430341 +0.226032034 +0.166550348 +0.067330782 +0.044625837 +0.028469971 +0.021650979 +0.022797084 +0.025155106 +0.02487681 +0.031662361 +0.104575613 +0.141521747 +0.120347198 +0.10212769 +0.095156346 +0.093078983 +0.08658934 +0.073714412 +0.058170274 +0.04677956 +0.041964724 +0.043013597 +0.048706825 +0.057257086 +0.065770154 +0.063971349 +0.014136346 +0.001716531 +0.000215785 +0.001727342 +0.006402757 +0.013890989 +0.024423889 +0.043053589 +0.14791011 +0.201650856 +0.18488867 +0.166686977 +0.163750995 +0.177164903 +0.204474479 +0.240689871 +0.277938803 +0.320126052 +0.358022043 +0.384165686 +0.396283084 +0.391298301 +0.374289857 +0.256519139 +0.120740524 +0.109739172 +0.094087885 +0.081754785 +0.07722504 +0.086184503 +0.107972158 +0.13945086 +0.255472076 +0.353815347 +0.339901616 +0.311426926 +0.291278196 +0.286117929 +0.293790518 +0.305649234 +0.312347229 +0.314390076 +0.315265282 +0.310188398 +0.300529859 +0.29020164 +0.282048662 +0.180969364 +0.0759849 +0.058221467 +0.045114741 +0.050620264 +0.064786223 +0.082484788 +0.098577755 +0.122067357 +0.246565154 +0.370536039 +0.388619902 +0.384424826 +0.374532497 +0.373695158 +0.379053779 +0.386742864 +0.387996948 +0.392167651 +0.399093061 +0.402415018 +0.300529859 +0.29020164 +0.282048662 +0.180969364 +0.0759849 +0.058221467 +0.045114741 +0.050620264 +0.064786223 +0.082484788 +0.098577755 +0.122067357 +0.246565154 +0.370536039 +0.388619902 +0.384424826 +0.374532497 +0.373695158 +0.379053779 +0.386742864 +0.387996948 +0.392167651 +0.399093061 +0.402415018 \ No newline at end of file diff --git a/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/capa/windcapaUPS.txt b/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/capa/windcapaUPS.txt new file mode 100644 index 0000000..b005c8c --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/capa/windcapaUPS.txt @@ -0,0 +1,8760 @@ +0.08181809 +0.058233331 +0.052305726 +0.046172336 +0.043205135 +0.048982037 +0.059325475 +0.090484773 +0.159088781 +0.189057852 +0.186944647 +0.199901022 +0.211498589 +0.22734688 +0.247191723 +0.269568201 +0.306789998 +0.330640614 +0.380791528 +0.409069173 +0.417066678 +0.427882227 +0.420931347 +0.40170015 +0.382530284 +0.335697823 +0.293091457 +0.293941816 +0.335972443 +0.359861918 +0.348602795 +0.361764131 +0.396537397 +0.402829201 +0.401142779 +0.391388879 +0.379302157 +0.358553864 +0.338023415 +0.3130777 +0.295594729 +0.266583816 +0.229263509 +0.202317789 +0.183133828 +0.157887788 +0.130898623 +0.102995798 +0.082476736 +0.064171119 +0.046551869 +0.032181348 +0.02865243 +0.025736956 +0.029466278 +0.046785949 +0.067460005 +0.090469242 +0.099490824 +0.097182424 +0.090135522 +0.076929328 +0.065640127 +0.061933781 +0.063187025 +0.064487711 +0.067705589 +0.073009018 +0.074510035 +0.074250868 +0.068823493 +0.062823195 +0.057274346 +0.048684416 +0.035987759 +0.023247622 +0.019778216 +0.020440149 +0.022102449 +0.02749157 +0.035831074 +0.046013381 +0.059035246 +0.073624849 +0.089518663 +0.110389092 +0.122318467 +0.131191374 +0.137415682 +0.147731127 +0.150479444 +0.152616006 +0.151087793 +0.140379202 +0.132595602 +0.132980191 +0.130758707 +0.116311906 +0.093572241 +0.088116142 +0.09049291 +0.088293886 +0.0808561 +0.081733568 +0.098711945 +0.110803834 +0.11786709 +0.134015109 +0.157047473 +0.183872944 +0.215076557 +0.248454827 +0.256799692 +0.259038977 +0.266926388 +0.26291907 +0.270745055 +0.286565841 +0.315192389 +0.343594846 +0.371905059 +0.370286817 +0.353918617 +0.364011136 +0.385506681 +0.405082261 +0.422064189 +0.455339582 +0.507337023 +0.541159649 +0.563919991 +0.573887001 +0.585450686 +0.588879406 +0.577506113 +0.555524722 +0.533421815 +0.509764575 +0.489060261 +0.471531691 +0.455674851 +0.455848628 +0.450743771 +0.434422995 +0.412413242 +0.370399371 +0.384311886 +0.347176765 +0.313786852 +0.324963867 +0.345880466 +0.331017735 +0.365657826 +0.396774099 +0.422402499 +0.431884379 +0.440979167 +0.450445216 +0.461119715 +0.425686977 +0.398194174 +0.378803457 +0.358974637 +0.335690086 +0.314586179 +0.317131368 +0.32561003 +0.323541805 +0.319562411 +0.282113473 +0.322455303 +0.316527948 +0.315537192 +0.335077052 +0.333738723 +0.330753849 +0.362334046 +0.375645244 +0.395404899 +0.421659883 +0.4465191 +0.488676257 +0.517448765 +0.506974918 +0.481696434 +0.460583663 +0.444618712 +0.433194582 +0.424907137 +0.430469806 +0.439028994 +0.443128867 +0.425637486 +0.362921838 +0.354124837 +0.406513884 +0.451763733 +0.471208795 +0.480504246 +0.489048467 +0.52629503 +0.526979572 +0.517081124 +0.507034984 +0.486273879 +0.473422958 +0.454423092 +0.440402854 +0.426897098 +0.419681132 +0.423672866 +0.416233347 +0.416068951 +0.425278749 +0.428488426 +0.417966387 +0.405103894 +0.349107628 +0.304978699 +0.348261625 +0.387180537 +0.398875009 +0.390449529 +0.358178244 +0.355460032 +0.342074336 +0.323995039 +0.310493004 +0.28335035 +0.272742313 +0.264139875 +0.260055597 +0.250850105 +0.243940383 +0.22947018 +0.233060635 +0.238104781 +0.238997334 +0.230047473 +0.211539097 +0.185582814 +0.168902111 +0.165720808 +0.147968091 +0.135827802 +0.136955418 +0.150830762 +0.141442462 +0.109388461 +0.098069998 +0.091902505 +0.087115617 +0.084673268 +0.08585648 +0.087621518 +0.088064099 +0.091317428 +0.093145423 +0.0936907 +0.098445937 +0.103044819 +0.108890634 +0.114475917 +0.12511498 +0.134479724 +0.118361183 +0.09572975 +0.067839463 +0.062360642 +0.064554602 +0.067980971 +0.070687084 +0.095027318 +0.123557874 +0.139803365 +0.147472567 +0.148785814 +0.149915048 +0.155637761 +0.151792987 +0.151234869 +0.150631576 +0.140405863 +0.121844146 +0.1076095 +0.098375098 +0.093835781 +0.093606822 +0.09552607 +0.085366138 +0.064396734 +0.043200677 +0.044372408 +0.057244671 +0.074281196 +0.114071295 +0.199199169 +0.261145087 +0.288633656 +0.31317269 +0.332958283 +0.363409809 +0.395905431 +0.416175272 +0.425566409 +0.42355118 +0.426757444 +0.419274708 +0.399079219 +0.402991941 +0.421748644 +0.442033795 +0.442256755 +0.38016204 +0.315858717 +0.313099787 +0.366617625 +0.416411551 +0.454015033 +0.548809404 +0.637362346 +0.679229473 +0.689560481 +0.668051352 +0.61485285 +0.559628005 +0.510998841 +0.459100818 +0.399873928 +0.329363172 +0.255528848 +0.191993355 +0.14303036 +0.10440381 +0.076422563 +0.055463141 +0.039992241 +0.022482375 +0.011076033 +0.005732747 +0.003321433 +0.002195939 +0.002776666 +0.003829599 +0.006731285 +0.013242841 +0.022440027 +0.034022959 +0.044166584 +0.052207146 +0.058268035 +0.058009015 +0.052818837 +0.047503893 +0.044100975 +0.041343072 +0.040402521 +0.044864023 +0.056615539 +0.06916539 +0.074883972 +0.067017368 +0.052972219 +0.059128876 +0.077572554 +0.1045707 +0.132849812 +0.197418226 +0.308788662 +0.400896812 +0.443025739 +0.47133449 +0.487634956 +0.502217863 +0.507397258 +0.491916183 +0.449519986 +0.404331338 +0.358087367 +0.331086244 +0.317006175 +0.300214235 +0.305380575 +0.32749084 +0.333005121 +0.257111092 +0.170356499 +0.138309345 +0.122920725 +0.117427761 +0.121676558 +0.146809995 +0.195465765 +0.227134955 +0.217763704 +0.191897929 +0.165118725 +0.147020329 +0.135508267 +0.124146922 +0.109193613 +0.091733726 +0.079089568 +0.073933752 +0.071914037 +0.074441692 +0.082248192 +0.082188197 +0.075061237 +0.056715249 +0.033847003 +0.016216688 +0.009705018 +0.008089336 +0.012460907 +0.021904486 +0.044555932 +0.07286812 +0.093338119 +0.106681031 +0.106692662 +0.095319285 +0.081241201 +0.06368211 +0.053271382 +0.05153541 +0.054702955 +0.062548722 +0.07275978 +0.082617556 +0.095129144 +0.107753416 +0.120441461 +0.113431757 +0.082047998 +0.07079952 +0.087488008 +0.10900298 +0.130972953 +0.153279536 +0.203556022 +0.289254184 +0.337709676 +0.361767532 +0.378161027 +0.386969302 +0.389282907 +0.378532177 +0.351823158 +0.335921224 +0.323864034 +0.311219761 +0.295656587 +0.278607055 +0.27700833 +0.277961014 +0.262421888 +0.188807908 +0.133218166 +0.102029582 +0.083591848 +0.084868962 +0.100072422 +0.127990283 +0.182818777 +0.251190381 +0.307156657 +0.322071049 +0.304902319 +0.292344012 +0.281755388 +0.266117194 +0.25844089 +0.257637153 +0.255593784 +0.240897354 +0.23012329 +0.235310813 +0.217603971 +0.195784079 +0.179968891 +0.139033867 +0.094747812 +0.075418909 +0.063941934 +0.059478192 +0.05931504 +0.077341609 +0.114390693 +0.152770481 +0.182186704 +0.202579196 +0.226851966 +0.258640787 +0.288163439 +0.302615767 +0.293178885 +0.281408673 +0.278076233 +0.27039385 +0.258793144 +0.238171934 +0.231447914 +0.242249133 +0.249509449 +0.204510656 +0.132165602 +0.121218968 +0.136459625 +0.146380654 +0.157591353 +0.227568624 +0.354243662 +0.446483156 +0.485333905 +0.489202148 +0.472181995 +0.441082666 +0.412418091 +0.401897112 +0.391388986 +0.380080631 +0.357662644 +0.322698045 +0.287201478 +0.279318801 +0.290111489 +0.305780885 +0.31624882 +0.270036308 +0.202214895 +0.169590617 +0.1765435 +0.20303644 +0.227389632 +0.317562265 +0.446662421 +0.497832291 +0.510377149 +0.506486825 +0.482113693 +0.453795399 +0.438205723 +0.412377133 +0.369588227 +0.337035617 +0.306925966 +0.288723778 +0.273614323 +0.276868608 +0.286278768 +0.301316575 +0.311826402 +0.300224406 +0.328265507 +0.388164153 +0.461494337 +0.523392822 +0.543874431 +0.506909493 +0.487043527 +0.542152408 +0.597506249 +0.615784529 +0.638069551 +0.654739154 +0.65359393 +0.577729234 +0.480373888 +0.425189716 +0.383978084 +0.357800963 +0.324553696 +0.301771159 +0.295198091 +0.297084869 +0.269678255 +0.197495154 +0.171247814 +0.186277937 +0.173887112 +0.161023392 +0.158390039 +0.15829564 +0.215307561 +0.282964564 +0.321541934 +0.37077218 +0.412158969 +0.430643888 +0.418461834 +0.384858006 +0.344798984 +0.309993347 +0.280603152 +0.252614795 +0.219864783 +0.191308411 +0.171040498 +0.148193326 +0.123033865 +0.097284882 +0.075177699 +0.086478449 +0.110696691 +0.153404181 +0.209449969 +0.27831259 +0.377738615 +0.480561817 +0.570119142 +0.640310243 +0.704020493 +0.759457529 +0.792442969 +0.777534117 +0.766507116 +0.759858673 +0.730409948 +0.710379037 +0.680388247 +0.666956494 +0.658353665 +0.640798735 +0.597717136 +0.545695245 +0.539437074 +0.478103054 +0.420830618 +0.384365286 +0.360194815 +0.332966406 +0.283610183 +0.27214655 +0.262736601 +0.238551749 +0.209238814 +0.179834294 +0.157721988 +0.147397662 +0.133835198 +0.114602144 +0.125035541 +0.131694244 +0.140633232 +0.149572829 +0.155665478 +0.157204753 +0.146376089 +0.105986878 +0.069607918 +0.072723342 +0.073388507 +0.067034443 +0.062780171 +0.07116907 +0.091798257 +0.119143682 +0.124663067 +0.110428998 +0.087639346 +0.06912068 +0.06172184 +0.063196157 +0.061374032 +0.060097062 +0.064159067 +0.07425526 +0.089552807 +0.123434294 +0.16147941 +0.199275042 +0.234119307 +0.261254267 +0.296748849 +0.296912251 +0.298881147 +0.309945065 +0.321313962 +0.324543772 +0.31915867 +0.34072546 +0.364316845 +0.367599487 +0.371070252 +0.366923777 +0.35133551 +0.319227228 +0.278332176 +0.239074087 +0.210338208 +0.185036566 +0.160892536 +0.132853915 +0.109395897 +0.093914007 +0.074907433 +0.058924177 +0.05810029 +0.052148446 +0.051191367 +0.052989639 +0.055065354 +0.066032698 +0.072984205 +0.091252418 +0.118850425 +0.15109444 +0.184244853 +0.214847384 +0.238512659 +0.249729337 +0.24285801 +0.224838749 +0.205546474 +0.188946474 +0.167894234 +0.158166896 +0.162617483 +0.167403387 +0.172050037 +0.168528308 +0.132873494 +0.114699198 +0.129391006 +0.139787025 +0.129262401 +0.112683332 +0.127210594 +0.162984569 +0.194207082 +0.22317284 +0.251139723 +0.255700129 +0.23556649 +0.205778307 +0.166334262 +0.129943968 +0.104301636 +0.08918055 +0.085261358 +0.081470288 +0.082426474 +0.094099275 +0.104199043 +0.091124671 +0.089793125 +0.102094189 +0.156682711 +0.235876321 +0.308847541 +0.366534974 +0.421780196 +0.51833827 +0.616401376 +0.675071088 +0.720392079 +0.739551975 +0.754093571 +0.75338237 +0.749608539 +0.744867814 +0.738008172 +0.722596434 +0.714877789 +0.722718087 +0.742428038 +0.756505668 +0.759859131 +0.768723844 +0.764607946 +0.815677364 +0.840248978 +0.842093466 +0.837464823 +0.829828514 +0.836491011 +0.856679251 +0.8744443 +0.882799617 +0.880858606 +0.874018582 +0.854770301 +0.817926653 +0.772796028 +0.750149824 +0.736155623 +0.728523413 +0.730584663 +0.744796793 +0.770247125 +0.788055342 +0.780291246 +0.784636921 +0.850229796 +0.840632928 +0.8095401 +0.779850816 +0.750400652 +0.703834237 +0.649375352 +0.689287992 +0.743889981 +0.772836753 +0.779309032 +0.774111373 +0.759269184 +0.709681952 +0.644915131 +0.591377424 +0.552085384 +0.53221189 +0.499069383 +0.482864234 +0.476406972 +0.469285735 +0.427690213 +0.358233131 +0.391408986 +0.398826973 +0.383515093 +0.381703269 +0.385903529 +0.381148402 +0.374087894 +0.441418168 +0.53285577 +0.585901519 +0.624155009 +0.650748944 +0.660771913 +0.62398459 +0.577758783 +0.546613239 +0.520754655 +0.497360834 +0.468332119 +0.459448064 +0.471414083 +0.485569055 +0.471444215 +0.404658453 +0.409829319 +0.53768601 +0.556058089 +0.548183323 +0.530276018 +0.491889969 +0.487148733 +0.544635731 +0.582015613 +0.584746685 +0.579387117 +0.575311899 +0.566103425 +0.516516967 +0.449322198 +0.396774609 +0.349479397 +0.307646462 +0.273782531 +0.259059212 +0.261725501 +0.277900278 +0.265771769 +0.229524876 +0.167458446 +0.227508303 +0.319922378 +0.390530227 +0.426801304 +0.418527537 +0.444555505 +0.535684367 +0.57825277 +0.576298266 +0.55873651 +0.548623946 +0.530471328 +0.4814916 +0.417011045 +0.359301694 +0.293746131 +0.235138817 +0.196600345 +0.17878992 +0.172283569 +0.177769807 +0.160878694 +0.1370139 +0.105798037 +0.100949494 +0.106532928 +0.109123717 +0.112573883 +0.119076006 +0.125540577 +0.14708434 +0.194957227 +0.223719736 +0.242338092 +0.259192231 +0.26856382 +0.266792676 +0.248227698 +0.232144059 +0.224981542 +0.218053341 +0.215881778 +0.209997528 +0.2055988 +0.203635765 +0.19665811 +0.181527923 +0.129960708 +0.137445421 +0.182217144 +0.207351759 +0.219646087 +0.234492402 +0.255529603 +0.313447674 +0.367792863 +0.397795283 +0.403258484 +0.406693824 +0.399658666 +0.384099427 +0.370999562 +0.350443959 +0.320175762 +0.287716451 +0.26416054 +0.247186326 +0.235188715 +0.217127436 +0.182868409 +0.140646083 +0.109791436 +0.120661406 +0.13529829 +0.139997923 +0.143918829 +0.143565531 +0.126046132 +0.159347327 +0.187789818 +0.212017514 +0.21985812 +0.211474773 +0.194113221 +0.181789009 +0.171466678 +0.152995961 +0.142836717 +0.138909207 +0.145937606 +0.145910611 +0.14310411 +0.141139674 +0.119885779 +0.105294853 +0.074508377 +0.058915689 +0.057046268 +0.061289327 +0.066112313 +0.072009162 +0.084916713 +0.114184384 +0.137908614 +0.137474033 +0.129014785 +0.114383611 +0.103626272 +0.089399913 +0.076002128 +0.069638411 +0.066748259 +0.066137937 +0.064987189 +0.062899975 +0.059277931 +0.054452409 +0.054593036 +0.054716074 +0.04566349 +0.053371842 +0.061935794 +0.071033622 +0.078568344 +0.08223686 +0.077867148 +0.087245613 +0.112216745 +0.114234728 +0.112134688 +0.104987027 +0.098418211 +0.090149471 +0.088943128 +0.094246711 +0.101453906 +0.108715845 +0.110807125 +0.116017816 +0.121264446 +0.117163312 +0.099170427 +0.099656304 +0.088735606 +0.099804795 +0.115469953 +0.125680411 +0.132675204 +0.135004748 +0.121323347 +0.102536115 +0.110563603 +0.105548375 +0.096576035 +0.086837943 +0.078135901 +0.074922882 +0.070327701 +0.064884356 +0.057774359 +0.049103568 +0.041556407 +0.037026934 +0.03602327 +0.035854436 +0.033264031 +0.0276427 +0.020202962 +0.028363757 +0.034598431 +0.035348632 +0.032544864 +0.02709352 +0.026408069 +0.022562945 +0.021871317 +0.027151933 +0.033461481 +0.03792201 +0.039047627 +0.039298227 +0.040298823 +0.039999316 +0.039894496 +0.040154805 +0.042031172 +0.040760113 +0.040732301 +0.040718399 +0.038833753 +0.024058833 +0.01070287 +0.013406062 +0.019997072 +0.028381032 +0.032718357 +0.034353174 +0.04567931 +0.076590529 +0.094880181 +0.101149499 +0.095627662 +0.083521899 +0.07154395 +0.062107321 +0.054318681 +0.048872081 +0.04621674 +0.043466612 +0.04302837 +0.041329675 +0.040212595 +0.038683966 +0.036734981 +0.022932585 +0.010723514 +0.008962102 +0.012221822 +0.016134138 +0.018782467 +0.023117971 +0.029023045 +0.042185866 +0.06508553 +0.082084311 +0.091334828 +0.097224826 +0.094685872 +0.09553216 +0.100759717 +0.103498428 +0.104379203 +0.106043262 +0.108293086 +0.113701555 +0.127302269 +0.139542666 +0.128731568 +0.134785963 +0.156569786 +0.184609389 +0.212535445 +0.24149935 +0.258537432 +0.260620725 +0.241690794 +0.26994732 +0.267950968 +0.245630894 +0.223487602 +0.209412054 +0.206354308 +0.195644466 +0.170215141 +0.146817118 +0.129838713 +0.116381612 +0.107837822 +0.108913099 +0.114322966 +0.111514535 +0.08411044 +0.056696445 +0.032749985 +0.062113081 +0.087192197 +0.094141909 +0.090843223 +0.081497251 +0.079517318 +0.067531954 +0.057847498 +0.053431538 +0.055709475 +0.064747546 +0.079401611 +0.098005348 +0.114848801 +0.124757478 +0.128055506 +0.127402851 +0.118999432 +0.10319417 +0.088403915 +0.07543123 +0.058664452 +0.036861073 +0.023649681 +0.032344339 +0.041284888 +0.043155656 +0.042982435 +0.045781272 +0.073407463 +0.141836673 +0.240824226 +0.357579252 +0.466873063 +0.547549672 +0.595860718 +0.60403288 +0.553322037 +0.494527125 +0.451322615 +0.411274618 +0.372618688 +0.328930883 +0.304485277 +0.301774644 +0.260737266 +0.17767454 +0.230694699 +0.318861295 +0.384332049 +0.401094766 +0.383035973 +0.337220945 +0.324336946 +0.380932864 +0.420889566 +0.435255277 +0.422978023 +0.399555297 +0.334610665 +0.281882698 +0.250274037 +0.233963557 +0.223729092 +0.21601397 +0.193810145 +0.167739884 +0.151175926 +0.141950844 +0.109074484 +0.04248626 +0.019538372 +0.021689269 +0.027933152 +0.033987648 +0.037810969 +0.043028387 +0.105079238 +0.173523828 +0.235268791 +0.274212504 +0.280537038 +0.284241709 +0.275799994 +0.248274637 +0.213485938 +0.181095705 +0.154205632 +0.134971074 +0.121017342 +0.115017759 +0.116225802 +0.128363456 +0.121497382 +0.06425581 +0.043317084 +0.047669245 +0.061252061 +0.070515967 +0.074522121 +0.079136868 +0.133397643 +0.241624656 +0.386411387 +0.495112847 +0.554749752 +0.582471464 +0.597707105 +0.585778041 +0.550986154 +0.515611955 +0.467299827 +0.410113485 +0.358779627 +0.316232528 +0.28947114 +0.275698793 +0.208425052 +0.11090062 +0.08184844 +0.125538932 +0.176046474 +0.208500349 +0.213626609 +0.196454214 +0.257626026 +0.376047409 +0.458768195 +0.500988321 +0.509711226 +0.497600225 +0.473806735 +0.455903594 +0.435563909 +0.407743701 +0.362767701 +0.308002719 +0.26477613 +0.239882276 +0.230748575 +0.22755708 +0.170403065 +0.08572584 +0.061238428 +0.092200519 +0.130644684 +0.149728429 +0.15343845 +0.136081411 +0.157809521 +0.159575532 +0.152178926 +0.151334742 +0.152702722 +0.147260509 +0.145006422 +0.162624454 +0.19138301 +0.215732105 +0.230449406 +0.222693567 +0.20246036 +0.187903354 +0.18858862 +0.194602794 +0.164136749 +0.100464631 +0.065789758 +0.066758069 +0.093223377 +0.106584236 +0.111490645 +0.117606348 +0.170095129 +0.24954715 +0.299414793 +0.311159281 +0.309466614 +0.301783628 +0.289946662 +0.298220568 +0.293685965 +0.311159983 +0.332196815 +0.353423574 +0.373430052 +0.387869442 +0.408949759 +0.425163172 +0.356736853 +0.299603244 +0.350755517 +0.389862949 +0.421875947 +0.445073772 +0.454909173 +0.44416555 +0.439867279 +0.4933823 +0.545818011 +0.555588962 +0.555023178 +0.546372215 +0.534789895 +0.519147322 +0.495093956 +0.480045728 +0.465398441 +0.448686964 +0.432653198 +0.415753827 +0.407603071 +0.39019302 +0.366242147 +0.347032111 +0.381997426 +0.423754823 +0.442178805 +0.452605003 +0.459863457 +0.458455939 +0.434776611 +0.458770741 +0.530872458 +0.548509876 +0.54327427 +0.53445718 +0.517818461 +0.479456826 +0.418906469 +0.371349564 +0.323423819 +0.284537287 +0.260184989 +0.261801848 +0.281628453 +0.280651475 +0.264101807 +0.263142147 +0.3811991 +0.452303872 +0.501814053 +0.551859001 +0.578944321 +0.569163592 +0.533019774 +0.517458472 +0.545290364 +0.509443017 +0.450388715 +0.386518404 +0.319082707 +0.246899437 +0.176875094 +0.128390954 +0.09786637 +0.076620139 +0.062548106 +0.05360238 +0.048630607 +0.040381591 +0.032052927 +0.026547848 +0.022032606 +0.02817696 +0.040994455 +0.060361258 +0.08141019 +0.099188972 +0.115819087 +0.110391895 +0.123721623 +0.119007736 +0.107184568 +0.094068574 +0.083768698 +0.075478081 +0.068626232 +0.061443576 +0.055901032 +0.052429497 +0.048604787 +0.044099728 +0.041161693 +0.036365066 +0.026515174 +0.018310749 +0.021964021 +0.02750154 +0.030250415 +0.032425251 +0.034739122 +0.036532364 +0.040684282 +0.046930878 +0.053687071 +0.053623926 +0.051676328 +0.048824975 +0.045974135 +0.04237713 +0.040945602 +0.043261891 +0.046795288 +0.049210583 +0.049970909 +0.047764589 +0.041795507 +0.032458587 +0.023562157 +0.013416763 +0.012535 +0.01655865 +0.02374436 +0.032788375 +0.040415267 +0.046113327 +0.04982357 +0.054923005 +0.089797172 +0.107383348 +0.10995615 +0.104857937 +0.099096415 +0.087936261 +0.080645522 +0.07813084 +0.080506379 +0.085531393 +0.087543246 +0.089757652 +0.091386621 +0.086694941 +0.080094368 +0.06080012 +0.062589129 +0.068105194 +0.073606704 +0.081150483 +0.090540516 +0.100149573 +0.110586337 +0.12320141 +0.179645059 +0.218405113 +0.22705594 +0.226034044 +0.219926538 +0.201035591 +0.176875116 +0.160409376 +0.154124263 +0.154292786 +0.156031853 +0.154079039 +0.147861199 +0.133811711 +0.118936 +0.102023582 +0.105606227 +0.116468783 +0.132419423 +0.15440772 +0.180316007 +0.204012385 +0.222548186 +0.209358413 +0.276148027 +0.318050972 +0.331794005 +0.331717425 +0.323280891 +0.304780746 +0.278098273 +0.255063165 +0.247246364 +0.247478342 +0.241963407 +0.231470107 +0.221459475 +0.205064116 +0.195525679 +0.166615018 +0.17897819 +0.202587661 +0.220889484 +0.234911771 +0.247321778 +0.255685978 +0.25643785 +0.260586811 +0.35439523 +0.417557276 +0.436529743 +0.437358078 +0.427951333 +0.392604507 +0.342910307 +0.299519186 +0.259602482 +0.232253251 +0.217295067 +0.20833661 +0.21208203 +0.19091254 +0.159412894 +0.121382052 +0.13540903 +0.153685436 +0.167761034 +0.179229847 +0.18668807 +0.187896054 +0.176828061 +0.158194802 +0.209613418 +0.239311393 +0.248556363 +0.239783886 +0.223055597 +0.203187117 +0.183267833 +0.166542634 +0.156089522 +0.151621226 +0.142415086 +0.120339301 +0.099061045 +0.070769669 +0.040123343 +0.026090953 +0.035947432 +0.046383099 +0.05105226 +0.052794158 +0.051164068 +0.046076198 +0.036427483 +0.04584443 +0.057158612 +0.067179605 +0.069375982 +0.066744443 +0.064643824 +0.064084662 +0.066123761 +0.071942799 +0.078818925 +0.089234933 +0.091952794 +0.089756777 +0.094747246 +0.092406028 +0.065865469 +0.044761648 +0.048009565 +0.057464326 +0.057563604 +0.055322828 +0.053382773 +0.052356024 +0.055488781 +0.089386881 +0.170689347 +0.252330159 +0.3041632 +0.332014399 +0.34706065 +0.35029971 +0.341488969 +0.334292542 +0.314372737 +0.292773283 +0.283516073 +0.289143525 +0.301690863 +0.280850651 +0.168516428 +0.11327708 +0.181217954 +0.207288121 +0.213598435 +0.215100988 +0.21194886 +0.209115279 +0.20893703 +0.325199897 +0.462272407 +0.510457978 +0.491465471 +0.442220729 +0.397094898 +0.342683884 +0.301014262 +0.271743167 +0.238974405 +0.210931934 +0.193224104 +0.169739884 +0.150595594 +0.126150224 +0.08865596 +0.101445043 +0.1405503 +0.177586828 +0.214895273 +0.256790824 +0.297529944 +0.322312902 +0.312654853 +0.281362038 +0.344974936 +0.388214519 +0.414148313 +0.444159944 +0.48708095 +0.511395402 +0.496606434 +0.472761307 +0.456179346 +0.443251919 +0.430137606 +0.433940325 +0.444636855 +0.407782674 +0.343408236 +0.391611839 +0.425806058 +0.423707324 +0.406887989 +0.390619814 +0.373748466 +0.352013025 +0.31312515 +0.34155159 +0.460442145 +0.471030871 +0.436686039 +0.384973986 +0.329382939 +0.273896019 +0.233188105 +0.216085231 +0.212856811 +0.207397066 +0.196967956 +0.182105954 +0.160177486 +0.112718726 +0.052930509 +0.031039198 +0.035357353 +0.042592675 +0.050452536 +0.058989394 +0.064669096 +0.067838589 +0.067988463 +0.075190379 +0.081757592 +0.102554892 +0.12269387 +0.137774669 +0.148454516 +0.153297063 +0.149929549 +0.149271226 +0.159387878 +0.178291296 +0.199641669 +0.210045142 +0.202189351 +0.15598455 +0.066170223 +0.046190006 +0.067560061 +0.084665564 +0.107711835 +0.135333833 +0.156285889 +0.162053378 +0.134004736 +0.131005065 +0.163751216 +0.23885395 +0.317543349 +0.380201647 +0.417417088 +0.430721456 +0.432147716 +0.431921169 +0.417956182 +0.387478843 +0.358138222 +0.353911236 +0.367340102 +0.331753067 +0.183407889 +0.129720051 +0.229720607 +0.350304193 +0.408466159 +0.445343488 +0.463534567 +0.443507027 +0.394318974 +0.433590222 +0.449346557 +0.437868459 +0.414258792 +0.374622925 +0.322043152 +0.277395698 +0.243386247 +0.225873834 +0.211699516 +0.193092114 +0.165435452 +0.12520571 +0.108784698 +0.09401455 +0.052084708 +0.031088939 +0.04163321 +0.077877799 +0.096455862 +0.098616028 +0.094280236 +0.09033105 +0.104294008 +0.172037243 +0.246876176 +0.306488832 +0.352808586 +0.389150989 +0.411373447 +0.39603602 +0.360159798 +0.325567306 +0.297823234 +0.285947141 +0.282314521 +0.273958269 +0.265026561 +0.23335156 +0.150060444 +0.075352304 +0.063037896 +0.121154227 +0.153855165 +0.166419478 +0.175368149 +0.174102392 +0.203499523 +0.288276277 +0.382965645 +0.429058106 +0.462015111 +0.50082514 +0.527757864 +0.509780122 +0.475858281 +0.450610522 +0.433030986 +0.403023721 +0.366914843 +0.337582797 +0.313795436 +0.204585357 +0.108091539 +0.110778187 +0.138081272 +0.144018384 +0.143332604 +0.138536958 +0.135336165 +0.138715082 +0.149345028 +0.203592349 +0.280798783 +0.31527147 +0.303701558 +0.273318688 +0.247403203 +0.224679478 +0.21393127 +0.217100518 +0.230061667 +0.245599684 +0.261442589 +0.274227357 +0.269412659 +0.185129345 +0.085212584 +0.086933611 +0.140863687 +0.162920537 +0.187247012 +0.217743158 +0.254841264 +0.290738045 +0.297880975 +0.330231864 +0.516767255 +0.641380033 +0.67257639 +0.655664168 +0.622758628 +0.567646303 +0.50677514 +0.455787443 +0.425484081 +0.416232621 +0.400496157 +0.381580067 +0.339101819 +0.227558323 +0.14448052 +0.174960194 +0.26171169 +0.286084816 +0.30627839 +0.330780622 +0.356841645 +0.370208027 +0.359843471 +0.376277073 +0.467900103 +0.507805353 +0.523170653 +0.524735843 +0.515002637 +0.499314218 +0.468517201 +0.436823946 +0.409976628 +0.38116898 +0.337576898 +0.295948284 +0.270776238 +0.170347498 +0.067845235 +0.050039584 +0.066669474 +0.083868534 +0.085324598 +0.079809852 +0.076331096 +0.075396404 +0.079103341 +0.107691103 +0.132648869 +0.173175674 +0.207608803 +0.227959726 +0.239863618 +0.252088747 +0.264755309 +0.268502017 +0.266916698 +0.267057286 +0.277821401 +0.287727434 +0.278178943 +0.169913537 +0.058809915 +0.051961897 +0.067552568 +0.075432675 +0.092981791 +0.117204813 +0.141340442 +0.157596135 +0.159393546 +0.193565511 +0.200004385 +0.203025946 +0.195113567 +0.192327894 +0.211216094 +0.243000468 +0.272681592 +0.302640086 +0.337688083 +0.372965693 +0.384139425 +0.362306876 +0.346289391 +0.233454999 +0.073672124 +0.050677248 +0.089713795 +0.100488572 +0.112439028 +0.123393747 +0.124837834 +0.113267089 +0.11488165 +0.116224748 +0.096595584 +0.081931971 +0.063886811 +0.043711737 +0.027930178 +0.019210832 +0.019690888 +0.024574006 +0.030461748 +0.035914431 +0.041218541 +0.042982787 +0.044659804 +0.051471142 +0.025412617 +0.007775451 +0.017320793 +0.028988734 +0.036914339 +0.040387094 +0.040537128 +0.03833751 +0.044805743 +0.064151602 +0.082830689 +0.107437386 +0.128722677 +0.141752213 +0.148884425 +0.162287604 +0.176515083 +0.184950192 +0.195064602 +0.211310041 +0.232434936 +0.23689296 +0.233339755 +0.14912211 +0.03309629 +0.031280034 +0.055924492 +0.070958654 +0.089329043 +0.109901242 +0.133081376 +0.153503404 +0.16710001 +0.215073239 +0.201313147 +0.187508731 +0.178238572 +0.172860842 +0.172099889 +0.182544327 +0.208682203 +0.247179962 +0.29981407 +0.352375923 +0.386920667 +0.399109455 +0.392077123 +0.192374953 +0.081704845 +0.17624115 +0.221847736 +0.231752058 +0.26546295 +0.304614699 +0.337104874 +0.348878347 +0.367129075 +0.463572777 +0.599118934 +0.647634438 +0.647319337 +0.631273709 +0.607375016 +0.546183133 +0.489900389 +0.45402853 +0.417441767 +0.384589129 +0.350494474 +0.331799274 +0.303126327 +0.196535179 +0.244625035 +0.272769945 +0.299100215 +0.342282952 +0.388238665 +0.428962555 +0.455789344 +0.470821861 +0.475730524 +0.553897096 +0.69542931 +0.724493662 +0.740850057 +0.744085355 +0.744104155 +0.667551041 +0.553205298 +0.476961021 +0.421879031 +0.367610389 +0.323150241 +0.330669889 +0.334028406 +0.169191683 +0.152943677 +0.277879157 +0.415942697 +0.559958382 +0.643552229 +0.692319066 +0.724220343 +0.735684949 +0.711144833 +0.634136988 +0.663712954 +0.689782764 +0.644396344 +0.59707734 +0.558440227 +0.498132574 +0.421115377 +0.353127135 +0.293221036 +0.240444437 +0.204790589 +0.180070313 +0.152669326 +0.069808165 +0.045712712 +0.091920364 +0.137470057 +0.166999914 +0.193608665 +0.212626304 +0.218866927 +0.208362796 +0.15814244 +0.160474878 +0.227027929 +0.358240592 +0.513252265 +0.622491467 +0.706371681 +0.785093545 +0.786137242 +0.726367927 +0.656438412 +0.61231909 +0.574069731 +0.568861351 +0.543131397 +0.382937336 +0.592427624 +0.726771006 +0.774204762 +0.801689705 +0.81472535 +0.821134624 +0.818055927 +0.803514395 +0.773522809 +0.699732151 +0.674954515 +0.700706916 +0.665769597 +0.625215662 +0.586278821 +0.525793106 +0.457920606 +0.399782304 +0.352985879 +0.317619519 +0.27952653 +0.251858388 +0.185853059 +0.11912124 +0.160283747 +0.164149012 +0.172510277 +0.193480594 +0.222856001 +0.252263245 +0.272862056 +0.278445877 +0.258149894 +0.237888969 +0.295377775 +0.298553492 +0.327100537 +0.386052555 +0.44278791 +0.445030315 +0.397538217 +0.383894525 +0.382053963 +0.380311597 +0.361834393 +0.348386553 +0.286301794 +0.15781664 +0.219899392 +0.255945806 +0.25404755 +0.257200721 +0.267223154 +0.269361464 +0.255555662 +0.220943035 +0.168999966 +0.152593801 +0.185270094 +0.213748535 +0.230540446 +0.239968223 +0.256600881 +0.273396114 +0.266855696 +0.253440799 +0.257117744 +0.261037713 +0.251233867 +0.241801241 +0.210902174 +0.199592549 +0.236053436 +0.255737551 +0.262319746 +0.259804622 +0.253250576 +0.243314225 +0.231830562 +0.223143553 +0.21529234 +0.197535867 +0.214831951 +0.229203042 +0.186768223 +0.13841772 +0.11090764 +0.100901197 +0.107134904 +0.130802434 +0.174427473 +0.217308971 +0.236031465 +0.230972981 +0.179118527 +0.096065311 +0.187197674 +0.292347208 +0.334304009 +0.378298866 +0.399700156 +0.3874525 +0.358913417 +0.320609016 +0.275889673 +0.211720889 +0.18020585 +0.128627666 +0.099234924 +0.118870293 +0.184365692 +0.229836611 +0.234249943 +0.248403755 +0.263072932 +0.275700897 +0.294507534 +0.336876432 +0.355243621 +0.28792118 +0.269028725 +0.392665954 +0.518222926 +0.61558439 +0.692578985 +0.747669684 +0.7944037 +0.820690099 +0.816314978 +0.807085951 +0.835820208 +0.823273821 +0.784512303 +0.736820423 +0.679371498 +0.599815818 +0.523268179 +0.476729932 +0.435529288 +0.403001855 +0.36660407 +0.335528646 +0.236578506 +0.169191295 +0.163471279 +0.116899987 +0.097217182 +0.089797887 +0.086445762 +0.086044288 +0.08801031 +0.088990334 +0.083996353 +0.088688488 +0.093433712 +0.116565345 +0.161663961 +0.202254207 +0.245878083 +0.308337879 +0.38347797 +0.450655324 +0.504955898 +0.535310197 +0.540663995 +0.511886503 +0.408478557 +0.113431972 +0.034315824 +0.048761126 +0.092341804 +0.135870502 +0.173102466 +0.218004596 +0.269563955 +0.307861745 +0.322981284 +0.459244782 +0.568484772 +0.628542947 +0.670540484 +0.69089647 +0.694782135 +0.666686497 +0.623298634 +0.584623455 +0.549605011 +0.516887748 +0.488615652 +0.451670604 +0.336090626 +0.120963904 +0.054951093 +0.090829037 +0.139084702 +0.144908178 +0.165806386 +0.188908266 +0.210285986 +0.225534791 +0.223608513 +0.275850827 +0.28003215 +0.274654646 +0.260724524 +0.241195375 +0.229518516 +0.216569857 +0.198737479 +0.186882457 +0.176775037 +0.178965891 +0.181256668 +0.193069872 +0.178770178 +0.106276945 +0.064383963 +0.05052061 +0.039553085 +0.029144516 +0.026067485 +0.030223806 +0.037112681 +0.045905298 +0.065372657 +0.155679424 +0.333311573 +0.456478371 +0.492237014 +0.500443015 +0.468111945 +0.416073564 +0.370727644 +0.316791953 +0.270276067 +0.241701165 +0.225477819 +0.226616797 +0.172695387 +0.132709201 +0.167537435 +0.189923004 +0.177591274 +0.165914095 +0.161413046 +0.15304598 +0.138988363 +0.121437721 +0.099395882 +0.084150943 +0.093184914 +0.132962826 +0.159866754 +0.171126997 +0.183421444 +0.197475398 +0.209390025 +0.21900008 +0.225807145 +0.231829198 +0.222097068 +0.186671942 +0.094187477 +0.029423759 +0.049980604 +0.086816223 +0.092451585 +0.103005835 +0.119614306 +0.135269913 +0.14640568 +0.150128025 +0.144445833 +0.138332167 +0.164235832 +0.192427099 +0.232507565 +0.258317894 +0.269568541 +0.258848762 +0.233844728 +0.211169077 +0.186363406 +0.159186191 +0.13590853 +0.116736375 +0.074094432 +0.012971127 +0.007971131 +0.013159936 +0.01626103 +0.019474567 +0.023554786 +0.02754095 +0.030677316 +0.031083489 +0.028905424 +0.042344017 +0.078279103 +0.110346404 +0.137218819 +0.153196761 +0.168443192 +0.182468241 +0.201747224 +0.222802769 +0.236443914 +0.242616121 +0.237419579 +0.244058084 +0.231271959 +0.084301523 +0.043737745 +0.077240704 +0.127496491 +0.164346948 +0.200749112 +0.229501202 +0.238838917 +0.227165288 +0.209231899 +0.205757975 +0.213393509 +0.201020021 +0.2018164 +0.203558947 +0.205643738 +0.219554016 +0.234406917 +0.242433737 +0.24386546 +0.245779789 +0.23729265 +0.215515591 +0.163870326 +0.048980901 +0.040772775 +0.090239046 +0.122063868 +0.141860019 +0.153731278 +0.15698175 +0.15132612 +0.135041818 +0.11193249 +0.120016134 +0.09801173 +0.090237549 +0.111286154 +0.143250901 +0.18344641 +0.233484994 +0.311180912 +0.41434644 +0.484072674 +0.522573437 +0.531512034 +0.524121132 +0.453316339 +0.254031111 +0.217088755 +0.289500983 +0.375118544 +0.450406453 +0.517833864 +0.576895114 +0.636875154 +0.683617945 +0.727759917 +0.745414681 +0.715560144 +0.735149352 +0.720560355 +0.68981026 +0.657334289 +0.615986194 +0.57458685 +0.552425328 +0.539662553 +0.527495705 +0.514375687 +0.497365075 +0.491091393 +0.563372147 +0.552306213 +0.547784585 +0.553353796 +0.566455365 +0.576471696 +0.575955888 +0.564697445 +0.543659442 +0.504548339 +0.437066062 +0.391126835 +0.412767776 +0.356411646 +0.289696234 +0.238064396 +0.192883098 +0.151687475 +0.118329734 +0.092040728 +0.077895109 +0.063056444 +0.044568943 +0.020028571 +0.005130156 +0.009039271 +0.013546597 +0.016830766 +0.023044192 +0.03437658 +0.048723436 +0.068811241 +0.091461422 +0.103283727 +0.104633296 +0.137055996 +0.203286654 +0.234318436 +0.237201868 +0.231479477 +0.226951712 +0.215564356 +0.202658406 +0.189267566 +0.183903972 +0.18511658 +0.178572618 +0.086283297 +0.064462954 +0.096977977 +0.115199888 +0.129304275 +0.129168144 +0.1235689 +0.113464201 +0.099643278 +0.087206706 +0.080587155 +0.075808883 +0.142171819 +0.22271645 +0.264299113 +0.255155305 +0.233475227 +0.223148982 +0.212664612 +0.18705716 +0.152868292 +0.123492181 +0.104374198 +0.094287703 +0.046618608 +0.021382785 +0.039387657 +0.045417576 +0.045127712 +0.041667318 +0.036829873 +0.031801841 +0.027093452 +0.02327047 +0.020892697 +0.020757422 +0.042272132 +0.06939472 +0.099967705 +0.120687558 +0.130558637 +0.130994454 +0.124139661 +0.11437378 +0.103569491 +0.095232106 +0.090000613 +0.074138186 +0.027888089 +0.003512866 +0.00608848 +0.008996114 +0.012141078 +0.014240677 +0.015334017 +0.015678396 +0.015741543 +0.015642909 +0.015676239 +0.022023188 +0.056620792 +0.09604201 +0.153799611 +0.202373654 +0.230730671 +0.220908876 +0.192907435 +0.166216005 +0.139890902 +0.131663969 +0.139063646 +0.141012486 +0.077097386 +0.024614785 +0.034118864 +0.043438321 +0.045177477 +0.048133009 +0.051105748 +0.056396368 +0.062063292 +0.067135082 +0.069616692 +0.073539056 +0.114437396 +0.220898793 +0.311331974 +0.36268635 +0.41957313 +0.451387989 +0.473782536 +0.475264392 +0.457065962 +0.430759019 +0.407045027 +0.359359773 +0.147164388 +0.126021777 +0.222336402 +0.256294648 +0.283836169 +0.322044622 +0.362500772 +0.39275762 +0.412394503 +0.417353294 +0.400090655 +0.369200388 +0.439811378 +0.561739122 +0.579462383 +0.56689053 +0.561329364 +0.529850653 +0.48300622 +0.459445714 +0.455888677 +0.448755433 +0.441758864 +0.395230803 +0.166299344 +0.049107256 +0.112491398 +0.197610405 +0.268164617 +0.346315747 +0.415133503 +0.453623206 +0.46623384 +0.462411497 +0.435250948 +0.389051117 +0.451503844 +0.476549829 +0.480901138 +0.487940063 +0.483039457 +0.477152745 +0.465278101 +0.453913734 +0.436297634 +0.41502808 +0.382004306 +0.344827714 +0.179384682 +0.037387413 +0.056900164 +0.171868158 +0.244725548 +0.287636063 +0.320841338 +0.336914237 +0.326302901 +0.291750059 +0.23796573 +0.216192574 +0.263627402 +0.266729829 +0.298788266 +0.326487514 +0.344616901 +0.355065704 +0.370247977 +0.401311875 +0.436669679 +0.481429063 +0.521249869 +0.527496808 +0.351917086 +0.135276848 +0.131719068 +0.284870769 +0.379888708 +0.407352716 +0.411906526 +0.402479824 +0.378577049 +0.344717162 +0.309610703 +0.329889266 +0.421010142 +0.506032317 +0.547358106 +0.525284225 +0.529154524 +0.541882796 +0.552452274 +0.55894111 +0.58272894 +0.590775326 +0.576815444 +0.553903171 +0.481572027 +0.512950222 +0.507443608 +0.472192841 +0.412497974 +0.337238725 +0.272072174 +0.22605292 +0.198820828 +0.197812936 +0.238588456 +0.318555994 +0.414188036 +0.511102698 +0.54761086 +0.524689121 +0.472720026 +0.462501637 +0.45904014 +0.465848628 +0.467503308 +0.451139376 +0.461798132 +0.422783392 +0.378326291 +0.371461903 +0.348478125 +0.330522143 +0.327556533 +0.349011861 +0.373705395 +0.394069318 +0.4089634 +0.416438829 +0.415665795 +0.388597072 +0.339389161 +0.348130756 +0.345809889 +0.331537785 +0.328132361 +0.322912871 +0.313092407 +0.330428826 +0.319770441 +0.297221794 +0.295897171 +0.276926073 +0.219770997 +0.287990923 +0.381673029 +0.379053899 +0.345481889 +0.305076746 +0.271930503 +0.254852263 +0.25051753 +0.246411736 +0.23311999 +0.201385691 +0.200352241 +0.151720814 +0.109073153 +0.096714303 +0.107096812 +0.14756041 +0.214526868 +0.287103504 +0.339753787 +0.357979257 +0.352256908 +0.326605307 +0.145122536 +0.025788177 +0.036443561 +0.074408076 +0.083985695 +0.093856949 +0.105454854 +0.117461135 +0.128893156 +0.137280059 +0.136701586 +0.113531338 +0.111015449 +0.141189032 +0.235135537 +0.356654786 +0.463472052 +0.519895517 +0.528715616 +0.514783731 +0.479020635 +0.440712078 +0.40829935 +0.37909742 +0.204554957 +0.061750642 +0.062527965 +0.17560928 +0.294722127 +0.413239106 +0.516811321 +0.586894841 +0.621521746 +0.625831514 +0.586398291 +0.530696645 +0.537489191 +0.51277111 +0.486852778 +0.472812457 +0.451016803 +0.407305858 +0.372429644 +0.357641137 +0.350486829 +0.356859871 +0.377979812 +0.415043776 +0.348863311 +0.164639365 +0.185658193 +0.312342315 +0.480999808 +0.600729358 +0.701042162 +0.780802851 +0.832905766 +0.848668009 +0.838821378 +0.842364915 +0.817582649 +0.776538184 +0.77278868 +0.76776799 +0.738043149 +0.706141243 +0.685412329 +0.665218284 +0.645134092 +0.622243851 +0.59332323 +0.545534173 +0.5551042 +0.627312194 +0.628903281 +0.625777883 +0.632362585 +0.652230812 +0.672768211 +0.686328334 +0.694261553 +0.697564617 +0.687988669 +0.664644348 +0.614837784 +0.564583296 +0.543003622 +0.490559206 +0.445457872 +0.384632353 +0.33592481 +0.304192437 +0.279475809 +0.246304702 +0.199223153 +0.121778562 +0.059131126 +0.075870236 +0.084818959 +0.085835381 +0.091784497 +0.105300534 +0.127246915 +0.154338727 +0.182088877 +0.203829098 +0.214711759 +0.218818459 +0.204090333 +0.228902274 +0.273005324 +0.283993853 +0.297810469 +0.29262941 +0.256742928 +0.219001292 +0.19058213 +0.163746855 +0.140387826 +0.085729215 +0.043664411 +0.051126251 +0.049120109 +0.054573108 +0.066765508 +0.076312079 +0.082856062 +0.093416899 +0.10697026 +0.122733179 +0.140514198 +0.157276789 +0.175106657 +0.291854617 +0.354802451 +0.346568595 +0.32894734 +0.301477044 +0.265107028 +0.236824523 +0.220460373 +0.204532041 +0.193261232 +0.138632973 +0.088084488 +0.137566629 +0.178238341 +0.204888873 +0.231897505 +0.256210929 +0.280791764 +0.309763045 +0.338411193 +0.361182897 +0.374435625 +0.374826887 +0.325570603 +0.389368485 +0.47788659 +0.45949845 +0.406392113 +0.328781936 +0.257476322 +0.21550994 +0.194941649 +0.176689539 +0.159612191 +0.102832177 +0.042585659 +0.064120817 +0.072240387 +0.066111596 +0.057631369 +0.055871348 +0.061352347 +0.071004119 +0.084936926 +0.101180463 +0.114630608 +0.108988973 +0.104252262 +0.084250776 +0.066068971 +0.061392645 +0.061822723 +0.069053178 +0.081835259 +0.095743504 +0.117305913 +0.134753439 +0.150891291 +0.152808846 +0.06279282 +0.093731299 +0.163139773 +0.188652403 +0.193585528 +0.183009402 +0.177445486 +0.184102371 +0.198754244 +0.216105126 +0.227251557 +0.236247688 +0.271130423 +0.378363332 +0.457901375 +0.469065737 +0.467241465 +0.439648632 +0.394085566 +0.339587102 +0.278376431 +0.240568127 +0.221050018 +0.177494992 +0.069058426 +0.082066304 +0.114841262 +0.103919399 +0.075316156 +0.043237397 +0.022265926 +0.012693168 +0.008713514 +0.007905706 +0.008518192 +0.010318157 +0.022042898 +0.048559574 +0.083823228 +0.122407276 +0.154250957 +0.185102802 +0.193630307 +0.187470362 +0.177510636 +0.16410298 +0.150188251 +0.143906399 +0.046420379 +0.032688355 +0.05135942 +0.063102378 +0.070837842 +0.070079751 +0.066449281 +0.067208593 +0.073935011 +0.086709933 +0.10500843 +0.120456637 +0.130314009 +0.173902701 +0.207491778 +0.20195519 +0.165974699 +0.138315101 +0.115050061 +0.092931579 +0.074381615 +0.057628119 +0.048291883 +0.042671853 +0.016874791 +0.006389535 +0.017551854 +0.025898316 +0.026692702 +0.022444986 +0.017186488 +0.013096829 +0.010723555 +0.010691316 +0.013262301 +0.018362111 +0.038131186 +0.089517995 +0.168626687 +0.233659742 +0.279526825 +0.293108601 +0.278324292 +0.248386539 +0.220424955 +0.191160203 +0.165569558 +0.107547817 +0.023088333 +0.008577003 +0.009896602 +0.013845049 +0.016726851 +0.017504539 +0.018285593 +0.019375114 +0.021421907 +0.024633448 +0.031038889 +0.041133684 +0.057586466 +0.117364136 +0.163665036 +0.185787828 +0.189000535 +0.185809484 +0.179404903 +0.146087392 +0.11161339 +0.102535902 +0.115916271 +0.132498059 +0.114968515 +0.035200457 +0.028788633 +0.081120601 +0.123835015 +0.173213605 +0.219624867 +0.238335858 +0.220571142 +0.184815004 +0.143466349 +0.101584941 +0.082556503 +0.10528889 +0.133849414 +0.145742391 +0.155422153 +0.154911732 +0.155377233 +0.164791848 +0.169860664 +0.167176523 +0.171159945 +0.160109764 +0.065722795 +0.037031361 +0.045489885 +0.045707716 +0.044112964 +0.042019373 +0.038058469 +0.034838547 +0.03348833 +0.035544689 +0.042064767 +0.050666224 +0.072526617 +0.150460295 +0.223278251 +0.237081415 +0.232952498 +0.231150964 +0.236872485 +0.234759854 +0.227411978 +0.211911519 +0.210185772 +0.198295907 +0.149116222 +0.151737475 +0.159671877 +0.157178376 +0.15718786 +0.166671518 +0.186586216 +0.219041143 +0.260388585 +0.30866939 +0.356428776 +0.385497992 +0.435934519 +0.504138135 +0.545308106 +0.548059473 +0.516664675 +0.494683726 +0.479520036 +0.469140685 +0.467200709 +0.465123412 +0.446965011 +0.392900092 +0.424816468 +0.451664124 +0.458234757 +0.464784991 +0.470384602 +0.479805694 +0.483984297 +0.484149316 +0.480168129 +0.472383772 +0.462384892 +0.453718 +0.418855082 +0.402458422 +0.466912964 +0.435748527 +0.388591439 +0.34423881 +0.30513183 +0.281447095 +0.261570867 +0.243184379 +0.231783377 +0.126718781 +0.117893132 +0.113893577 +0.095939482 +0.075327468 +0.053857703 +0.043537137 +0.039928744 +0.039321904 +0.043561351 +0.053840216 +0.069728208 +0.088276927 +0.113317678 +0.220690305 +0.34003992 +0.361412809 +0.364110717 +0.329222509 +0.281868307 +0.244995897 +0.222602349 +0.205259547 +0.177032367 +0.11367011 +0.06217068 +0.076655893 +0.08796283 +0.095894518 +0.101654617 +0.10785989 +0.112390784 +0.120335233 +0.13536712 +0.155120633 +0.17270851 +0.175769573 +0.156384982 +0.205429847 +0.302252395 +0.29467992 +0.26284968 +0.222535177 +0.179017509 +0.150020673 +0.129663867 +0.118699163 +0.111194772 +0.072492743 +0.036613138 +0.046052913 +0.067845087 +0.08549964 +0.094213667 +0.089497361 +0.079310457 +0.071789027 +0.067995382 +0.068172623 +0.067357506 +0.059863519 +0.04889629 +0.072027622 +0.107909964 +0.121015563 +0.120459973 +0.11034026 +0.102413849 +0.094922695 +0.08991192 +0.092848246 +0.095357187 +0.069833401 +0.015955766 +0.014152608 +0.023781285 +0.037365529 +0.045447241 +0.045258635 +0.04390656 +0.042127699 +0.041048027 +0.039447878 +0.036789032 +0.035603376 +0.035261525 +0.046615453 +0.044324866 +0.038645763 +0.034929764 +0.03466408 +0.03468874 +0.036608989 +0.038721413 +0.038908531 +0.039625394 +0.054773001 +0.01560616 +0.006793038 +0.014570799 +0.019897364 +0.022866334 +0.021889172 +0.018263256 +0.013808111 +0.009273402 +0.006159607 +0.005467429 +0.007957182 +0.021096429 +0.092441009 +0.184121666 +0.245754956 +0.261755669 +0.241152776 +0.192339227 +0.145709316 +0.116356344 +0.09650362 +0.085498576 +0.080018149 +0.026015342 +0.012094761 +0.021161719 +0.025568244 +0.029559672 +0.028688716 +0.024685427 +0.021736105 +0.020745352 +0.022475938 +0.026917738 +0.033130097 +0.04992154 +0.103434437 +0.174050194 +0.242237821 +0.277585997 +0.32660645 +0.355653855 +0.362887693 +0.359601985 +0.35967653 +0.352229631 +0.316697103 +0.329160576 +0.358137095 +0.385350524 +0.410227016 +0.431341922 +0.44955341 +0.453671545 +0.441648337 +0.417882964 +0.38726852 +0.351935253 +0.304419375 +0.23522605 +0.188574387 +0.197779912 +0.201010585 +0.190220504 +0.1894891 +0.200997262 +0.207303008 +0.194665596 +0.181104064 +0.160083773 +0.068628605 +0.026849784 +0.051386113 +0.080282264 +0.109400351 +0.140141294 +0.165596838 +0.184271075 +0.19503707 +0.195332875 +0.184711814 +0.161206991 +0.13883684 +0.152318171 +0.192571813 +0.245690679 +0.288206363 +0.294490569 +0.281298602 +0.262979941 +0.239518626 +0.217505205 +0.19542684 +0.175829586 +0.112479393 +0.073743391 +0.09814692 +0.13079967 +0.151416327 +0.169750104 +0.190755667 +0.21015335 +0.232459229 +0.266647279 +0.314824039 +0.370311707 +0.415365817 +0.428278306 +0.479418193 +0.60825917 +0.628623246 +0.609229911 +0.561704705 +0.510045221 +0.46220554 +0.421718295 +0.382679527 +0.344424034 +0.146823961 +0.041973112 +0.072149591 +0.072452425 +0.065947129 +0.07906268 +0.102670879 +0.130002438 +0.147709542 +0.151009199 +0.138980027 +0.119467153 +0.089776391 +0.066921426 +0.07092154 +0.116804369 +0.170057313 +0.218514449 +0.287868906 +0.345448062 +0.367953113 +0.368636038 +0.354223739 +0.35128741 +0.286060109 +0.135847095 +0.137987612 +0.230963897 +0.288570626 +0.384064177 +0.462218548 +0.458348082 +0.43456685 +0.400032431 +0.365965026 +0.336243893 +0.308648411 +0.273564404 +0.243245966 +0.316066614 +0.353075448 +0.366860971 +0.369487226 +0.385254657 +0.387084963 +0.38347034 +0.374956302 +0.359006781 +0.241688919 +0.223128514 +0.339917643 +0.390384961 +0.409376692 +0.428448204 +0.443953779 +0.451756887 +0.450185608 +0.436189724 +0.411206424 +0.37494573 +0.323111987 +0.243129796 +0.229852685 +0.165569849 +0.128944243 +0.107285267 +0.104297559 +0.117735566 +0.137331443 +0.151941557 +0.157645225 +0.147748579 +0.117619587 +0.02199795 +0.020819904 +0.043594991 +0.053362127 +0.064138236 +0.082512345 +0.111818392 +0.145124275 +0.177030482 +0.206080031 +0.232090002 +0.239961329 +0.236962753 +0.257408715 +0.372955686 +0.430795262 +0.449715973 +0.43995974 +0.400955235 +0.379972939 +0.360483262 +0.345047525 +0.319879422 +0.224605745 +0.24076619 +0.317274088 +0.337402636 +0.347625347 +0.350467513 +0.354009315 +0.371534062 +0.383585114 +0.396964398 +0.414771722 +0.41989673 +0.383076971 +0.319814231 +0.321605459 +0.422039731 +0.472277194 +0.466693223 +0.462477531 +0.462345471 +0.464844624 +0.480973111 +0.481508511 +0.453680234 +0.28669591 +0.378186787 +0.456434197 +0.486788546 +0.525810539 +0.564726081 +0.608568876 +0.645076229 +0.666154812 +0.669980678 +0.659951435 +0.639305991 +0.603920878 +0.539114684 +0.478501939 +0.530888072 +0.504212112 +0.467031178 +0.413446136 +0.378712621 +0.357741621 +0.32737497 +0.306451656 +0.264317926 +0.129712327 +0.058791465 +0.04653742 +0.054003075 +0.066589279 +0.082450136 +0.115022937 +0.164938304 +0.216789434 +0.260615942 +0.286305558 +0.28374501 +0.250099609 +0.234735261 +0.297037817 +0.352815558 +0.339215442 +0.282248316 +0.243667529 +0.226082016 +0.218805669 +0.187540359 +0.161870007 +0.139648009 +0.064005252 +0.016642038 +0.010696662 +0.007792981 +0.012393971 +0.027687425 +0.054042085 +0.081845554 +0.104217098 +0.118030947 +0.120182417 +0.107030588 +0.076484588 +0.046371736 +0.143606066 +0.400735648 +0.557388539 +0.570030968 +0.535886002 +0.522736326 +0.505696293 +0.452807738 +0.416342602 +0.380890738 +0.288937306 +0.155158524 +0.132519052 +0.13057305 +0.126225966 +0.126904909 +0.140416202 +0.151718333 +0.15329741 +0.142995655 +0.125129656 +0.110086044 +0.100561222 +0.111913935 +0.193920536 +0.300508636 +0.325826517 +0.29988332 +0.324188786 +0.338617791 +0.338910584 +0.344444113 +0.327584555 +0.311412267 +0.172059812 +0.157624523 +0.196309368 +0.210807974 +0.208948024 +0.205943839 +0.196602206 +0.195141816 +0.198220312 +0.202409377 +0.219593339 +0.256802503 +0.313172526 +0.380507391 +0.406719163 +0.419869976 +0.43308817 +0.422391316 +0.397358015 +0.369082295 +0.339372804 +0.312600367 +0.289327467 +0.260551559 +0.238848616 +0.244537375 +0.259631359 +0.260268097 +0.248603898 +0.234456878 +0.207607285 +0.180317109 +0.159243787 +0.138559324 +0.116741463 +0.096789176 +0.083481339 +0.073898891 +0.091237583 +0.105343669 +0.097071076 +0.089395994 +0.08460664 +0.092845008 +0.115880174 +0.135766705 +0.149146871 +0.154942575 +0.117587091 +0.033218203 +0.021275271 +0.061532729 +0.094184668 +0.097245857 +0.097624008 +0.089502112 +0.070919054 +0.056114846 +0.053626834 +0.057383186 +0.058903272 +0.070167109 +0.138076013 +0.249317387 +0.346875082 +0.370093022 +0.345776337 +0.303093449 +0.25301499 +0.21493435 +0.207754044 +0.212821358 +0.126126206 +0.072272223 +0.136120621 +0.22341849 +0.286128736 +0.317341479 +0.336252209 +0.355304378 +0.370670223 +0.383222058 +0.393533333 +0.405456158 +0.413861712 +0.406831002 +0.376791612 +0.38675519 +0.373249942 +0.363856558 +0.369387392 +0.378238053 +0.363227996 +0.360429381 +0.361178493 +0.341471357 +0.202458911 +0.225682911 +0.372836682 +0.444816037 +0.48552424 +0.516893331 +0.548520817 +0.566912353 +0.572407893 +0.565276117 +0.543570976 +0.503782565 +0.441331007 +0.358930775 +0.34583244 +0.406344041 +0.399656001 +0.364563863 +0.334500364 +0.319066865 +0.301430749 +0.268780215 +0.240121367 +0.217671709 +0.120409832 +0.038504206 +0.064991839 +0.069818222 +0.05567662 +0.048371169 +0.054767559 +0.07036316 +0.087324081 +0.095501485 +0.092930452 +0.084419391 +0.080550883 +0.089561499 +0.130503369 +0.217279085 +0.287925175 +0.337452068 +0.354002049 +0.360235616 +0.362310713 +0.343492977 +0.312790017 +0.296488341 +0.176707542 +0.080528459 +0.120962905 +0.229086559 +0.278962636 +0.315176826 +0.35571289 +0.386968813 +0.397812205 +0.388310924 +0.366201 +0.340058743 +0.303408193 +0.260259496 +0.301445261 +0.366750966 +0.37947066 +0.360734014 +0.328452422 +0.295873029 +0.254312133 +0.206489169 +0.163347891 +0.134098037 +0.044362683 +0.01499769 +0.01620478 +0.034649565 +0.064664583 +0.096390555 +0.112453537 +0.109593816 +0.101578263 +0.09737536 +0.098315106 +0.096650841 +0.09244216 +0.109485446 +0.150369578 +0.184849669 +0.198916286 +0.192694113 +0.167761453 +0.1464572 +0.142985949 +0.137627472 +0.134845812 +0.132623917 +0.127921956 +0.082114855 +0.10237905 +0.138587533 +0.162124403 +0.163876221 +0.132055381 +0.091713997 +0.061777932 +0.042510765 +0.031029599 +0.02579604 +0.030665476 +0.048289152 +0.0961643 +0.203834569 +0.268718916 +0.253705455 +0.198434796 +0.147239079 +0.112096266 +0.092344797 +0.081118814 +0.080968212 +0.037259233 +0.008946851 +0.010075852 +0.007536445 +0.005120083 +0.003118796 +0.001849406 +0.001538258 +0.002071096 +0.003757896 +0.006994286 +0.012092868 +0.018471361 +0.02526333 +0.045823103 +0.080741695 +0.092039803 +0.088192522 +0.081006101 +0.07686735 +0.07901104 +0.084526158 +0.091861363 +0.101811938 +0.069693496 +0.011081828 +0.019276562 +0.033688827 +0.037696342 +0.042769761 +0.043029215 +0.039103878 +0.035011843 +0.032328101 +0.033703075 +0.041084539 +0.054279261 +0.08247668 +0.181207069 +0.403157039 +0.492743071 +0.488563607 +0.470166621 +0.441269407 +0.398480595 +0.358781108 +0.317366985 +0.269891817 +0.158219926 +0.15924358 +0.223459282 +0.248143636 +0.268596816 +0.298674267 +0.322608396 +0.335982515 +0.341768873 +0.339662256 +0.323405225 +0.299035004 +0.260720386 +0.219873698 +0.184010232 +0.22904429 +0.26327555 +0.263517902 +0.266582661 +0.263777202 +0.250361255 +0.248254821 +0.236395367 +0.193500155 +0.0811918 +0.034656662 +0.049765547 +0.041718765 +0.040170853 +0.047791538 +0.06080388 +0.070566076 +0.074082719 +0.073695248 +0.07102899 +0.068886376 +0.064590256 +0.061843561 +0.052294393 +0.047382475 +0.052253459 +0.054458442 +0.048966119 +0.048749825 +0.056687919 +0.065276034 +0.073892608 +0.088456495 +0.096313316 +0.018749404 +0.017979311 +0.027300288 +0.037224658 +0.04910066 +0.062250461 +0.085308997 +0.115632511 +0.144214307 +0.166930594 +0.184655515 +0.18981546 +0.187597514 +0.179504721 +0.242867817 +0.295459767 +0.27884828 +0.270000948 +0.251880478 +0.223496519 +0.203002516 +0.193683554 +0.170479553 +0.078315743 +0.080311089 +0.086659458 +0.071428636 +0.052596421 +0.038638436 +0.030509846 +0.025986199 +0.023374613 +0.022545736 +0.022680083 +0.024511911 +0.031930991 +0.064537526 +0.172790459 +0.27669564 +0.296802871 +0.259391583 +0.206948697 +0.176528932 +0.160185569 +0.158697655 +0.149525053 +0.142758261 +0.131340675 +0.121526943 +0.124708885 +0.127395546 +0.133483772 +0.144424959 +0.139543332 +0.11798568 +0.097631384 +0.085235881 +0.081418409 +0.081342813 +0.085071074 +0.092885167 +0.118129813 +0.186813803 +0.203557534 +0.190189664 +0.169962061 +0.149573164 +0.134225478 +0.118827662 +0.105157886 +0.088771773 +0.051886869 +0.04506915 +0.042268645 +0.037303476 +0.037708606 +0.045053858 +0.050660881 +0.051500002 +0.047039627 +0.040017835 +0.034997136 +0.029877089 +0.028676594 +0.040657669 +0.082255171 +0.132912138 +0.182699404 +0.217094269 +0.206261048 +0.186449149 +0.173675738 +0.172690116 +0.176262577 +0.157872315 +0.067955251 +0.029552196 +0.036516165 +0.057914824 +0.072074164 +0.078370966 +0.077164852 +0.065020541 +0.049393308 +0.03861643 +0.034329703 +0.034979148 +0.034566146 +0.041921072 +0.09336724 +0.165090061 +0.207837228 +0.210004159 +0.196566388 +0.190188688 +0.185756161 +0.180346789 +0.182660065 +0.152911993 +0.065654666 +0.041504824 +0.076949049 +0.144402329 +0.188902019 +0.20876949 +0.201893287 +0.180925707 +0.166449014 +0.156388518 +0.154977829 +0.155293174 +0.150801734 +0.146441027 +0.162107661 +0.253555347 +0.283853014 +0.273081983 +0.253738683 +0.233476342 +0.219004849 +0.212748556 +0.219024199 +0.213570918 +0.11804877 +0.06261284 +0.09497312 +0.155787717 +0.189714874 +0.205546163 +0.217996501 +0.220315853 +0.211040804 +0.196743329 +0.179909349 +0.162930322 +0.144556588 +0.112380337 +0.089014193 +0.134054748 +0.173821261 +0.165521326 +0.14062616 +0.116209668 +0.099144423 +0.09481865 +0.098479791 +0.111741991 +0.08162747 +0.020180828 +0.036295186 +0.084053378 +0.122390577 +0.141987595 +0.1399322 +0.126926016 +0.110143556 +0.093930205 +0.080517542 +0.069345527 +0.061632703 +0.055586894 +0.048605085 +0.034399434 +0.041551205 +0.052724905 +0.058166764 +0.060359506 +0.070586032 +0.085180722 +0.09845203 +0.113660646 +0.117089104 +0.016199728 +0.003679728 +0.009282529 +0.012482737 +0.01472546 +0.014767695 +0.013389812 +0.01169269 +0.010838089 +0.01003866 +0.009883193 +0.012925603 +0.02012847 +0.043416908 +0.087567197 +0.137626461 +0.184652804 +0.185950111 +0.176656501 +0.167044761 +0.158771471 +0.148354189 +0.13477359 +0.050394552 +0.003472504 +0.001730593 +0.001398169 +0.000498145 +0.000178588 +0.000299783 +0.001159612 +0.00244333 +0.00383948 +0.005158084 +0.006551638 +0.009785752 +0.015307352 +0.036545541 +0.056642812 +0.079965701 +0.091896405 +0.085419661 +0.072658007 +0.066170271 +0.068198973 +0.070826289 +0.074721633 +0.042814115 +0.008828865 +0.010797774 +0.013103493 +0.015121131 +0.020611559 +0.03141235 +0.046269008 +0.064706024 +0.086766984 +0.107784586 +0.125784632 +0.124233919 +0.117002745 +0.142652697 +0.222365638 +0.25333776 +0.238441138 +0.206020114 +0.186825764 +0.187498417 +0.207506666 +0.226857032 +0.206641146 +0.135214589 +0.141827974 +0.178251754 +0.202666914 +0.216983845 +0.225975348 +0.225155745 +0.216598432 +0.206311614 +0.195519361 +0.186732199 +0.181503821 +0.17658443 +0.16816773 +0.205950973 +0.262001785 +0.297974233 +0.286926249 +0.267568985 +0.252440477 +0.243364921 +0.239581864 +0.229417436 +0.210197393 +0.091096558 +0.015016556 +0.015738598 +0.024781728 +0.030550188 +0.03443035 +0.035748455 +0.03703364 +0.040474084 +0.045879647 +0.052866927 +0.05995338 +0.066905796 +0.073990621 +0.080607126 +0.073160161 +0.066527043 +0.056387188 +0.046066001 +0.043147876 +0.047629181 +0.064639536 +0.087222253 +0.113361462 +0.11712299 +0.015499883 +0.004542315 +0.012021911 +0.011506015 +0.012246486 +0.013942809 +0.01756456 +0.022312745 +0.02680108 +0.029853474 +0.031854351 +0.039421434 +0.049136253 +0.077966252 +0.139182712 +0.220690621 +0.28335334 +0.296227932 +0.301346547 +0.312048619 +0.316107562 +0.314177872 +0.298457325 +0.166691905 +0.028875897 +0.015525681 +0.035621433 +0.041932554 +0.046482648 +0.059190597 +0.079017268 +0.097891725 +0.110741073 +0.118087878 +0.12137442 +0.124232781 +0.121038824 +0.208222187 +0.329702018 +0.399836506 +0.407543214 +0.393047189 +0.361178168 +0.317738444 +0.278966701 +0.249734069 +0.215299294 +0.082875049 +0.016179824 +0.017696735 +0.018038776 +0.016738247 +0.014617648 +0.013252999 +0.01403236 +0.015350861 +0.017750353 +0.022241082 +0.031549888 +0.045345374 +0.071237194 +0.13081786 +0.176573204 +0.197028109 +0.186698243 +0.168518188 +0.156902145 +0.146007174 +0.134482694 +0.121038631 +0.110685289 +0.057777193 +0.012583975 +0.012245311 +0.017230562 +0.026080778 +0.036578697 +0.046282971 +0.055761402 +0.064001411 +0.068431644 +0.070633483 +0.073704777 +0.079487568 +0.081725323 +0.118247303 +0.189545576 +0.250783988 +0.285234806 +0.311462995 +0.323396109 +0.29961279 +0.275976222 +0.250898805 +0.207217524 +0.083536366 +0.022458895 +0.015187812 +0.01731536 +0.024256472 +0.035280525 +0.052060662 +0.076449355 +0.103722573 +0.125589487 +0.141211251 +0.148611856 +0.145917447 +0.150616833 +0.18434764 +0.222112828 +0.239620152 +0.229402442 +0.223215266 +0.209167335 +0.197792646 +0.183317209 +0.166106032 +0.137506963 +0.101057108 +0.093318238 +0.078773345 +0.065168093 +0.059897136 +0.057088584 +0.061579493 +0.071740055 +0.080982142 +0.089235125 +0.096273682 +0.102681428 +0.110809312 +0.121741168 +0.138030311 +0.227180206 +0.301971672 +0.324495595 +0.32119949 +0.304171734 +0.283179912 +0.263276503 +0.25549961 +0.228864557 +0.101932239 +0.099981777 +0.15517225 +0.174236914 +0.174869731 +0.174469796 +0.18311901 +0.21092994 +0.252927665 +0.298812656 +0.337956391 +0.3693342 +0.390383583 +0.380836669 +0.357881019 +0.47381533 +0.525542199 +0.510504889 +0.455338295 +0.407212547 +0.367845931 +0.336574164 +0.303526638 +0.258719367 +0.161817564 +0.187885178 +0.20109222 +0.19708596 +0.193617184 +0.185807632 +0.187740353 +0.19580104 +0.203149093 +0.206060623 +0.204482602 +0.202734569 +0.192795209 +0.174623173 +0.196095971 +0.22301416 +0.229586787 +0.220083653 +0.213904054 +0.207796531 +0.188763464 +0.170193046 +0.146155233 +0.121975309 +0.036647647 +0.003764463 +0.005472707 +0.008920885 +0.010826243 +0.012340218 +0.013737488 +0.018078558 +0.026750988 +0.038889323 +0.052853128 +0.065227602 +0.073294775 +0.075186742 +0.126178485 +0.188825341 +0.220186149 +0.222133495 +0.230112005 +0.245333204 +0.248885456 +0.243956061 +0.235253017 +0.203224062 +0.099605804 +0.090873499 +0.117888404 +0.119336003 +0.109136819 +0.102316007 +0.112680145 +0.137295493 +0.165931141 +0.190916934 +0.209402299 +0.220916612 +0.232546661 +0.229837653 +0.252379556 +0.320492278 +0.373200527 +0.355517611 +0.320986769 +0.287642809 +0.252006507 +0.224218745 +0.207374421 +0.172545895 +0.068535279 +0.057886929 +0.072146973 +0.086247164 +0.106623986 +0.126938358 +0.144433439 +0.161413437 +0.191870293 +0.234441019 +0.282426702 +0.325082372 +0.344072356 +0.335359705 +0.343504545 +0.514127098 +0.558708446 +0.538724215 +0.501999257 +0.455318449 +0.423863519 +0.391227298 +0.377729734 +0.355780037 +0.268465193 +0.269315358 +0.365419519 +0.428458017 +0.467696577 +0.485150766 +0.474879462 +0.466569834 +0.468134361 +0.456549627 +0.430137341 +0.39589131 +0.352704028 +0.320192518 +0.350234314 +0.421589305 +0.439645243 +0.411281187 +0.372259622 +0.331084338 +0.314826832 +0.298387842 +0.283461651 +0.265349893 +0.2115788 +0.16904644 +0.158298827 +0.15825588 +0.185878938 +0.201413966 +0.185026645 +0.149763657 +0.126239639 +0.116199055 +0.114296103 +0.115882728 +0.113296726 +0.1337354 +0.234688156 +0.336876465 +0.359261775 +0.331746332 +0.288043592 +0.255540404 +0.246515908 +0.26273352 +0.281997676 +0.287620624 +0.131146608 +0.047002076 +0.060083125 +0.147001925 +0.1882663 +0.203914841 +0.211592649 +0.21858239 +0.230675055 +0.244340843 +0.256273888 +0.266584048 +0.26484243 +0.261298708 +0.331392817 +0.400091146 +0.428479109 +0.411072275 +0.374072041 +0.350896184 +0.355356141 +0.365591759 +0.363690179 +0.351172013 +0.17650894 +0.032846586 +0.025939707 +0.086267717 +0.11684662 +0.133876329 +0.154931516 +0.181135414 +0.201025946 +0.214552468 +0.227278654 +0.238430372 +0.22972348 +0.203669686 +0.226924568 +0.272451097 +0.291823823 +0.290819071 +0.270530228 +0.263614081 +0.256840952 +0.249805137 +0.254219186 +0.2563526 +0.1210259 +0.024356171 +0.041140912 +0.059149387 +0.067007613 +0.070827695 +0.07394311 +0.079223286 +0.090437996 +0.107963266 +0.126376573 +0.138625549 +0.140922778 +0.134383389 +0.183850359 +0.247549513 +0.265486136 +0.255591109 +0.18690206 +0.12573599 +0.103892285 +0.102334999 +0.116067224 +0.135420346 +0.072167626 +0.009147187 +0.011793624 +0.014452281 +0.010992087 +0.007053221 +0.004763256 +0.005367886 +0.008949716 +0.014735771 +0.021160314 +0.027105932 +0.034923872 +0.045584217 +0.077194988 +0.101263072 +0.096596899 +0.077510485 +0.062747928 +0.049641408 +0.041688082 +0.03854871 +0.041375057 +0.047335887 +0.030460565 +0.00861048 +0.013321429 +0.022037829 +0.026224111 +0.025993784 +0.018942367 +0.012263161 +0.010058404 +0.010595619 +0.012713832 +0.015961246 +0.020905422 +0.027406442 +0.04612893 +0.073961195 +0.088748725 +0.083725726 +0.079033797 +0.073941554 +0.068164342 +0.065283197 +0.067244432 +0.076468561 +0.057373962 +0.007061293 +0.013762892 +0.023751076 +0.032000877 +0.037322747 +0.034470948 +0.026391426 +0.019423449 +0.01475343 +0.012208203 +0.011346734 +0.015459238 +0.026543152 +0.068514073 +0.124367457 +0.170347036 +0.175046836 +0.15285947 +0.125398221 +0.105391456 +0.094806622 +0.09025957 +0.093182228 +0.061967757 +0.014014508 +0.035351996 +0.069562457 +0.090797384 +0.10192842 +0.090597278 +0.072358237 +0.060517694 +0.054215085 +0.053015477 +0.056655997 +0.074657646 +0.101643076 +0.163570082 +0.310503835 +0.357130425 +0.347080484 +0.30781709 +0.264527361 +0.217537014 +0.180681559 +0.15409998 +0.145882671 +0.076235949 +0.030484309 +0.078548704 +0.116313882 +0.142244679 +0.161098835 +0.16896972 +0.167270433 +0.16284179 +0.157804154 +0.1505111 +0.145046821 +0.148460693 +0.163282143 +0.265057529 +0.469803684 +0.551789757 +0.551199911 +0.50467349 +0.438788533 +0.378247956 +0.341586908 +0.30289597 +0.273830064 +0.179046898 +0.15014698 +0.218465234 +0.283725857 +0.326224015 +0.350111341 +0.363750961 +0.370637857 +0.368953789 +0.35428791 +0.33373484 +0.315422614 +0.323345101 +0.331041473 +0.367702071 +0.470632133 +0.494485278 +0.464285645 +0.409615513 +0.37432557 +0.361529555 +0.331878843 +0.296426487 +0.273615441 +0.158571235 +0.101084241 +0.168435926 +0.199123346 +0.220991082 +0.232855512 +0.240735647 +0.242116269 +0.234730544 +0.217243028 +0.191064939 +0.172762496 +0.147614632 +0.13300855 +0.144335854 +0.198673884 +0.216561891 +0.20444259 +0.250503695 +0.243723262 +0.225466177 +0.191529276 +0.153004934 +0.134667664 +0.080360193 +0.027044372 +0.027037712 +0.039818417 +0.034559331 +0.027044147 +0.027344589 +0.034276528 +0.04714716 +0.061698952 +0.074600852 +0.087087522 +0.097065009 +0.105080706 +0.172993277 +0.283980177 +0.357575818 +0.377733727 +0.36932856 +0.359679776 +0.355667993 +0.355596672 +0.346475187 +0.339469968 +0.215849418 +0.120376922 +0.152403755 +0.188719539 +0.206587846 +0.210460428 +0.20885829 +0.211079544 +0.220898472 +0.237932242 +0.255314546 +0.266083384 +0.261481975 +0.242783929 +0.262092679 +0.332974231 +0.364615326 +0.35672541 +0.344520346 +0.327981911 +0.311045085 +0.295085378 +0.289233026 +0.258140114 +0.111643767 +0.075949334 +0.13592832 +0.136131718 +0.124578596 +0.119774691 +0.119578439 +0.123007628 +0.133509657 +0.151894688 +0.172265652 +0.194781537 +0.207207444 +0.203340861 +0.298982436 +0.46513582 +0.513897665 +0.477948941 +0.441855175 +0.397374399 +0.357943168 +0.331517273 +0.305081871 +0.27169007 +0.134749218 +0.035317161 +0.035627514 +0.04370026 +0.045302434 +0.045724199 +0.045966908 +0.044814091 +0.042042165 +0.039353465 +0.037031594 +0.035102537 +0.031733877 +0.023934155 +0.013879402 +0.010796864 +0.009432779 +0.016119073 +0.031396851 +0.049265587 +0.071584298 +0.086693692 +0.09096989 +0.088233186 +0.077670222 +0.019261225 +0.002421732 +0.006600743 +0.008005573 +0.00819183 +0.007400121 +0.006536402 +0.00658269 +0.007681272 +0.010546883 +0.016241191 +0.026771024 +0.04794533 +0.121521343 +0.233884249 +0.319559024 +0.341183212 +0.329959278 +0.31210588 +0.29715324 +0.277532622 +0.260807749 +0.248168596 +0.194024363 +0.05360516 +0.041060776 +0.094460125 +0.128675126 +0.141605989 +0.137431758 +0.120637994 +0.100620981 +0.080942926 +0.065260609 +0.058553667 +0.068867004 +0.113478313 +0.316523144 +0.51250663 +0.554380873 +0.506831534 +0.460661116 +0.423448073 +0.389049074 +0.344740579 +0.307481453 +0.270813709 +0.144867577 +0.065330575 +0.057513411 +0.082013399 +0.103108264 +0.121161058 +0.134626565 +0.151151443 +0.166855884 +0.170810344 +0.161294934 +0.142984883 +0.127102802 +0.14082458 +0.166334911 +0.166903697 +0.159101195 +0.149737 +0.147528085 +0.158611137 +0.175256373 +0.153215925 +0.125126882 +0.112135928 +0.054647796 +0.014485837 +0.018076161 +0.019293809 +0.024370786 +0.039425552 +0.060855431 +0.081605438 +0.102049055 +0.12271668 +0.141218329 +0.158006641 +0.159913807 +0.146472406 +0.192026878 +0.278856094 +0.304894258 +0.291169149 +0.287296778 +0.278748332 +0.251837969 +0.238836026 +0.22657138 +0.213381356 +0.122443551 +0.099795678 +0.137527146 +0.181105712 +0.212052886 +0.241404173 +0.270149718 +0.297438919 +0.320289306 +0.339762012 +0.34790424 +0.33563057 +0.296617265 +0.233305111 +0.255813754 +0.308946487 +0.299501673 +0.276432982 +0.267274744 +0.253345942 +0.228095036 +0.205515644 +0.183052477 +0.160168092 +0.085117235 +0.029557089 +0.034194753 +0.049374354 +0.055690053 +0.055190951 +0.053231144 +0.049713693 +0.045085813 +0.038617098 +0.029611105 +0.020456188 +0.013365042 +0.013752839 +0.031189771 +0.077937357 +0.160897472 +0.25318242 +0.283980934 +0.273049309 +0.266646317 +0.267313466 +0.257689044 +0.244940137 +0.228510159 +0.133864237 +0.122756152 +0.171959479 +0.220340187 +0.289492232 +0.346122705 +0.372770379 +0.364746528 +0.338053641 +0.301711144 +0.258717529 +0.209035851 +0.174711496 +0.26656012 +0.361816759 +0.387811059 +0.358611333 +0.339555978 +0.325803485 +0.324160458 +0.290490881 +0.268168539 +0.256423932 +0.199955761 +0.1820033 +0.254948277 +0.335490737 +0.393778376 +0.430508448 +0.469274902 +0.509322009 +0.536373732 +0.552075052 +0.559737956 +0.559462306 +0.540968028 +0.493897617 +0.495032713 +0.58893697 +0.582597725 +0.550615119 +0.532587986 +0.519780801 +0.511387589 +0.501715818 +0.506233166 +0.501370232 +0.40093054 +0.365129131 +0.505890094 +0.537229651 +0.550861173 +0.559970345 +0.565356615 +0.553818453 +0.527889545 +0.491161994 +0.444234352 +0.386804999 +0.311586663 +0.245970489 +0.286804476 +0.348602721 +0.336479259 +0.297974273 +0.27004907 +0.240634791 +0.216664619 +0.204976195 +0.192047884 +0.17114017 +0.097698696 +0.022165085 +0.026264727 +0.044872342 +0.057357147 +0.069786323 +0.082943337 +0.096242651 +0.108220626 +0.116872604 +0.122528485 +0.125021955 +0.124213528 +0.123366995 +0.199087315 +0.337070112 +0.339337133 +0.324693226 +0.297070711 +0.267932581 +0.238408787 +0.221682268 +0.201966426 +0.185252356 +0.125739147 +0.124033881 +0.188288628 +0.23730677 +0.270038853 +0.290664423 +0.300108014 +0.298913188 +0.292471005 +0.281684063 +0.264035926 +0.238151734 +0.199640433 +0.160192353 +0.21764989 +0.279297509 +0.292186054 +0.279160037 +0.258516462 +0.2387385 +0.220808944 +0.20720699 +0.20083875 +0.194679955 +0.129842677 +0.02660814 +0.022510113 +0.027099448 +0.024996457 +0.023578151 +0.023189073 +0.024410133 +0.027790875 +0.03375175 +0.042470572 +0.051960299 +0.061923604 +0.080768702 +0.142923978 +0.196023818 +0.211477613 +0.213676313 +0.213146251 +0.218555959 +0.22279451 +0.2237451 +0.214331214 +0.198785495 +0.123408752 +0.023648369 +0.019167122 +0.02947936 +0.032401373 +0.030566559 +0.028338864 +0.027938114 +0.029241985 +0.031209664 +0.032932969 +0.033228107 +0.030536763 +0.031001423 +0.03022504 +0.031552811 +0.042756678 +0.06255647 +0.083096602 +0.102163074 +0.114551368 +0.120533355 +0.124113929 +0.124883645 +0.110407797 +0.015832291 +0.003363496 +0.008546933 +0.010050838 +0.013514634 +0.017870085 +0.022275579 +0.025346829 +0.025950496 +0.024773134 +0.021782622 +0.018532592 +0.018143009 +0.024135424 +0.059732803 +0.143978422 +0.260692455 +0.335523008 +0.34909354 +0.315901589 +0.263070694 +0.218968201 +0.195350423 +0.161682593 +0.032626563 +0.00981876 +0.039129273 +0.061650693 +0.07530601 +0.088117457 +0.099291548 +0.109314258 +0.12007 +0.134195105 +0.148473808 +0.155315293 +0.222083713 +0.38149866 +0.478763613 +0.438666426 +0.383357166 +0.346965388 +0.329232404 +0.304143755 +0.278207261 +0.27155635 +0.269523967 +0.226841704 +0.114733817 +0.089100448 +0.168630993 +0.239780551 +0.298316297 +0.349276706 +0.38684677 +0.398595917 +0.400248923 +0.388546708 +0.359965066 +0.324417843 +0.314478577 +0.355372431 +0.354410712 +0.301610016 +0.277859208 +0.270724805 +0.257496552 +0.241239889 +0.22226084 +0.210106737 +0.199166061 +0.150764877 +0.105447428 +0.113827092 +0.127394799 +0.14492254 +0.150124407 +0.154340229 +0.160943829 +0.161444698 +0.156997265 +0.14647718 +0.140797621 +0.138536082 +0.145555162 +0.201210855 +0.260355645 +0.278656357 +0.289751779 +0.268557397 +0.245665348 +0.22712093 +0.20961106 +0.19973437 +0.187808561 +0.175190256 +0.159105775 +0.220696397 +0.273963904 +0.328127789 +0.383466186 +0.435937189 +0.476233686 +0.506052768 +0.524481769 +0.533538786 +0.532560805 +0.515129607 +0.490640896 +0.571094991 +0.653637519 +0.637281134 +0.603716528 +0.511024749 +0.431488527 +0.398137807 +0.364195514 +0.332674079 +0.303791316 +0.233064517 +0.118869321 +0.182763984 +0.21011369 +0.239105498 +0.266293574 +0.285046285 +0.294618818 +0.301262778 +0.305501921 +0.305981692 +0.297472615 +0.273197013 +0.225848993 +0.25312981 +0.252247846 +0.241109231 +0.218781443 +0.189603859 +0.174295155 +0.178606001 +0.182498759 +0.178309172 +0.164959026 +0.130143023 +0.025432466 +0.033227022 +0.064877052 +0.073971204 +0.077052003 +0.080277508 +0.084794056 +0.092704686 +0.104387904 +0.117146305 +0.129892736 +0.13676594 +0.141907141 +0.190196845 +0.2260824 +0.201503743 +0.172027776 +0.138443266 +0.114045069 +0.108843222 +0.121012447 +0.110027663 +0.087590288 +0.070551535 +0.037854883 +0.036241258 +0.042437962 +0.048747357 +0.055250363 +0.05817596 +0.058897084 +0.06107344 +0.065375664 +0.06918656 +0.071467492 +0.074254177 +0.108223594 +0.182421321 +0.232999359 +0.246087363 +0.233144033 +0.187920386 +0.139558926 +0.104548243 +0.085324943 +0.079918509 +0.081458647 +0.083564736 +0.041956021 +0.011712488 +0.023548921 +0.029559221 +0.029129886 +0.025458109 +0.021310788 +0.017453996 +0.014871673 +0.013713442 +0.013478648 +0.012506423 +0.018086323 +0.029963801 +0.062642471 +0.114593064 +0.177850667 +0.224935882 +0.256521204 +0.259355153 +0.239255547 +0.203999321 +0.171919518 +0.136508427 +0.032001523 +0.002001253 +0.001584204 +0.003579003 +0.003005604 +0.002613108 +0.002579877 +0.003051488 +0.003846605 +0.004675517 +0.005618937 +0.008691387 +0.026793158 +0.08127709 +0.158734914 +0.236053912 +0.292577068 +0.303086801 +0.294090483 +0.284141131 +0.270661722 +0.251925238 +0.236270087 +0.225759319 +0.069691793 +0.010038437 +0.016991885 +0.025479742 +0.023152483 +0.021164541 +0.021205896 +0.022627751 +0.023901925 +0.023731971 +0.022376624 +0.023448451 +0.043415985 +0.094432837 +0.140662428 +0.150145616 +0.148114956 +0.139345714 +0.131976237 +0.123993548 +0.113157244 +0.106197758 +0.102078659 +0.088643902 +0.026101144 +0.001652455 +0.006413391 +0.012394596 +0.017809791 +0.020891103 +0.022057974 +0.023252972 +0.024190515 +0.024469586 +0.024357946 +0.023540461 +0.025582583 +0.039806741 +0.066489928 +0.093054129 +0.116811802 +0.130416915 +0.148195555 +0.165888026 +0.169180912 +0.163729757 +0.160463262 +0.16206107 +0.039347152 +0.003955625 +0.009818809 +0.019466886 +0.02869962 +0.042444681 +0.05792713 +0.068076702 +0.068826022 +0.066501111 +0.065285472 +0.077036641 +0.113076438 +0.150606061 +0.202616491 +0.237176264 +0.252786154 +0.258372324 +0.243431577 +0.246579721 +0.25807127 +0.248410865 +0.251542671 +0.230513942 +0.115300306 +0.122284291 +0.137075755 +0.152892814 +0.178552752 +0.223651206 +0.267588727 +0.295436109 +0.309181335 +0.31399255 +0.311464875 +0.305124615 +0.313013905 +0.455621264 +0.530303383 +0.501917739 +0.464638662 +0.429145511 +0.380574299 +0.32847573 +0.278067343 +0.241236533 +0.229374867 +0.191194632 +0.052952145 +0.034812425 +0.077491615 +0.085517121 +0.090362795 +0.096483371 +0.107585601 +0.123313143 +0.135852222 +0.13889726 +0.127848181 +0.110462537 +0.116401295 +0.112812601 +0.123342589 +0.139567307 +0.163731483 +0.196667235 +0.227802822 +0.25873155 +0.277355721 +0.287397383 +0.290926143 +0.264194269 +0.087046891 +0.0072687 +0.022294114 +0.043518421 +0.047581612 +0.051713099 +0.057637073 +0.065927294 +0.07678708 +0.092490445 +0.115370107 +0.14664204 +0.263122548 +0.423051125 +0.507010497 +0.528666594 +0.545719307 +0.53360813 +0.515317166 +0.477277714 +0.432533131 +0.405321709 +0.390956414 +0.351945108 +0.210597773 +0.155490053 +0.242081294 +0.273622122 +0.272849153 +0.275133898 +0.271407236 +0.27032469 +0.283845423 +0.305088585 +0.321912101 +0.323438017 +0.34363857 +0.376322032 +0.387118401 +0.367800755 +0.348701248 +0.33116272 +0.329810284 +0.316080628 +0.287874826 +0.27466448 +0.258085889 +0.234933108 +0.182050641 +0.197680188 +0.248696739 +0.252438241 +0.237379163 +0.2273874 +0.221043609 +0.215716856 +0.212509275 +0.209891753 +0.202451711 +0.196413226 +0.218735334 +0.329354631 +0.401614786 +0.386936381 +0.363067449 +0.311786112 +0.259988043 +0.231508745 +0.209301073 +0.186674715 +0.17227585 +0.161300644 +0.064752273 +0.041531 +0.062191292 +0.066124786 +0.058491509 +0.055169904 +0.059019033 +0.063899144 +0.069767884 +0.075020313 +0.075374105 +0.065001919 +0.066395377 +0.070185962 +0.063586734 +0.049870113 +0.039861578 +0.033157684 +0.040017991 +0.058832619 +0.073770472 +0.082769514 +0.083110997 +0.080846375 +0.035877693 +0.006847529 +0.02993223 +0.047612193 +0.06272377 +0.074978333 +0.079848132 +0.076987283 +0.07117419 +0.065372769 +0.058169336 +0.058853341 +0.081291315 +0.151999518 +0.196404859 +0.201853346 +0.181687741 +0.158040909 +0.146944106 +0.139091292 +0.135407803 +0.137152115 +0.14390588 +0.126809202 +0.039278568 +0.030726525 +0.044196104 +0.044427624 +0.042274518 +0.043258617 +0.044177438 +0.04203812 +0.038841928 +0.036055118 +0.033254269 +0.029176182 +0.038000578 +0.039969915 +0.045707853 +0.047420951 +0.04485767 +0.046471098 +0.052454323 +0.062004931 +0.068695774 +0.068401061 +0.067271834 +0.071001036 +0.053548213 +0.00675327 +0.008789921 +0.021132477 +0.024138309 +0.025897946 +0.027598978 +0.029389641 +0.030955491 +0.031273807 +0.029992015 +0.027704541 +0.043244842 +0.100066735 +0.219706481 +0.350907378 +0.454300911 +0.531150013 +0.555849516 +0.529647471 +0.484882135 +0.424003675 +0.366755752 +0.323639237 +0.189846298 +0.044035143 +0.069484028 +0.180529082 +0.224268895 +0.229899728 +0.21763246 +0.199375225 +0.182040596 +0.166039904 +0.154266591 +0.150129529 +0.273234825 +0.45390982 +0.599054719 +0.646207095 +0.632870464 +0.584946077 +0.558941032 +0.530799599 +0.511113099 +0.494245439 +0.473145375 +0.435232343 +0.236481171 +0.113458733 +0.140090732 +0.20200004 +0.26266471 +0.307930254 +0.33962471 +0.365286639 +0.38085272 +0.379716207 +0.364374357 +0.366875249 +0.464511857 +0.520940365 +0.503952467 +0.476083674 +0.43936553 +0.393986718 +0.348250854 +0.302362787 +0.261925463 +0.239617135 +0.229982517 +0.214433775 +0.166449093 +0.177667909 +0.179148204 +0.189235057 +0.207895509 +0.223421586 +0.239393822 +0.273146391 +0.315680409 +0.334028712 +0.330013472 +0.32865843 +0.338613897 +0.433833898 +0.471171732 +0.486790567 +0.480048899 +0.43847027 +0.375757876 +0.331612122 +0.301974754 +0.279527557 +0.260533958 +0.220936143 +0.09259226 +0.113455767 +0.125287762 +0.12608379 +0.125566519 +0.13199076 +0.141059905 +0.147475083 +0.151083639 +0.150670633 +0.142949264 +0.122231914 +0.138736784 +0.148865674 +0.168280557 +0.175421143 +0.168778019 +0.163263776 +0.153984101 +0.141491696 +0.134168479 +0.136393682 +0.1358009 +0.131715174 +0.075702497 +0.006744969 +0.005469663 +0.004024101 +0.002873582 +0.003101095 +0.003966318 +0.005770614 +0.008109684 +0.010439009 +0.01165069 +0.010671618 +0.009017814 +0.016117155 +0.051029065 +0.130808879 +0.228492471 +0.311597559 +0.33163039 +0.321255337 +0.29895259 +0.27461725 +0.256462165 +0.235674661 +0.16331856 +0.027863938 +0.014876094 +0.052898464 +0.058328788 +0.053433648 +0.045166855 +0.036831254 +0.029711217 +0.024551167 +0.021401385 +0.024848307 +0.087767426 +0.232790782 +0.363891403 +0.398788796 +0.404224742 +0.410404835 +0.383902307 +0.34198474 +0.291471947 +0.243484498 +0.207162972 +0.182280766 +0.150614521 +0.027105096 +0.005390324 +0.018216563 +0.027333389 +0.032217386 +0.046508929 +0.068657745 +0.093683621 +0.114589995 +0.126669046 +0.145781814 +0.22943212 +0.373886682 +0.446433516 +0.424628311 +0.363809099 +0.288842273 +0.222551832 +0.180111879 +0.153403999 +0.130175559 +0.112018502 +0.085087818 +0.052529991 +0.023055347 +0.01852433 +0.027219496 +0.048073792 +0.067043321 +0.079046368 +0.087932989 +0.094192666 +0.111365244 +0.136665715 +0.184805137 +0.33725273 +0.443106842 +0.446944189 +0.428392166 +0.396678784 +0.373534425 +0.363053845 +0.34433686 +0.329369717 +0.299565494 +0.274937054 +0.2461209 +0.158893305 +0.10483276 +0.126015898 +0.143958799 +0.153548226 +0.171694723 +0.196256316 +0.22059801 +0.23474448 +0.233245787 +0.225154185 +0.214870175 +0.216629883 +0.239577319 +0.24005011 +0.222647894 +0.213465382 +0.212413437 +0.199641029 +0.185067964 +0.18607001 +0.181964622 +0.183457191 +0.167674083 +0.095128786 +0.054519496 +0.06651841 +0.088491325 +0.092174021 +0.082148945 +0.074203707 +0.079388543 +0.099813344 +0.119865816 +0.125817932 +0.162431795 +0.226219672 +0.270197517 +0.27974069 +0.275834212 +0.274665002 +0.282576558 +0.289248914 +0.307936938 +0.325761543 +0.351295924 +0.371364426 +0.381464244 +0.392862424 +0.392101502 +0.412191662 +0.445460811 +0.475241027 +0.524715079 +0.576242085 +0.621355406 +0.658207764 +0.685846814 +0.691607733 +0.661413455 +0.650561341 +0.678191781 +0.638463999 +0.57760766 +0.525081732 +0.503346948 +0.497721111 +0.482696652 +0.468506337 +0.459026213 +0.446770618 +0.419576698 +0.306397781 +0.203817561 +0.287407934 +0.315586573 +0.320458468 +0.367775361 +0.430987829 +0.475413735 +0.502436412 +0.50453766 +0.467600505 +0.397959988 +0.422877669 +0.429003076 +0.393685487 +0.347618276 +0.309075053 +0.283645407 +0.265252138 +0.250300899 +0.255234385 +0.274095254 +0.287765173 +0.29526935 +0.212990247 +0.038346741 +0.0501265 +0.147478402 +0.175802006 +0.202180214 +0.222984286 +0.233017612 +0.228551289 +0.204606786 +0.161600809 +0.112305865 +0.074032303 +0.047455408 +0.050704776 +0.071414338 +0.114291465 +0.173472712 +0.218574752 +0.242306339 +0.261051996 +0.273215336 +0.289013497 +0.300456484 +0.287683494 +0.095658256 +0.054863528 +0.164966842 +0.271865912 +0.314413999 +0.338855155 +0.351350177 +0.349845045 +0.328755175 +0.307266761 +0.423500988 +0.604527666 +0.688850652 +0.681725964 +0.643722558 +0.599505897 +0.528982924 +0.472376978 +0.402708939 +0.336187203 +0.269969582 +0.213708097 +0.173140646 +0.121035341 +0.054504884 +0.041763034 +0.032894582 +0.024445216 +0.017287746 +0.012750201 +0.011116815 +0.010089869 +0.009676769 +0.013094163 +0.039972509 +0.16555997 +0.354619038 +0.445963155 +0.460850974 +0.447712961 +0.417481947 +0.395580768 +0.365094001 +0.320051177 +0.271712703 +0.237831921 +0.229530404 +0.206439453 +0.090862758 +0.100941404 +0.16349901 +0.231745458 +0.301347714 +0.363891449 +0.408873215 +0.431863788 +0.433184627 +0.423675756 +0.430670651 +0.463338066 +0.46663926 +0.437703739 +0.386945273 +0.317836193 +0.279037327 +0.251221071 +0.218899845 +0.193347135 +0.174720543 +0.185735032 +0.181892446 +0.159069081 +0.156683233 +0.208621152 +0.23509189 +0.258743009 +0.297691538 +0.344131756 +0.385071222 +0.421909979 +0.450780553 +0.442770657 +0.408375933 +0.4660278 +0.543825388 +0.590712601 +0.619392898 +0.630497226 +0.612379852 +0.605406311 +0.612225965 +0.622250411 +0.623642378 +0.628037131 +0.616005366 +0.581467724 +0.521821149 +0.62168564 +0.68882276 +0.721886408 +0.757574418 +0.785553864 +0.793428335 +0.790415003 +0.773299341 +0.734537203 +0.678504442 +0.65788027 +0.687125225 +0.674769015 +0.662127825 +0.634358867 +0.575291056 +0.522574135 +0.473328863 +0.416672988 +0.377381684 +0.339856562 +0.31519261 +0.280240179 +0.215286257 +0.28261512 +0.329068972 +0.353925131 +0.356209429 +0.346352704 +0.330232943 +0.303359446 +0.262524684 +0.211676451 +0.197741442 +0.232218097 +0.243038177 +0.280833393 +0.311689963 +0.336096465 +0.333874865 +0.314873325 +0.310900969 +0.303055557 +0.279556468 +0.250422817 +0.223709104 +0.192642433 +0.042441913 +0.004161334 +0.015371063 +0.026864056 +0.037948434 +0.051230984 +0.06071008 +0.063784835 +0.061482492 +0.054868929 +0.053578041 +0.043199945 +0.042933639 +0.042377329 +0.042044663 +0.043806872 +0.041858027 +0.038797779 +0.038269037 +0.043424963 +0.052733238 +0.062350747 +0.065593332 +0.068777466 +0.046471089 +0.00362445 +0.001873611 +0.005343263 +0.006567409 +0.006573396 +0.006796461 +0.006604826 +0.005875066 +0.005632477 +0.012146794 +0.034717656 +0.086610424 +0.149893668 +0.205816196 +0.253780741 +0.278098522 +0.274749342 +0.261218991 +0.247606719 +0.246231686 +0.253130578 +0.283491753 +0.317056277 +0.224334002 +0.135907677 +0.134605725 +0.19460838 +0.20447174 +0.198927841 +0.186991726 +0.168605862 +0.152222866 +0.164891732 +0.262180703 +0.337992024 +0.386693781 +0.397865585 +0.391261779 +0.360131506 +0.310908542 +0.264859833 +0.234592006 +0.206168459 +0.191877965 +0.195199738 +0.201588935 +0.191334094 +0.095003946 +0.055048457 +0.087091442 +0.123333451 +0.128222001 +0.117158004 +0.099932217 +0.081093512 +0.070189341 +0.067195188 +0.080281728 +0.121632582 +0.139771726 +0.152895836 +0.167440168 +0.176197601 +0.175496093 +0.173675045 +0.168186884 +0.176587535 +0.191130482 +0.201927236 +0.191769845 +0.181040331 +0.109090843 +0.054733744 +0.165792125 +0.240100774 +0.267293855 +0.269923642 +0.257456311 +0.237695447 +0.201958248 +0.162796464 +0.222543039 +0.354508461 +0.417460351 +0.405959702 +0.396524321 +0.384325312 +0.380261446 +0.378277602 +0.386090761 +0.405274815 +0.411466249 +0.41541526 +0.419201242 +0.420416756 +0.328930329 +0.352915007 +0.444642064 +0.5271847 +0.586452865 +0.62604014 +0.631104665 +0.615631926 +0.573011814 +0.512182631 +0.437055429 +0.426654817 +0.430710302 +0.409895151 +0.381803181 +0.360806465 +0.32524767 +0.296369693 +0.273144869 +0.259794339 +0.250446748 +0.22721653 +0.205480908 +0.179857411 +0.044848177 +0.034959659 +0.067884024 +0.081794691 +0.103187273 +0.127698429 +0.146198499 +0.15516786 +0.153879437 +0.143566649 +0.128386954 +0.154779178 +0.223442177 +0.309229103 +0.394441668 +0.455129484 +0.483413926 +0.500863653 +0.473880526 +0.426610542 +0.354786533 +0.294746842 +0.250483762 +0.212052963 +0.073957296 +0.042990836 +0.073578318 +0.111692449 +0.141773396 +0.170498499 +0.204940983 +0.233565712 +0.247305943 +0.239566755 +0.2568891 +0.381194878 +0.433164685 +0.395009558 +0.341245398 +0.299987963 +0.267183931 +0.226154129 +0.196013448 +0.172747914 +0.157691617 +0.150883422 +0.140877792 +0.129851143 +0.042378001 +0.037299797 +0.066310318 +0.086567683 +0.104568259 +0.126315213 +0.14905376 +0.169901815 +0.184473191 +0.185624155 +0.237262483 +0.321383683 +0.353517092 +0.356389134 +0.350495254 +0.360693066 +0.359871248 +0.362163074 +0.362168125 +0.368877032 +0.386648827 +0.397826393 +0.400282149 +0.374784665 +0.156983725 +0.070338943 +0.204595658 +0.333145524 +0.351363422 +0.361127189 +0.364563403 +0.353684695 +0.322843173 +0.281892816 +0.39044596 +0.54058601 +0.606090693 +0.604418773 +0.603749636 +0.590816199 +0.564103242 +0.530279291 +0.492225964 +0.458171491 +0.431426465 +0.402772835 +0.395511115 +0.383919864 +0.18572398 +0.068864316 +0.135704782 +0.244003297 +0.288947754 +0.310824802 +0.332804154 +0.354443936 +0.372691518 +0.387371487 +0.434847505 +0.50365026 +0.49131282 +0.470092432 +0.465661293 +0.450959398 +0.383414553 +0.320300233 +0.270020092 +0.238062329 +0.214152799 +0.197400279 +0.196628525 +0.186937908 +0.148277867 +0.191678915 +0.170518659 +0.152035064 +0.162032394 +0.194103332 +0.235730983 +0.277855775 +0.30431946 +0.308870115 +0.291767832 +0.364334209 +0.38413256 +0.376173351 +0.373275587 +0.367045845 +0.335445363 +0.298184296 +0.261496937 +0.231868507 +0.211635195 +0.193090894 +0.179629435 +0.155097339 +0.097400997 +0.141163266 +0.158723353 +0.167991627 +0.15789229 +0.143954081 +0.137342001 +0.135806661 +0.136815685 +0.137102373 +0.159566157 +0.238773501 +0.281906639 +0.269511505 +0.252402543 +0.229756185 +0.191942403 +0.147935799 +0.117127139 +0.098995971 +0.089161432 +0.085680929 +0.079766849 +0.070988608 +0.024475882 +0.033680156 +0.045024287 +0.051243795 +0.058090653 +0.064972365 +0.070610653 +0.072827566 +0.071681988 +0.066529018 +0.082538451 +0.089174662 +0.120370247 +0.144232233 +0.161022448 +0.171594464 +0.168648614 +0.160276399 +0.146482461 +0.126403199 +0.103223968 +0.082986745 +0.070695725 +0.062799777 +0.024531295 +0.004273138 +0.010788661 +0.014061871 +0.016731301 +0.018269266 +0.019310146 +0.019954714 +0.020455557 +0.022417837 +0.024405837 +0.023216401 +0.02555155 +0.028345305 +0.030987303 +0.032474331 +0.040247701 +0.053412458 +0.062961109 +0.067896418 +0.071360683 +0.072349498 +0.065094199 +0.053995714 +0.027554893 +0.002800759 +0.001762456 +0.001892937 +0.002220693 +0.002937823 +0.00483542 +0.00853572 +0.013632691 +0.023941922 +0.0349976 +0.038984573 +0.042801487 +0.045425411 +0.045793683 +0.044614305 +0.049780513 +0.061695339 +0.076476664 +0.091365918 +0.10542653 +0.116099144 +0.118860093 +0.1166992 +0.06226502 +0.009431631 +0.01281222 +0.01161383 +0.00820651 +0.006777237 +0.007975618 +0.011197811 +0.015411306 +0.023500023 +0.041783763 +0.051175386 +0.05657873 +0.055638827 +0.052196464 +0.050417266 +0.053665772 +0.059816492 +0.074740839 +0.095194937 +0.115979779 +0.127562568 +0.139821775 +0.155253649 +0.094054184 +0.028076507 +0.048822737 +0.046504367 +0.038247701 +0.031192125 +0.027369129 +0.028048405 +0.03280102 +0.045039704 +0.099341211 +0.13378344 +0.146634187 +0.125676607 +0.09269725 +0.068507052 +0.053617603 +0.046793625 +0.044521417 +0.045054497 +0.048329645 +0.054269228 +0.059842912 +0.064080956 +0.042108224 +0.00295147 +0.008234397 +0.01302689 +0.016015671 +0.020535333 +0.027583292 +0.036511931 +0.045086423 +0.052566062 +0.062591626 +0.058447643 +0.063101508 +0.06944083 +0.075609529 +0.085399621 +0.089739249 +0.091356155 +0.100506512 +0.112800032 +0.127209627 +0.130932455 +0.131635266 +0.125137086 +0.100872998 +0.023584564 +0.026174151 +0.060513741 +0.091494658 +0.130953857 +0.16693034 +0.18555375 +0.173134241 +0.238948956 +0.328441538 +0.402764076 +0.475995565 +0.502083918 +0.512178766 +0.502385067 +0.452136033 +0.388888416 +0.344197042 +0.301738349 +0.254604267 +0.214037141 +0.209162359 +0.212573834 +0.156208536 +0.057203548 +0.084526591 +0.099955124 +0.113578676 +0.147037525 +0.190182959 +0.231628016 +0.254045261 +0.233399657 +0.24228387 +0.328577822 +0.32783109 +0.282819781 +0.232320712 +0.196823955 +0.168195862 +0.145812598 +0.129544582 +0.119693608 +0.114905301 +0.102680568 +0.08967703 +0.086719378 +0.04849155 +0.020809635 +0.021225737 +0.01816042 +0.020203369 +0.026075003 +0.034416084 +0.045278295 +0.05645157 +0.065882517 +0.097217399 +0.13335369 +0.159151133 +0.174243182 +0.18314437 +0.192946714 +0.19903516 +0.208547632 +0.204357663 +0.195729278 +0.179841173 +0.149395582 +0.126914031 +0.119598762 +0.07356662 +0.026547311 +0.024094131 +0.02407582 +0.019837783 +0.015953927 +0.01344288 +0.01108463 +0.009177829 +0.015079329 +0.02557314 +0.03517448 +0.038262436 +0.043217372 +0.044816724 +0.046481205 +0.0470021 +0.050535033 +0.05572984 +0.056645483 +0.053109192 +0.046670486 +0.042254477 +0.042062289 +0.04187289 +0.024786124 +0.030872549 +0.058683337 +0.096545134 +0.127353789 +0.145910077 +0.148121175 +0.130842364 +0.131240197 +0.172986968 +0.209547633 +0.214035167 +0.224034897 +0.225044201 +0.224740447 +0.193060893 +0.163301213 +0.144564721 +0.134877506 +0.125977389 +0.112702525 +0.101831874 +0.089611094 +0.06124719 +0.040617129 +0.046694891 +0.060077702 +0.074397027 +0.085059249 +0.096569571 +0.110878237 +0.117044416 +0.132670719 +0.176959983 +0.206070671 +0.223107967 +0.243628409 +0.265676561 +0.31415066 +0.346685044 +0.395975538 +0.467564923 +0.526640419 +0.574242542 +0.608127736 +0.632706962 +0.656634899 +0.660577574 +0.702426591 +0.76555696 +0.793474122 +0.815919825 +0.83247269 +0.836115583 +0.828923621 +0.804922609 +0.758788113 +0.687662924 +0.640628086 +0.609853027 +0.589024586 +0.574845749 +0.559581676 +0.505754549 +0.436564857 +0.391138745 +0.34149265 +0.295525912 +0.260304056 +0.235876219 +0.215651694 +0.167558952 +0.242649331 +0.246371533 +0.2272917 +0.220753122 +0.220269803 +0.22570364 +0.227900157 +0.221250059 +0.172019248 +0.199502288 +0.3032018 +0.362722752 +0.39454836 +0.415925724 +0.41964546 +0.38664499 +0.327369529 +0.289884694 +0.251674224 +0.219747526 +0.191423936 +0.172167282 +0.167223831 +0.134435351 +0.096488917 +0.103115701 +0.13644137 +0.169470059 +0.203056936 +0.223416613 +0.215943781 +0.193622379 +0.205706944 +0.2711891 +0.341530595 +0.398210204 +0.426439602 +0.431016465 +0.421596963 +0.387253196 +0.347048553 +0.307168182 +0.27234212 +0.248684273 +0.232957987 +0.216474951 +0.187606199 +0.149687266 +0.06560076 +0.064330974 +0.11992549 +0.188359379 +0.246808366 +0.292629028 +0.32211215 +0.332416663 +0.359530571 +0.380579937 +0.399529214 +0.403569559 +0.39189082 +0.374970827 +0.356108282 +0.321410655 +0.292863785 +0.261780604 +0.240253665 +0.214499822 +0.192052056 +0.187166811 +0.190778261 +0.168582975 +0.173990787 +0.22586712 +0.232478694 +0.229093616 +0.226638043 +0.224599739 +0.213745395 +0.191101725 +0.14775978 +0.116522966 +0.088673555 +0.075337674 +0.086347792 +0.104946667 +0.122732425 +0.132390127 +0.139475167 +0.140826385 +0.128606787 +0.121409818 +0.11724241 +0.116209254 +0.102828995 +0.084529287 +0.043087439 +0.050699745 +0.070118269 +0.076509507 +0.08512417 +0.09493976 +0.099284626 +0.094817969 +0.102069294 +0.128263494 +0.168589776 +0.195562508 +0.201991476 +0.218351578 +0.239186301 +0.259729153 +0.262126471 +0.254177331 +0.254292578 +0.270080645 +0.292246399 +0.3418458 +0.380187698 +0.369795398 +0.288254475 +0.314387597 +0.45926813 +0.569324074 +0.625226131 +0.654630443 +0.653727206 +0.649930438 +0.633264689 +0.671520184 +0.674345747 +0.648155112 +0.598031792 +0.548923692 +0.516588476 +0.460539361 +0.377600335 +0.303507525 +0.227149745 +0.153731947 +0.109402763 +0.078832053 +0.061892327 +0.046176964 +0.025950033 +0.023912916 +0.029545793 +0.040478876 +0.052676955 +0.062072882 +0.059649038 +0.057923066 +0.07471377 +0.118913791 +0.159405205 +0.175306785 +0.182201767 +0.200571544 +0.232362814 +0.281506357 +0.334492361 +0.409977064 +0.46867653 +0.518543191 +0.55853464 +0.59060639 +0.609513118 +0.640363417 +0.68792916 +0.721377919 +0.733019681 +0.752598442 +0.761938987 +0.775538678 +0.785594635 +0.784271093 +0.758763966 +0.708079682 +0.669753914 +0.656958568 +0.645668361 +0.62677835 +0.600382793 +0.565029207 +0.530766497 +0.490225558 +0.451749699 +0.420343577 +0.379566599 +0.335156199 +0.290928185 +0.243885613 +0.331001824 +0.375460353 +0.35212919 +0.354098028 +0.371407864 +0.386290087 +0.390330039 +0.381829409 +0.345914915 +0.304313681 +0.273478735 +0.226226659 +0.200363797 +0.192424151 +0.182017175 +0.16510861 +0.154426264 +0.145718103 +0.131936472 +0.117123546 +0.103738122 +0.093686491 +0.086911408 +0.068773673 +0.033533692 +0.03487322 +0.034909097 +0.034878494 +0.037520113 +0.042642849 +0.04866857 +0.052861659 +0.055316022 +0.097726122 +0.166903913 +0.236681368 +0.292717128 +0.308891359 +0.313954014 +0.306382449 +0.285377825 +0.268390824 +0.254673224 +0.244623376 +0.232850751 +0.222626433 +0.217772699 +0.204835765 +0.104773793 +0.056675528 +0.078835044 +0.106206048 +0.147571589 +0.205792137 +0.252721551 +0.277272757 +0.341109591 +0.547463921 +0.598883762 +0.592497927 +0.590065002 +0.582035653 +0.567685447 +0.538004334 +0.49005025 +0.437777836 +0.390133416 +0.358714829 +0.321539888 +0.303407378 +0.303071683 +0.281311659 +0.186617115 +0.171217597 +0.322832648 +0.350080964 +0.348071952 +0.339231916 +0.309632953 +0.244507196 +0.170179112 +0.134676456 +0.12589751 +0.122579423 +0.126093082 +0.128382703 +0.131439226 +0.134683999 +0.125578298 +0.106079288 +0.092143956 +0.087374778 +0.089637159 +0.087808855 +0.090115825 +0.096207202 +0.06522873 +0.035561019 +0.062383327 +0.126736837 +0.175510577 +0.215271345 +0.218893269 +0.194749549 +0.24439076 +0.367191681 +0.452042194 +0.511169009 +0.537183884 +0.53413025 +0.510227201 +0.48538715 +0.457939776 +0.464704935 +0.490039458 +0.514826844 +0.579192899 +0.648238599 +0.723921413 +0.769768526 +0.796098005 +0.828491276 +0.833187196 +0.825910561 +0.815019312 +0.801183339 +0.779428243 +0.745523078 +0.694328082 +0.646946024 +0.618544075 +0.568921481 +0.51005587 +0.45077127 +0.388718381 +0.31840839 +0.262695037 +0.229542071 +0.213274053 +0.202737762 +0.190786172 +0.175911946 +0.162439694 +0.146512997 +0.097381495 +0.066234129 +0.06820357 +0.070689849 +0.074830456 +0.079073824 +0.084283927 +0.087724428 +0.097258686 +0.16506933 +0.244977456 +0.288158494 +0.289792083 +0.274397779 +0.26050094 +0.258248071 +0.246286257 +0.228934521 +0.209113617 +0.194323326 +0.182547531 +0.170180713 +0.158087209 +0.14494382 +0.107863458 +0.054165875 +0.040763872 +0.065560684 +0.098382143 +0.123033564 +0.142274884 +0.159819334 +0.303012276 +0.488251442 +0.556261916 +0.558454224 +0.524063925 +0.464341537 +0.404171985 +0.360788105 +0.323928826 +0.294437946 +0.268223296 +0.235718301 +0.200649154 +0.181860374 +0.192981161 +0.202570977 +0.17324433 +0.13317432 +0.17598804 +0.21708526 +0.250545936 +0.285535617 +0.316245896 +0.339390274 +0.384074226 +0.429737222 +0.460492526 +0.480882637 +0.507535098 +0.520655494 +0.534109316 +0.523522325 +0.484875357 +0.445771105 +0.406341383 +0.361906292 +0.325202706 +0.29794838 +0.274580577 +0.246874566 +0.152258945 +0.205467297 +0.200177386 +0.184610848 +0.203476883 +0.228368686 +0.24165497 +0.23895596 +0.168195451 +0.148341765 +0.145384356 +0.156470377 +0.175051372 +0.193479732 +0.203583449 +0.198461213 +0.188027648 +0.177642664 +0.164886659 +0.154097978 +0.146584935 +0.138549698 +0.13658599 +0.139967203 +0.090933234 +0.034489598 +0.05000917 +0.109599709 +0.135192158 +0.140453785 +0.139688012 +0.129041812 +0.143281491 +0.222288451 +0.308886393 +0.343911021 +0.35606416 +0.353476691 +0.348574677 +0.339866857 +0.322600602 +0.293660117 +0.257928022 +0.220071674 +0.185580379 +0.151592305 +0.127374321 +0.107936487 +0.06136444 +0.018737147 +0.020137188 +0.033810964 +0.052368029 +0.069012569 +0.08271988 +0.089342753 +0.104376684 +0.173215955 +0.231033166 +0.254600467 +0.296864956 +0.329506233 +0.354101031 +0.362977153 +0.351508754 +0.334233991 +0.317579308 +0.305123642 +0.296984483 +0.301496512 +0.316248791 +0.334787846 +0.239751307 +0.153315634 +0.287450769 +0.528255411 +0.60604527 +0.660707592 +0.675420612 +0.644938122 +0.639968183 +0.70843674 +0.737301755 +0.72946505 +0.705669425 +0.673475961 +0.642436981 +0.566519501 +0.462417175 +0.406068152 +0.363926987 +0.311295881 +0.275039722 +0.268442931 +0.27709824 +0.282353742 +0.231113026 +0.208181483 +0.2902418 +0.423046182 +0.496148971 +0.53972399 +0.552105596 +0.5478766 +0.576713372 +0.671865899 +0.715874187 +0.723253202 +0.702069619 +0.659711227 +0.599916939 +0.508236165 +0.418235093 +0.349514791 +0.290090569 +0.253063749 +0.237130627 +0.231262327 +0.236197268 +0.234813251 +0.193668757 +0.088422532 +0.10617514 +0.175648533 +0.227288031 +0.260755448 +0.256595406 +0.228459515 +0.322814274 +0.443209976 +0.504984794 +0.54308227 +0.561092336 +0.567510969 +0.577024296 +0.540055303 +0.500084822 +0.467758526 +0.439982788 +0.395975148 +0.350752391 +0.356135376 +0.368526351 +0.35629653 +0.293455541 +0.214002424 +0.321634617 +0.481933634 +0.569767455 +0.624343101 +0.660427477 +0.682599946 +0.713660611 +0.757461361 +0.782076167 +0.7764602 +0.757484775 +0.722412113 +0.681179942 +0.631813243 +0.596372798 +0.559340202 +0.529458387 +0.496092254 +0.467073901 +0.44908405 +0.429024276 +0.402011648 +0.324990399 +0.307999787 +0.312487741 +0.281742428 +0.283681269 +0.307642978 +0.334457313 +0.341276233 +0.260669636 +0.297841471 +0.330155472 +0.340348553 +0.326650473 +0.304010109 +0.281517774 +0.244743648 +0.216348888 +0.211173026 +0.208281688 +0.201757873 +0.193045833 +0.190411277 +0.201561879 +0.214448837 +0.159056394 +0.080020824 +0.134893468 +0.214948259 +0.230235183 +0.244776129 +0.257650784 +0.253528383 +0.30477495 +0.408481013 +0.485594207 +0.53670467 +0.561569378 +0.562867342 +0.564303259 +0.538929547 +0.501899106 +0.472709089 +0.448835256 +0.427070305 +0.405148547 +0.397725124 +0.393958673 +0.385982332 +0.290354203 +0.12135576 +0.090717877 +0.150445002 +0.16899198 +0.177460716 +0.179008892 +0.193793158 +0.337764087 +0.45466735 +0.499950157 +0.522745542 +0.537015821 +0.536985765 +0.526012613 +0.506201362 +0.474153883 +0.435486978 +0.405868407 +0.387950645 +0.374154438 +0.376032865 +0.383358632 +0.387034665 +0.323680456 +0.154218622 +0.101371513 +0.121416758 +0.147167678 +0.162067004 +0.155206949 +0.200230324 +0.353189449 +0.461095347 +0.509700001 +0.543878704 +0.566124956 +0.580440726 +0.59008778 +0.583254654 +0.5553925 +0.533352147 +0.496122889 +0.469287951 +0.444304089 +0.43964539 +0.44709493 +0.437792987 +0.375538368 +0.200480488 +0.104324045 +0.100426007 +0.153381623 +0.193738168 +0.217760144 +0.298015698 +0.44676021 +0.577838253 +0.609746969 +0.617464142 +0.624654922 +0.6344741 +0.651086162 +0.620408601 +0.573880454 +0.555176449 +0.539119094 +0.507945584 +0.481474683 +0.476815527 +0.461141497 +0.427222556 +0.354887808 +0.286872489 +0.344867066 +0.342903282 +0.36544754 +0.404201958 +0.440425544 +0.456628146 +0.470135248 +0.517948407 +0.564935141 +0.589187992 +0.59856036 +0.603686316 +0.613069675 +0.581905321 +0.504969601 +0.448708412 +0.41142075 +0.420828162 +0.454168898 +0.532161898 +0.592915159 +0.640370193 +0.667800772 +0.752721358 +0.810469675 +0.820300561 +0.818685487 +0.814257098 +0.804780308 +0.786831795 +0.753529308 +0.729661071 +0.696855948 +0.640005386 +0.580891313 +0.523441727 +0.466625387 +0.414546517 +0.371527134 +0.340968937 +0.328824247 +0.323586771 +0.320685144 +0.344259523 +0.38327842 +0.402990589 +0.388112512 +0.401200822 +0.514032153 +0.572314837 +0.597975332 +0.61686595 +0.626957172 +0.622224398 +0.637758578 +0.677690005 +0.70807448 +0.727184703 +0.716691864 +0.697219033 +0.66352821 +0.602030475 +0.538831667 +0.486292684 +0.433532797 +0.391674668 +0.360495592 +0.350233581 +0.355341575 +0.362766732 +0.299288472 +0.13727074 +0.147348245 +0.186621455 +0.184862487 +0.176091629 +0.171282907 +0.151915021 +0.164335977 +0.242674426 +0.3318687 +0.393114531 +0.439908543 +0.456329203 +0.446062414 +0.418079561 +0.386960329 +0.363552384 +0.3429805 +0.326440169 +0.308130552 +0.290449751 +0.278328549 +0.264365615 +0.208330241 +0.074321601 +0.0432832 +0.048788048 +0.05274156 +0.047686258 +0.044913623 +0.04790007 +0.060368989 +0.069748908 +0.082356647 +0.096146312 +0.11092695 +0.127561048 +0.145451745 +0.167894341 +0.188556895 +0.198992364 +0.201767851 +0.202594526 +0.197181141 +0.182357833 +0.167731486 +0.154166919 +0.132706521 +0.040424513 +0.010946222 +0.013284916 +0.016705824 +0.021064611 +0.022307313 +0.026203369 +0.034543641 +0.038133036 +0.042099514 +0.045162469 +0.047565989 +0.050328446 +0.056768266 +0.071026302 +0.087947938 +0.099224642 +0.106345539 +0.106074685 +0.096152687 +0.081616179 +0.07415083 +0.076123206 +0.072560056 +0.039207252 +0.018319744 +0.020715312 +0.030101087 +0.034877676 +0.035774869 +0.033549251 +0.03092533 +0.030438603 +0.032961941 +0.036981848 +0.041178725 +0.046491019 +0.054154654 +0.063036763 +0.065879771 +0.061515268 +0.053078639 +0.045855675 +0.044319388 +0.052078922 +0.063876647 +0.073687539 +0.071623939 +0.037046903 +0.037285334 +0.05309473 +0.076154176 +0.100643075 +0.120605415 +0.125714682 +0.147477527 +0.206646214 +0.243344255 +0.265777524 +0.287482735 +0.297770485 +0.293456116 +0.281291042 +0.247854314 +0.206963545 +0.161058478 +0.128307792 +0.106683546 +0.094144912 +0.096522046 +0.102418835 +0.098171777 +0.056466617 +0.03710054 +0.055791179 +0.082076314 +0.091144322 +0.088440668 +0.077473908 +0.092867825 +0.127104129 +0.137574992 +0.130348246 +0.110923148 +0.091128626 +0.080413828 +0.074735924 +0.078482281 +0.094847685 +0.111948016 +0.120007271 +0.12603151 +0.13096643 +0.127982461 +0.117790334 +0.111837064 +0.087304358 +0.07873236 +0.092473205 +0.097314408 +0.100736439 +0.102245083 +0.122094629 +0.218265136 +0.285564155 +0.280746151 +0.258303418 +0.220490851 +0.177919012 +0.142484425 +0.11060675 +0.093655133 +0.087795084 +0.084651993 +0.083338593 +0.083778206 +0.087124392 +0.092308142 +0.098907387 +0.106741216 +0.100340601 +0.10074182 +0.103893321 +0.122013364 +0.157712042 +0.200697011 +0.217034541 +0.250371969 +0.275727537 +0.267781404 +0.254368707 +0.258748324 +0.267860506 +0.277016729 +0.293653196 +0.302567301 +0.297232034 +0.280309433 +0.269811633 +0.252587517 +0.244026638 +0.248515802 +0.25584791 +0.236954284 +0.140766581 +0.133936636 +0.213984474 +0.269118594 +0.291425698 +0.292524656 +0.245928034 +0.260340934 +0.264135807 +0.262985716 +0.265133623 +0.268033397 +0.27118342 +0.271294816 +0.272209622 +0.250911521 +0.224858566 +0.203431535 +0.189172468 +0.176966546 +0.154401406 +0.132634656 +0.116801432 +0.107265159 +0.059844476 +0.023869983 +0.017350187 +0.024487079 +0.027357686 +0.027325107 +0.031098913 +0.052816789 +0.083912745 +0.107320428 +0.108859123 +0.100322792 +0.095700855 +0.102938349 +0.11791474 +0.134880856 +0.153518246 +0.171315328 +0.183116842 +0.202328877 +0.226457202 +0.247633713 +0.271943105 +0.280988961 +0.246038758 +0.219894127 +0.236474414 +0.290636432 +0.311275995 +0.292313892 +0.297922286 +0.35943248 +0.401530734 +0.439633176 +0.461744597 +0.483715687 +0.484570504 +0.461690633 +0.429873465 +0.401303827 +0.389496554 +0.4050408 +0.429704514 +0.448700092 +0.430011114 +0.396647174 +0.372725901 +0.351736272 +0.273663294 +0.188161318 +0.210508026 +0.25649361 +0.262071587 +0.246827767 +0.313914787 +0.447175854 +0.510847754 +0.518087518 +0.50751139 +0.488909862 +0.458687656 +0.431046486 +0.411481209 +0.380739599 +0.339967779 +0.289732814 +0.249181684 +0.218789381 +0.196231896 +0.177357878 +0.165151007 +0.162564031 +0.137393915 +0.080953605 +0.067922338 +0.08940542 +0.127798216 +0.172675067 +0.256594319 +0.35317354 +0.384939686 +0.381844639 +0.37604491 +0.390429358 +0.414248811 +0.436846443 +0.471547934 +0.505983203 +0.539229199 +0.556681484 +0.576651929 +0.603820056 +0.631386885 +0.651686603 +0.664317282 +0.676846275 +0.67212326 +0.675437038 +0.679827805 +0.699346023 +0.712934755 +0.702130959 +0.669624247 +0.608334579 +0.590661591 +0.578506521 +0.552887662 +0.520558296 +0.490341015 +0.461721863 +0.3741049 +0.280202489 +0.239672506 +0.215572746 +0.198527602 +0.182969403 +0.166643451 +0.152765877 +0.140130711 +0.118149163 +0.052126108 +0.037128796 +0.047090345 +0.048699873 +0.046181011 +0.045671912 +0.051089122 +0.130155381 +0.265604326 +0.352553557 +0.384446111 +0.35980762 +0.314525198 +0.266950597 +0.225758187 +0.196101579 +0.176687693 +0.163815102 +0.151574189 +0.140606668 +0.132973551 +0.129461628 +0.121154696 +0.107651265 +0.061580409 +0.024209419 +0.01031782 +0.005795966 +0.002770749 +0.002079219 +0.006505704 +0.017684308 +0.022342762 +0.01943443 +0.019629838 +0.023025675 +0.028864598 +0.036418203 +0.043418581 +0.053759622 +0.069245459 +0.084767988 +0.095593451 +0.101819908 +0.103481634 +0.099049547 +0.093047952 +0.077975762 +0.053259768 +0.049925195 +0.050319035 +0.056189648 +0.06967191 +0.090862169 +0.107082983 +0.149640705 +0.188081859 +0.197654236 +0.194560066 +0.188868266 +0.185957723 +0.180658775 +0.173366378 +0.179318759 +0.179563621 +0.180556453 +0.191583664 +0.20582285 +0.223380818 +0.247944749 +0.273151251 +0.290938722 +0.257660166 +0.253555311 +0.260894662 +0.258203189 +0.270181165 +0.286980751 +0.285148355 +0.302571622 +0.303046175 +0.293196077 +0.272190062 +0.247686969 +0.229801172 +0.220629636 +0.205988318 +0.195258235 +0.187867472 +0.184771124 +0.181364777 +0.170082094 +0.16101194 +0.15221321 +0.142542413 +0.130800726 +0.066012011 +0.050025925 +0.072915284 +0.095213664 +0.113831662 +0.125906261 +0.124402315 +0.163494253 +0.178494728 +0.173090798 +0.169052204 +0.169107597 +0.172048684 +0.165882846 +0.161981653 +0.150457526 +0.132380661 +0.11714397 +0.1015294 +0.09244584 +0.097905327 +0.115964344 +0.140368782 +0.156288518 +0.140384079 +0.177064944 +0.192894593 +0.212668466 +0.237512434 +0.27057562 +0.286124825 +0.370916839 +0.41972755 +0.434673128 +0.436781576 +0.435308675 +0.432717428 +0.429509062 +0.425643044 +0.411177229 +0.400104428 +0.384017347 +0.388438846 +0.399024655 +0.410882902 +0.418954173 +0.423751971 +0.419953438 +0.406721664 +0.385105679 +0.399680047 +0.416627463 +0.431059441 +0.439918654 +0.450948383 +0.497299373 +0.51604885 +0.510582351 +0.495314919 +0.473339747 +0.452440073 +0.426554199 +0.362022174 +0.299633125 +0.279925875 +0.268038785 +0.258055116 +0.256859778 +0.273277707 +0.280823786 +0.285935131 +0.279320142 +0.201319918 +0.170200834 +0.180941086 +0.175844546 +0.178553411 +0.187675185 +0.19473709 +0.250147061 +0.277797195 +0.285830216 +0.274860098 +0.254352679 +0.233008848 +0.216917456 +0.196256428 +0.174224118 +0.157885007 +0.152791636 +0.150437384 +0.151871722 +0.147215238 +0.147326157 +0.146191052 +0.134178147 +0.077192148 +0.043328398 +0.040376064 +0.044971209 +0.053116338 +0.067179835 +0.095939662 +0.175999021 +0.226060187 +0.249584745 +0.26105692 +0.272505781 +0.284960475 +0.283776714 +0.259003034 +0.214685915 +0.168088435 +0.133974137 +0.106682794 +0.082251381 +0.068821212 +0.061200851 +0.057109284 +0.052620509 +0.040197559 +0.026893462 +0.023108405 +0.025635396 +0.031868908 +0.041457791 +0.049807258 +0.079875772 +0.10996785 +0.135867993 +0.150911709 +0.162660784 +0.177836542 +0.19373789 +0.196114303 +0.18682877 +0.165344686 +0.145815969 +0.128389456 +0.113751912 +0.102441391 +0.101479846 +0.102758269 +0.105517128 +0.074377584 +0.051931288 +0.044513802 +0.043531103 +0.046492376 +0.053942029 +0.061521464 +0.082075813 +0.103492395 +0.116288337 +0.123951172 +0.119805123 +0.114215565 +0.100706101 +0.085902103 +0.074696807 +0.066753066 +0.060627104 +0.055952849 +0.047942367 +0.037752434 +0.032362389 +0.031047995 +0.030542885 +0.027905767 +0.016568778 +0.021192263 +0.029633191 +0.035205108 +0.040226037 +0.05996179 +0.088454782 +0.106936172 +0.11547924 +0.115690604 +0.111691261 +0.099331756 +0.083448351 +0.081056191 +0.074973959 +0.058946157 +0.045753616 +0.033974945 +0.025815805 +0.025153894 +0.028059113 +0.031413796 +0.032947633 +0.026596568 +0.018387738 +0.013911125 +0.013530022 +0.017357836 +0.024894847 +0.036939415 +0.053330973 +0.06324932 +0.065101907 +0.062578519 +0.060861507 +0.061815659 +0.063752616 +0.06146317 +0.060995308 +0.063466863 +0.069225771 +0.077872301 +0.089006127 +0.105733911 +0.127551176 +0.145862421 +0.153175946 +0.1228437 +0.094351627 +0.082411675 +0.092845192 +0.128406759 +0.158453588 +0.202861659 +0.301773937 +0.337317448 +0.371570987 +0.388163876 +0.386183025 +0.382739134 +0.391609279 +0.364940068 +0.322096118 +0.286573023 +0.257105517 +0.249693021 +0.246883738 +0.244620135 +0.262061432 +0.281001438 +0.280755503 +0.222592001 +0.168797018 +0.193954631 +0.210994516 +0.218179914 +0.217469238 +0.201678266 +0.202066938 +0.209939145 +0.211681341 +0.209762887 +0.198354682 +0.190485762 +0.17695769 +0.164537668 +0.149916901 +0.142435035 +0.132027742 +0.129195645 +0.141041185 +0.138467705 +0.137994353 +0.136163573 +0.125423224 +0.109307819 +0.107103129 +0.117712658 +0.120966195 +0.123924537 +0.12844339 +0.110640312 +0.111093028 +0.120196201 +0.125342604 +0.132916296 +0.142776837 +0.152695076 +0.166816865 +0.17239041 +0.167866375 +0.160773329 +0.150102213 +0.148091543 +0.140490786 +0.127627646 +0.115448492 +0.109466043 +0.10696494 +0.08726614 +0.060553652 +0.069479302 +0.097738138 +0.11706632 +0.122872521 +0.144037018 +0.207279081 +0.240839655 +0.262054008 +0.285676125 +0.29632937 +0.301305972 +0.301687911 +0.282882608 +0.26276875 +0.243872799 +0.254759291 +0.262856129 +0.280707104 +0.321632268 +0.386078059 +0.421174183 +0.437580734 +0.417146563 +0.383722137 +0.404444889 +0.436199008 +0.444819483 +0.446875105 +0.456977004 +0.470934068 +0.471890503 +0.46382294 +0.449299242 +0.444239616 +0.443977638 +0.444256623 +0.429645983 +0.420710974 +0.418181973 +0.416825604 +0.418165624 +0.423677205 +0.421452375 +0.412110247 +0.397358848 +0.387897131 +0.343281366 +0.317124577 +0.371228746 +0.385168297 +0.383370571 +0.378270273 +0.349662112 +0.337811561 +0.346498496 +0.355099153 +0.356768588 +0.357788461 +0.365935367 +0.374716018 +0.344567795 +0.320272025 +0.300206674 +0.291154374 +0.296431666 +0.295035019 +0.290880608 +0.298462887 +0.290045825 +0.284876253 +0.238991097 +0.208621509 +0.208694031 +0.219737486 +0.232897523 +0.229033197 +0.232930625 +0.270256736 +0.286931831 +0.299084868 +0.30904925 +0.312150454 +0.311461461 +0.299135456 +0.282070122 +0.26522514 +0.242518976 +0.221168692 +0.209939239 +0.201058275 +0.203799975 +0.212011342 +0.216470926 +0.214886707 +0.181332922 +0.118394695 +0.091556847 +0.091878535 +0.096061444 +0.104539529 +0.16719676 +0.252950906 +0.299951685 +0.3110752 +0.31151621 +0.312031512 +0.307660178 +0.298782638 +0.279595309 +0.259282302 +0.241180204 +0.236451483 +0.238914082 +0.237916502 +0.255755637 +0.283937064 +0.311958116 +0.350740227 +0.351059589 +0.341072588 +0.347250229 +0.38222247 +0.402254323 +0.405375228 +0.433681824 +0.463538345 +0.486737443 +0.497742388 +0.481219217 +0.475660829 +0.460508418 +0.425492725 +0.392203763 +0.38126856 +0.371494645 +0.360741416 +0.354187499 +0.349972493 +0.350008526 +0.358547289 +0.365720202 +0.362674654 +0.312924506 +0.253739504 +0.27450155 +0.304530222 +0.321888715 +0.319669396 +0.312020421 +0.340893232 +0.369561181 +0.389477025 +0.394544007 +0.389624439 +0.37855648 +0.370217313 +0.331112842 +0.282884662 +0.253538673 +0.239031466 +0.222193248 +0.214099698 +0.20791404 +0.212451005 +0.220868734 +0.232954933 +0.227241426 +0.209184022 +0.184806499 +0.192218057 +0.218066325 +0.24315965 +0.294537124 +0.366856819 +0.403052624 +0.423267165 +0.429308066 +0.437011534 +0.435096573 +0.445054138 +0.443742987 +0.436573082 +0.425438085 +0.40793114 +0.397670602 +0.400613671 +0.420653481 +0.457625574 +0.48928769 +0.512787673 +0.497389536 +0.439477881 +0.40308636 +0.440063197 +0.457112142 +0.454706354 +0.493129865 +0.520857947 +0.535322775 +0.557261344 +0.574429774 +0.582977667 +0.583577612 +0.58010557 +0.543196281 +0.509774265 +0.479297975 +0.449818738 +0.427357041 +0.412996123 +0.419293614 +0.44160755 +0.45235278 +0.460880452 +0.42608724 +0.336237662 +0.292891057 +0.311287444 +0.337623309 +0.359845316 +0.419962858 +0.482488033 +0.530678779 +0.549974787 +0.552935977 +0.561873758 +0.559373965 +0.537049963 +0.475312165 +0.40508429 +0.356844004 +0.314346939 +0.279686163 +0.260654778 +0.264945399 +0.283705554 +0.30645566 +0.325947302 +0.286689651 +0.187725625 +0.125823382 +0.121356843 +0.126507131 +0.125430799 +0.172354012 +0.256432257 +0.308731278 +0.35070582 +0.384229131 +0.417761013 +0.442898424 +0.490842476 +0.522519296 +0.535420298 +0.509940729 +0.486768386 +0.460878087 +0.429223853 +0.426818265 +0.440186325 +0.444723859 +0.439066693 +0.398212424 +0.300074301 +0.223019181 +0.206945119 +0.210888942 +0.232410492 +0.32847836 +0.40266262 +0.415158498 +0.39960944 +0.374498568 +0.342601954 +0.315059142 +0.29331754 +0.261468113 +0.243362544 +0.241532875 +0.238876957 +0.235077721 +0.221089038 +0.193582955 +0.168644052 +0.150591944 +0.142602955 +0.123658335 +0.084230452 +0.048915539 +0.037383771 +0.035598857 +0.041804628 +0.055483784 +0.075932633 +0.08686474 +0.089913443 +0.090660111 +0.088917692 +0.084199026 +0.076806248 +0.072489928 +0.069660864 +0.06821915 +0.066283942 +0.065195954 +0.064846982 +0.059237746 +0.050748529 +0.046841004 +0.046448566 +0.045248692 +0.039551467 +0.036222043 +0.033951132 +0.039161554 +0.056421517 +0.088105442 +0.143185775 +0.184338806 +0.209443871 +0.223860817 +0.227801985 +0.231883468 +0.23243595 +0.229995333 +0.224586832 +0.214458131 +0.208197466 +0.200831668 +0.196449198 +0.186893612 +0.178707773 +0.172716254 +0.164985467 +0.160424895 +0.139381566 +0.11415223 +0.103425606 +0.103210502 +0.109600324 +0.131904668 +0.188711771 +0.205491704 +0.203487422 +0.198265585 +0.189594024 +0.172704373 +0.146818744 +0.126076393 +0.106998291 +0.090849305 +0.077018975 +0.067116554 +0.057352058 +0.048958343 +0.040705041 +0.03561664 +0.032010991 +0.025307347 +0.01666133 +0.009255492 +0.007611943 +0.008033897 +0.010699376 +0.020541891 +0.043743902 +0.07327228 +0.105384374 +0.138497717 +0.172063218 +0.202000677 +0.228345915 +0.246176449 +0.253526882 +0.253424449 +0.250749531 +0.250970887 +0.245368669 +0.233225214 +0.224253996 +0.215441934 +0.214185613 +0.202195821 +0.173347489 +0.143701817 +0.142361123 +0.162647385 +0.198706087 +0.28089654 +0.376545022 +0.413088617 +0.453194257 +0.482565526 +0.499917971 +0.513139744 +0.529051285 +0.523612407 +0.48588821 +0.455251178 +0.436073397 +0.423875325 +0.419200776 +0.435084478 +0.462919451 +0.489500546 +0.49389112 +0.469402423 +0.433065952 +0.431886233 +0.467130169 +0.483223862 +0.501092661 +0.516431592 +0.549013406 +0.573054032 +0.571644927 +0.560912871 +0.541628532 +0.521832145 +0.498612602 +0.468373749 +0.443069184 +0.417944617 +0.400222313 +0.382757735 +0.36617712 +0.345960992 +0.323135132 +0.304142986 +0.277689325 +0.215029312 +0.180008958 +0.152817355 +0.136180812 +0.137363186 +0.14608575 +0.143538008 +0.183669969 +0.206785912 +0.207165889 +0.192281753 +0.178689261 +0.161292437 +0.144880033 +0.131644513 +0.10955277 +0.082635359 +0.063554021 +0.05190968 +0.04735368 +0.048673022 +0.062969108 +0.091559012 \ No newline at end of file diff --git a/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/settings.ini b/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/settings.ini new file mode 100644 index 0000000..015374b --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/settings.ini @@ -0,0 +1,6 @@ +uc_type = expansion_fast +master = relaxed +optimality_gap = 0 +max_iteration = Inf +cut_type = weekly +solver = amplxpress \ No newline at end of file diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000..5a42243 --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,58 @@ +import shutil +from pathlib import Path +from unittest import mock + +import pytest + +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.use_cases.create_list.study_list_composer import StudyListComposer, StudyListComposerParameters +from tests.unit.assets import ASSETS_DIR + + +@pytest.fixture(name="studies_in_dir") +def studies_in_dir_fixture(tmp_path: Path) -> str: + studies_in_dir = tmp_path.joinpath("STUDIES-IN") + assets_dir = ASSETS_DIR.joinpath("study_list_composer/studies") + shutil.copytree(assets_dir, studies_in_dir) + return str(studies_in_dir) + + +@pytest.fixture(name="repo") +def repo_fixture(tmp_path: Path) -> DataRepoTinydb: + return DataRepoTinydb( + database_file_path=tmp_path.joinpath("repo.json"), + db_primary_key="name", + ) + + +@pytest.fixture(name="study_list_composer") +def study_list_composer_fixture( + tmp_path: Path, + repo: DataRepoTinydb, + studies_in_dir: str, +) -> StudyListComposer: + display = mock.Mock(spec=DisplayTerminal) + composer = StudyListComposer( + repo=repo, + display=display, + parameters=StudyListComposerParameters( + studies_in_dir=studies_in_dir, + time_limit=42, + n_cpu=24, + log_dir=str(tmp_path.joinpath("LOGS")), + xpansion_mode="", + output_dir=str(tmp_path.joinpath("FINISHED")), + post_processing=False, + antares_versions_on_remote_server=[ + "800", + "810", + "820", + "830", + "840", + "850", + ], + other_options="", + ), + ) + return composer diff --git a/tests/unit/launcher/conftest.py b/tests/unit/launcher/conftest.py new file mode 100644 index 0000000..3797bae --- /dev/null +++ b/tests/unit/launcher/conftest.py @@ -0,0 +1,21 @@ +from pathlib import Path + +import pytest + +from antareslauncher.study_dto import StudyDTO + + +@pytest.fixture(name="pending_study") +def pending_study_fixture(tmp_path: Path) -> StudyDTO: + study_path = tmp_path.joinpath("My Study") + job_log_dir = tmp_path.joinpath("LOG_DIR") + output_dir = tmp_path.joinpath("OUTPUT_DIR") + return StudyDTO( + path=str(study_path), + started=False, + job_log_dir=str(job_log_dir), + output_dir=str(output_dir), + zipfile_path="", + zip_is_sent=False, + job_id=0, + ) diff --git a/tests/unit/launcher/test_launch_controller.py b/tests/unit/launcher/test_launch_controller.py index bf58121..590419a 100644 --- a/tests/unit/launcher/test_launch_controller.py +++ b/tests/unit/launcher/test_launch_controller.py @@ -1,187 +1,314 @@ -import copy -import getpass +import zipfile +from pathlib import Path, PurePosixPath from unittest import mock -from unittest.mock import call import pytest -import antareslauncher.remote_environnement.remote_environment_with_slurm -import antareslauncher.use_cases.launch.study_submitter -import antareslauncher.use_cases.launch.study_zip_uploader from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb -from antareslauncher.data_repo.data_reporter import DataReporter -from antareslauncher.data_repo.idata_repo import IDataRepo -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.file_manager.file_manager import FileManager -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO -from antareslauncher.use_cases.launch import launch_controller -from antareslauncher.use_cases.launch.launch_controller import StudyLauncher +from antareslauncher.use_cases.launch.launch_controller import LaunchController, StudyLauncher from antareslauncher.use_cases.launch.study_submitter import StudySubmitter -from antareslauncher.use_cases.launch.study_zip_cleaner import StudyZipCleaner from antareslauncher.use_cases.launch.study_zip_uploader import StudyZipfileUploader -from antareslauncher.use_cases.launch.study_zipper import StudyZipper + +# noinspection SpellCheckingInspection +STUDY_FILES = [ + "check-config.json", + "Desktop.ini", + "input/areas/dummy.txt", + "input/wind/dummy.txt", + "layers/layers.ini", + "output/20230321-1901eco/dummy.txt", + "output/20230321-1901eco.zip", + "output/20230926-1230adq/dummy.txt", + "settings/comments.txt", + "settings/generaldata.ini", + "settings/resources/dummy.txt", + "settings/scenariobuilder.dat", + "study.antares", +] + + +def prepare_study_data(study_dir: Path) -> None: + for file in STUDY_FILES: + file_path = study_dir.joinpath(file) + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.touch() + + +@pytest.fixture(name="ready_study") +def ready_study_fixture(pending_study: StudyDTO) -> StudyDTO: + """Prepare the study data and return the study.""" + study_dir = Path(pending_study.path) + prepare_study_data(study_dir) + return pending_study + + +@pytest.fixture(name="study_uploaded") +def study_uploaded_fixture(tmp_path: Path) -> StudyDTO: + study_path = tmp_path.joinpath("upload-failure") + job_log_dir = tmp_path.joinpath("LOG_DIR") + output_dir = tmp_path.joinpath("OUTPUT_DIR") + study = StudyDTO( + path=str(study_path), + started=False, + job_log_dir=str(job_log_dir), + output_dir=str(output_dir), + zipfile_path="", + zip_is_sent=False, + job_id=0, + ) + study_dir = Path(study.path) + prepare_study_data(study_dir) + return study + + +@pytest.fixture(name="study_submitted") +def study_submitted_fixture(tmp_path: Path) -> StudyDTO: + study_path = tmp_path.joinpath("submit-failure") + job_log_dir = tmp_path.joinpath("LOG_DIR") + output_dir = tmp_path.joinpath("OUTPUT_DIR") + study = StudyDTO( + path=str(study_path), + started=False, + job_log_dir=str(job_log_dir), + output_dir=str(output_dir), + zipfile_path="", + zip_is_sent=False, + job_id=0, + ) + study_dir = Path(study.path) + prepare_study_data(study_dir) + return study class TestStudyLauncher: - def setup_method(self): - env = mock.Mock(spec_set=RemoteEnvironmentWithSlurm) - display = mock.Mock(spec_set=IDisplay) - file_manager = mock.Mock(spec_set=FileManager) - repo = mock.Mock(spec_set=IDataRepo) - self.reporter = DataReporter(repo) - self.zipper = StudyZipper(file_manager, display) - self.study_uploader = StudyZipfileUploader(env, display) - self.zipfile_cleaner = StudyZipCleaner(file_manager, display) - self.study_submitter = StudySubmitter(env, display) - self.study_launcher = StudyLauncher( - self.zipper, - self.study_uploader, - self.zipfile_cleaner, - self.study_submitter, - self.reporter, - ) - - def test_launch_study_calls_all_four_steps(self): - study = StudyDTO(path="hello") - study1 = StudyDTO(path="hello1") - self.zipper.zip = mock.Mock(return_value=study1) - study2 = StudyDTO(path="hello2", zip_is_sent=True) - self.study_uploader.upload = mock.Mock(return_value=study2) - study3 = StudyDTO(path="hello3") - self.zipfile_cleaner.remove_input_zipfile = mock.Mock(return_value=study3) - study4 = StudyDTO(path="hello4") - self.study_submitter.submit_job = mock.Mock(return_value=study4) - self.reporter.save_study = mock.Mock() - - self.study_launcher.launch_study(study) - - self.zipper.zip.assert_called_once_with(study) - self.study_uploader.upload.assert_called_once_with(study1) - self.zipfile_cleaner.remove_input_zipfile.assert_called_once_with(study2) - self.study_submitter.submit_job.assert_called_once_with(study3) - - assert self.reporter.save_study.call_count == 4 - calls = self.reporter.save_study.call_args_list - assert calls[0] == call(study1) - assert calls[1] == call(study2) - assert calls[2] == call(study3) - assert calls[3] == call(study4) - - -class TestLauncherController: - def setup_method(self): - self.data_repo = DataRepoTinydb("", "name") - self.data_repo.save_study = mock.Mock() - self.display = mock.Mock() - - @pytest.fixture(scope="function") - def my_launch_controller(self): - expected_study = StudyDTO(path="hello") - list_of_studies = [copy.deepcopy(expected_study)] - self.data_repo.get_list_of_studies = mock.Mock(return_value=list_of_studies) - remote_env_mock = mock.Mock(spec=RemoteEnvironmentWithSlurm) - file_manager_mock = mock.Mock() - my_launcher = launch_controller.LaunchController( - self.data_repo, remote_env_mock, file_manager_mock, self.display - ) - return my_launcher, expected_study - - def test_with_one_study_the_compressor_is_called_once(self): - my_study = StudyDTO(path="hello") - list_of_studies = [my_study] - self.data_repo.get_list_of_studies = mock.Mock(return_value=list_of_studies) - - remote_env_mock = mock.Mock(spec=RemoteEnvironmentWithSlurm) - file_manager = mock.Mock(spec_set=FileManager) - file_manager.zip_dir_excluding_subdir = mock.Mock() - - my_launcher = launch_controller.LaunchController( - self.data_repo, remote_env_mock, file_manager, self.display - ) - my_launcher.launch_all_studies() - - zipfile_path = f"{my_study.path}-{getpass.getuser()}.zip" - file_manager.zip_dir_excluding_subdir.assert_called_once_with( - my_study.path, zipfile_path, None - ) + """ + The gaol is to test the launching of a study. + Every call to the remote environment is mocked. + """ @pytest.mark.unit_test - def test_given_one_study_then_repo_is_called_to_save_the_study_with_updated_zip_is_sent( - self, my_launch_controller - ): - # given - my_launcher, expected_study = my_launch_controller - # when - my_launcher.env.upload_file = mock.Mock(return_value=True) - my_launcher.repo.save_study = mock.Mock() - my_launcher.launch_all_studies() - # then - expected_study.zipfile_path = f"{expected_study.path}-{getpass.getuser()}.zip" - second_call = my_launcher.repo.save_study.call_args_list[1] - first_argument = second_call[0][0] - assert first_argument.zip_is_sent + def test_launch_study__nominal_case(self, ready_study: StudyDTO) -> None: + """ + Test the nominal case of launching a study. - @pytest.mark.unit_test - def test_given_one_study_when_launcher_is_called_then_study_is_saved_with_job_id_and_submitted_flag( - self, my_launch_controller - ): - # given - my_launcher, expected_study = my_launch_controller - # when - my_launcher.env.upload_file = mock.Mock(return_value=True) - my_launcher.env.submit_job = mock.Mock(return_value=42) - my_launcher.repo.save_study = mock.Mock() - my_launcher.launch_all_studies() - # then - expected_study.zipfile_path = "ciao.zip" - expected_study.zip_is_sent = True - fourth_call = my_launcher.repo.save_study.call_args_list[3] - first_argument = fourth_call[0][0] - assert first_argument.job_id == 42 + - The study directory must be correctly compressed. + - The `upload` method of the `study_uploader` must be called. + - The ZIP file must be removed after the upload. + - The `submit_job` method of the `study_submitter` must be called. + - The `save_study` method of the `data_repo` must be called. + """ - @pytest.mark.unit_test - def test_given_one_study_when_submit_fails_then_exception_is_raised( - self, my_launch_controller - ): - # given - my_launcher, expected_study = my_launch_controller - # when - my_launcher.env.upload_file = mock.Mock(return_value=True) - my_launcher.env.submit_job = mock.Mock(return_value=None) - my_launcher.repo.save_study = mock.Mock() - # then - with pytest.raises( - antareslauncher.use_cases.launch.study_submitter.FailedSubmissionException - ): - my_launcher.launch_all_studies() + class Uploader: + def __init__(self): + self.actual_names = frozenset() + + def upload(self, study: StudyDTO) -> None: + """Simulate the upload and check that the ZIP file has been correctly created.""" + zip_path = Path(study.zipfile_path) + with zipfile.ZipFile(zip_path, mode="r") as zf: + # keep only file names, excluding directories + self.actual_names = frozenset(name for name in zf.namelist() if "." in name) + study.zip_is_sent = True + + __call__ = upload + + upload = Uploader() + + def submit_job(study: StudyDTO) -> None: + """Simulate the submission of the job.""" + study.job_id = 40414243 + # Given + study_uploader = mock.Mock(spec=StudyZipfileUploader) + study_uploader.upload = upload + study_uploader.remove = mock.Mock() + + study_submitter = mock.Mock(spec=StudySubmitter) + study_submitter.submit_job = submit_job + + data_repo = mock.Mock(spec=DataRepoTinydb) + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = mock.Mock(side_effect=lambda x, **kwargs: x) + study_launcher = StudyLauncher(study_uploader, study_submitter, data_repo, display) + + # When + study_launcher.launch_study(ready_study) + + # Then + prefix_path = PurePosixPath(ready_study.name) + expected_names = frozenset([prefix_path.joinpath(name).as_posix() for name in STUDY_FILES]) + assert upload.actual_names == expected_names + + assert ready_study.zipfile_path, "The ZIP file should have been created" + assert ready_study.zip_is_sent, "The ZIP file should have been uploaded" + assert ready_study.job_id == 40414243, "The job should have been submitted" + assert not ready_study.with_error, "The study should not be marked as failed" + + assert not Path(ready_study.zipfile_path).exists(), "The ZIP file should have been removed" + + study_uploader.remove.assert_not_called() + data_repo.save_study.assert_called_once() + + @pytest.mark.parametrize("scenario", ["set_false", "raise_exception"]) @pytest.mark.unit_test - def test_given_one_study_when_zip_fails_then_return_none( - self, my_launch_controller - ): - # given - my_launcher, expected_study = my_launch_controller - my_launcher.file_manager.zip_dir_excluding_subdir = mock.Mock( - return_value=False - ) - # when - my_launcher.launch_all_studies() - # then - assert expected_study.zipfile_path is "" + def test_launch_study__upload_fails(self, ready_study: StudyDTO, scenario: str) -> None: + def upload(study: StudyDTO) -> None: + """Simulate the upload that fails""" + if scenario == "set_false": + study.zip_is_sent = False + elif scenario == "raise_exception": + raise Exception("Upload failed") + else: + raise NotImplementedError(scenario) + + # Given + study_uploader = mock.Mock(spec=StudyZipfileUploader) + study_uploader.upload = upload + + study_submitter = mock.Mock(spec=StudySubmitter) + + data_repo = mock.Mock(spec=DataRepoTinydb) + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = mock.Mock(side_effect=lambda x, **kwargs: x) + study_launcher = StudyLauncher(study_uploader, study_submitter, data_repo, display) + + # When + study_launcher.launch_study(ready_study) + + # Then + assert ready_study.zipfile_path, "The ZIP file should have been created" + assert not ready_study.zip_is_sent, "The ZIP file should not have been uploaded" + assert not ready_study.job_id, "The job should not have been submitted" + assert ready_study.with_error, "The study should be marked as failed" + + assert not Path(ready_study.zipfile_path).exists(), "The ZIP file should have been removed" + study_submitter.submit_job.assert_not_called() + study_uploader.remove.assert_called_once() + data_repo.save_study.assert_called_once() + + @pytest.mark.parametrize("scenario", ["set_null", "raise_exception"]) @pytest.mark.unit_test - def test_given_a_sent_study_when_launch_all_studies_called_then_file_manager_remove_zip_file_is_called_once( - self, my_launch_controller - ): - # given - my_launcher, expected_study = my_launch_controller - my_launcher._upload_zipfile = mock.Mock(return_value=True) - my_launcher.file_manager.remove_file = mock.Mock() - - # when - my_launcher.launch_all_studies() - # then - my_launcher.file_manager.remove_file.assert_called_once() + def test_launch_study__submit_job_fails(self, ready_study: StudyDTO, scenario: str) -> None: + def upload(study: StudyDTO) -> None: + study.zip_is_sent = True + + def submit_job(study: StudyDTO) -> None: + """Simulate the submission of the job that fails""" + if scenario == "set_null": + study.job_id = 0 + elif scenario == "raise_exception": + raise Exception("Simulation of submission failure") + else: + raise NotImplementedError(scenario) + + # Given + study_uploader = mock.Mock(spec=StudyZipfileUploader) + study_uploader.upload = upload + study_submitter = mock.Mock(spec=StudySubmitter) + study_submitter.submit_job = submit_job + + data_repo = mock.Mock(spec=DataRepoTinydb) + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = mock.Mock(side_effect=lambda x, **kwargs: x) + study_launcher = StudyLauncher(study_uploader, study_submitter, data_repo, display) + + # When + study_launcher.launch_study(ready_study) + + # Then + assert ready_study.zipfile_path, "The ZIP file should have been created" + assert ready_study.zip_is_sent, "The ZIP file should have been uploaded" + assert not ready_study.job_id, "The job should not have been submitted" + assert ready_study.with_error, "The study should be marked as failed" + + assert not Path(ready_study.zipfile_path).exists(), "The ZIP file should have been removed" + + study_uploader.remove.assert_called_once() + data_repo.save_study.assert_called_once() + + +class TestLaunchController: + def test_launch_all_studies__nominal_case(self, ready_study: StudyDTO) -> None: + # Given + data_repo = mock.Mock(spec=DataRepoTinydb) + data_repo.get_list_of_studies = mock.Mock(return_value=[ready_study]) + + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.upload_file = mock.Mock(return_value=True) + env.submit_job = mock.Mock(return_value=40414243) + + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = mock.Mock(side_effect=lambda x, **kwargs: x) + + launch_controller = LaunchController(data_repo, env, display) + + # When + launch_controller.launch_all_studies() + + # Then + assert ready_study.zipfile_path, "The ZIP file should have been created" + assert ready_study.zip_is_sent, "The ZIP file should have been uploaded" + assert ready_study.job_id == 40414243, "The job should have been submitted" + assert not ready_study.with_error, "The study should not be marked as failed" + + assert not Path(ready_study.zipfile_path).exists(), "The ZIP file should have been removed" + + data_repo.save_study.assert_called_once() + + def test_launch_all_studies__bad_studies( + self, + study_uploaded: StudyDTO, + study_submitted: StudyDTO, + ready_study: StudyDTO, + ) -> None: + """ + We want to check that even if some studies fail on download or submission, + all valid studies are processed correctly. + """ + + def upload_file(src: str) -> bool: + return "upload-failure" not in src + + class JobSubmitter: + def __init__(self): + self.job_id = 0 + + def __call__(self, study: StudyDTO) -> int: + self.job_id += 1 + return 0 if study.name == "submit-failure" else self.job_id + + # Given + data_repo = mock.Mock(spec=DataRepoTinydb) + studies = [study_uploaded, study_submitted, ready_study] + data_repo.get_list_of_studies = mock.Mock(return_value=studies) + + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.upload_file = upload_file + env.submit_job = JobSubmitter() + env.remove_input_zipfile = mock.Mock(return_value=True) + + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = mock.Mock(side_effect=lambda x, **kwargs: x) + + launch_controller = LaunchController(data_repo, env, display) + + # When + launch_controller.launch_all_studies() + + # Then + actual_states = [ + {"zip_is_sent": study.zip_is_sent, "job_id": study.job_id, "with_error": study.with_error} + for study in studies + ] + + # In case of upload failure, the remote ZIP file is + expected_states = [ + {"zip_is_sent": False, "job_id": 0, "with_error": True}, + {"zip_is_sent": False, "job_id": 0, "with_error": True}, + {"zip_is_sent": True, "job_id": 2, "with_error": False}, + ] + assert actual_states == expected_states diff --git a/tests/unit/launcher/test_submitter.py b/tests/unit/launcher/test_submitter.py index fa1384c..6bb729b 100644 --- a/tests/unit/launcher/test_submitter.py +++ b/tests/unit/launcher/test_submitter.py @@ -1,74 +1,56 @@ -from dataclasses import asdict from unittest import mock import pytest -import antareslauncher.use_cases -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.launch.study_submitter import StudySubmitter class TestStudySubmitter: - def setup_method(self): - self.remote_env = mock.Mock(spec_set=RemoteEnvironmentWithSlurm) - self.display_mock = mock.Mock(spec_set=IDisplay) - self.study_submitter = StudySubmitter(self.remote_env, self.display_mock) - + @pytest.mark.parametrize("actual_job_id", [0, 123456]) @pytest.mark.unit_test - def test_submit_study_shows_message_if_submit_succeeds(self): - self.remote_env.submit_job = mock.Mock(return_value=42) - study = StudyDTO(path="hello") - - new_study = self.study_submitter.submit_job(study) - - expected_message = f'"hello": was submitted' - self.display_mock.show_message.assert_called_once_with( - expected_message, mock.ANY - ) - assert new_study.job_id == 42 - - @pytest.mark.unit_test - def test_submit_study_shows_error_if_submit_fails_and_exception_is_raised( - self, - ): - self.remote_env.submit_job = mock.Mock(return_value=None) - study = StudyDTO(path="hello") - - with pytest.raises( - antareslauncher.use_cases.launch.study_submitter.FailedSubmissionException - ): - self.study_submitter.submit_job(study) - - expected_error_message = f'"hello": was not submitted' - self.display_mock.show_error.assert_called_once_with( - expected_error_message, mock.ANY - ) + def test_submit_job__nominal_case(self, pending_study: StudyDTO, actual_job_id: int) -> None: + # Given + pending_study.job_id = actual_job_id + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.submit_job = mock.Mock(return_value=987654) + display = mock.Mock(spec=DisplayTerminal) + submitter = StudySubmitter(env, display) + + # When + submitter.submit_job(pending_study) + + # Then + if actual_job_id: + # The study job_id is not changed + assert pending_study.job_id == actual_job_id + # The display shows a message + display.show_message.assert_called_once() + display.show_error.assert_not_called() + else: + # The study job_id is changed + assert pending_study.job_id == 987654 + # The display shows a message + display.show_message.assert_called_once() + display.show_error.assert_not_called() @pytest.mark.unit_test - def test_remote_env_not_called_if_study_has_already_a_jobid(self): - self.remote_env.submit_job = mock.Mock() - study = StudyDTO(path="hello") - study.job_id = 42 - - self.study_submitter.submit_job(study) - - self.remote_env.submit_job.assert_not_called() - - @pytest.mark.unit_test - def test_remote_env_is_called_if_study_has_no_jobid(self): - self.remote_env.submit_job = mock.Mock(return_value=42) - study = StudyDTO(path="hello") - study.zipfile_path = "ciao.zip" - study.job_id = None - - new_study = self.study_submitter.submit_job(study) - - self.remote_env.submit_job.assert_called_once() - first_call = self.remote_env.submit_job.call_args_list[0] - first_argument = first_call[0][0] - assert asdict(first_argument) == asdict(study) - assert new_study.job_id is 42 + def test_submit_job__error_case(self, pending_study: StudyDTO) -> None: + # Given + pending_study.job_id = 0 + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.submit_job = mock.Mock(return_value=0) + display = mock.Mock(spec=DisplayTerminal) + submitter = StudySubmitter(env, display) + + # When + submitter.submit_job(pending_study) + + # Then + # The study job_id is not changed + assert pending_study.job_id == 0 + # The display shows an error + display.show_message.assert_not_called() + display.show_error.assert_called_once() diff --git a/tests/unit/launcher/test_zip_uploader.py b/tests/unit/launcher/test_zip_uploader.py index 5cb013e..094f04b 100644 --- a/tests/unit/launcher/test_zip_uploader.py +++ b/tests/unit/launcher/test_zip_uploader.py @@ -1,80 +1,88 @@ -from copy import copy from unittest import mock -from unittest.mock import call import pytest -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO -from antareslauncher.use_cases.launch.study_zip_uploader import ( - FailedUploadException, - StudyZipfileUploader, -) +from antareslauncher.use_cases.launch.study_zip_uploader import StudyZipfileUploader -class TestZipfileUploader: - def setup_method(self): - self.remote_env = mock.Mock(spec_set=RemoteEnvironmentWithSlurm) - self.display_mock = mock.Mock(spec_set=IDisplay) - self.study_uploader = StudyZipfileUploader(self.remote_env, self.display_mock) - +class TestStudyZipfileUploader: + @pytest.mark.parametrize("actual_sent_flag", [True, False]) @pytest.mark.unit_test - def test_upload_study_shows_message_if_upload_succeeds(self): - self.remote_env.upload_file = mock.Mock(return_value=True) - study = StudyDTO(path="hello") - - self.study_uploader.upload(study) - - expected_message1 = f'"hello": uploading study ...' - expected_message2 = f'"hello": was uploaded' - calls = [ - call(expected_message1, mock.ANY), - call(expected_message2, mock.ANY), - ] - self.display_mock.show_message.assert_has_calls(calls) + def test_upload__nominal_case(self, pending_study: StudyDTO, actual_sent_flag: bool) -> None: + # Given + pending_study.zip_is_sent = actual_sent_flag + display = mock.Mock(spec=DisplayTerminal) + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.upload_file = mock.Mock(return_value=True) + uploader = StudyZipfileUploader(env, display) + + # When + uploader.upload(pending_study) + + # Then + if actual_sent_flag: + env.upload_file.assert_not_called() + display.show_message.assert_called_once() + else: + env.upload_file.assert_called_once_with(pending_study.zipfile_path) + assert display.show_message.call_count == 2 + display.show_error.assert_not_called() + assert pending_study.zip_is_sent @pytest.mark.unit_test - def test_upload_study_shows_error_if_upload_fails_and_exception_is_raised( - self, - ): - self.remote_env.upload_file = mock.Mock(return_value=False) - study = StudyDTO(path="hello") - - with pytest.raises(FailedUploadException): - self.study_uploader.upload(study) - - expected_welcome_message = f'"hello": uploading study ...' - expected_error_message = f'"hello": was not uploaded' - self.display_mock.show_message.assert_called_once_with( - expected_welcome_message, mock.ANY - ) - self.display_mock.show_error.assert_called_once_with( - expected_error_message, mock.ANY - ) - + def test_upload__error_case(self, pending_study: StudyDTO) -> None: + # Given + display = mock.Mock(spec=DisplayTerminal) + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.upload_file = mock.Mock(return_value=False) + uploader = StudyZipfileUploader(env, display) + + # When + uploader.upload(pending_study) + + # Then + env.upload_file.assert_called_once_with(pending_study.zipfile_path) + assert display.show_message.call_count == 1 + assert display.show_error.call_count == 1 + assert not pending_study.zip_is_sent + + @pytest.mark.parametrize("actual_sent_flag", [True, False]) @pytest.mark.unit_test - def test_remote_env_not_called_if_upload_was_done(self): - self.remote_env.upload_file = mock.Mock() - study = StudyDTO(path="hello") - study.zip_is_sent = True - - new_study = self.study_uploader.upload(study) - - self.remote_env.upload_file.assert_not_called() - assert new_study == study + def test_remove__nominal_case(self, pending_study: StudyDTO, actual_sent_flag: bool) -> None: + # Given + pending_study.zip_is_sent = actual_sent_flag + display = mock.Mock(spec=DisplayTerminal) + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.remove_input_zipfile = mock.Mock(return_value=True) + uploader = StudyZipfileUploader(env, display) + + # When + uploader.remove(pending_study) + + # Then + # NOTE: The remote ZIP file is always removed even if `zip_is_sent` is `False`. + env.remove_input_zipfile.assert_called_once_with(pending_study) + display.show_message.assert_called_once() + display.show_error.assert_not_called() + assert not pending_study.zip_is_sent @pytest.mark.unit_test - def test_remote_env_is_called_if_upload_was_not_done(self): - self.remote_env.upload_file = mock.Mock() - study = StudyDTO(path="hello") - study.zip_is_sent = False - expected_study = copy(study) - expected_study.zip_is_sent = True - - new_study = self.study_uploader.upload(study) - - self.remote_env.upload_file.assert_called_once_with(study.zipfile_path) - assert new_study == expected_study + def test_remove__error_case(self, pending_study: StudyDTO) -> None: + # Given + pending_study.zip_is_sent = True + display = mock.Mock(spec=DisplayTerminal) + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.remove_input_zipfile = mock.Mock(return_value=False) + uploader = StudyZipfileUploader(env, display) + + # When + uploader.remove(pending_study) + + # Then + env.remove_input_zipfile.assert_called_once_with(pending_study) + display.show_message.assert_not_called() + display.show_error.assert_called_once() + assert pending_study.zip_is_sent diff --git a/tests/unit/launcher/test_zipper.py b/tests/unit/launcher/test_zipper.py deleted file mode 100644 index dfd5562..0000000 --- a/tests/unit/launcher/test_zipper.py +++ /dev/null @@ -1,67 +0,0 @@ -import getpass -from unittest import mock - -import pytest - -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.file_manager.file_manager import FileManager -from antareslauncher.study_dto import StudyDTO -from antareslauncher.use_cases.launch.study_zipper import StudyZipper - - -class TestStudyZipper: - def setup_method(self): - self.file_manager = mock.Mock(spec_set=FileManager) - self.display_mock = mock.Mock(spec_set=IDisplay) - self.study_zipper = StudyZipper(self.file_manager, self.display_mock) - - @pytest.mark.unit_test - def test_zip_study_show_message_if_zip_succeeds(self): - self.file_manager.zip_dir_excluding_subdir = mock.Mock(return_value=True) - study = StudyDTO(path="hello") - - self.study_zipper.zip(study) - - expected_message = '"hello": was zipped' - self.display_mock.show_message.assert_called_once_with( - expected_message, mock.ANY - ) - - @pytest.mark.unit_test - def test_zip_study_show_error_if_zip_fails(self): - self.file_manager.zip_dir_excluding_subdir = mock.Mock(return_value=False) - study = StudyDTO(path="hello") - - new_study = self.study_zipper.zip(study) - - expected_message = '"hello": was not zipped' - self.display_mock.show_error.assert_called_once_with(expected_message, mock.ANY) - assert new_study.zipfile_path == "" - - @pytest.mark.unit_test - def test_file_manager_not_called_if_zip_exists(self): - self.file_manager.zip_dir_excluding_subdir = mock.Mock() - study = StudyDTO(path="hello") - study.zipfile_path = "ciao.zip" - - new_zip = self.study_zipper.zip(study) - - self.file_manager.zip_dir_excluding_subdir.assert_not_called() - self.display_mock.show_error.assert_not_called() - self.display_mock.show_message.assert_not_called() - assert new_zip == study - - @pytest.mark.unit_test - def test_file_manager_is_called_if_zip_doesnt_exist(self): - self.file_manager.zip_dir_excluding_subdir = mock.Mock(return_value=True) - study_path = "hello" - study = StudyDTO(path=study_path) - study.zipfile_path = "" - - new_study = self.study_zipper.zip(study) - - expected_zipfile_path = f"{study.path}-{getpass.getuser()}.zip" - self.file_manager.zip_dir_excluding_subdir.assert_called_once_with( - study_path, expected_zipfile_path, None - ) - assert new_study.zipfile_path == expected_zipfile_path diff --git a/tests/unit/retriever/conftest.py b/tests/unit/retriever/conftest.py new file mode 100644 index 0000000..07e5467 --- /dev/null +++ b/tests/unit/retriever/conftest.py @@ -0,0 +1,48 @@ +import dataclasses +from pathlib import Path + +import pytest + +from antareslauncher.study_dto import StudyDTO + + +@pytest.fixture(name="pending_study") +def pending_study_fixture(tmp_path: Path) -> StudyDTO: + study_path = tmp_path.joinpath("My Study") + job_log_dir = tmp_path.joinpath("LOG_DIR") + output_dir = tmp_path.joinpath("OUTPUT_DIR") + return StudyDTO( + path=str(study_path), + started=False, + job_log_dir=str(job_log_dir), + output_dir=str(output_dir), + zipfile_path="", + zip_is_sent=False, + job_id=0, + ) + + +@pytest.fixture(name="started_study") +def started_study_fixture(pending_study: StudyDTO) -> StudyDTO: + study_dir = Path(pending_study.path) + zip_name = f"{study_dir.name}-john_doe.zip" + zip_path = study_dir.parent / zip_name + return dataclasses.replace( + pending_study, + started=True, + finished=False, + with_error=False, + zipfile_path=str(zip_path), + zip_is_sent=True, + job_id=46505574, + ) + + +@pytest.fixture(name="finished_study") +def finished_study_fixture(started_study: StudyDTO) -> StudyDTO: + return dataclasses.replace(started_study, finished=True, with_error=False) + + +@pytest.fixture(name="with_error_study") +def with_error_study_fixture(started_study: StudyDTO) -> StudyDTO: + return dataclasses.replace(started_study, finished=True, with_error=True) diff --git a/tests/unit/retriever/test_download_final_zip.py b/tests/unit/retriever/test_download_final_zip.py index b477c7f..b2a0cde 100644 --- a/tests/unit/retriever/test_download_final_zip.py +++ b/tests/unit/retriever/test_download_final_zip.py @@ -1,119 +1,144 @@ -from copy import copy +import typing as t from pathlib import Path from unittest import mock -from unittest.mock import call import pytest -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO -from antareslauncher.use_cases.retrieve.download_final_zip import ( - FinalZipDownloader, - FinalZipNotDownloadedException, -) +from antareslauncher.use_cases.retrieve.download_final_zip import FinalZipDownloader + + +def download_final_zip(study: StudyDTO) -> t.Optional[Path]: + """Simulate the download of the final ZIP.""" + dst_dir = Path(study.output_dir) # must exist + out_path = dst_dir.joinpath(f"finished_{study.name}_{study.job_id}.zip") + out_path.write_bytes(b"PK fake zip") + return out_path class TestFinalZipDownloader: - def setup_method(self): - self.remote_env = mock.Mock(spec_set=RemoteEnvironmentWithSlurm) - self.display_mock = mock.Mock(spec_set=IDisplay) - self.final_zip_downloader = FinalZipDownloader( - self.remote_env, self.display_mock - ) - - @pytest.fixture(scope="function") - def successfully_finished_zip_study(self): - return StudyDTO( - path="path/hello", - started=True, - finished=True, - with_error=False, - job_id=42, - ) + @pytest.mark.unit_test + def test_download__pending_study(self, pending_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = FinalZipDownloader(env=env, display=display) + downloader.download(pending_study) + + # Check the result + env.download_final_zip.assert_not_called() + display.show_message.assert_not_called() + display.show_error.assert_not_called() @pytest.mark.unit_test - def test_download_study_shows_message_if_succeeds( - self, successfully_finished_zip_study - ): - final_zipfile_path = "results.zip" - self.remote_env.download_final_zip = mock.Mock(return_value=final_zipfile_path) - - self.final_zip_downloader.download(successfully_finished_zip_study) - expected_message1 = '"hello": downloading final ZIP...' - expected_message2 = '"hello": Final ZIP downloaded' - calls = [ - call(expected_message1, mock.ANY), - call(expected_message2, mock.ANY), - ] - self.display_mock.show_message.assert_has_calls(calls) + def test_download__started_study(self, started_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = FinalZipDownloader(env=env, display=display) + downloader.download(started_study) + + # Check the result + env.download_final_zip.assert_not_called() + display.show_message.assert_not_called() + display.show_error.assert_not_called() @pytest.mark.unit_test - def test_download_study_shows_error_and_raises_exceptions_if_failure( - self, successfully_finished_zip_study - ): - self.remote_env.download_final_zip = mock.Mock(return_value=None) - - with pytest.raises(FinalZipNotDownloadedException): - self.final_zip_downloader.download(successfully_finished_zip_study) - - expected_welcome_message = '"hello": downloading final ZIP...' - expected_error_message = '"hello": Final ZIP not downloaded' - self.display_mock.show_message.assert_called_once_with( - expected_welcome_message, mock.ANY - ) - self.display_mock.show_error.assert_called_once_with( - expected_error_message, mock.ANY - ) + def test_download__with_error_study(self, with_error_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = FinalZipDownloader(env=env, display=display) + downloader.download(with_error_study) + + # Check the result + env.download_final_zip.assert_not_called() + display.show_message.assert_not_called() + display.show_error.assert_not_called() + + @pytest.mark.unit_test + def test_download__finished_study__download_ok(self, finished_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.download_final_zip = download_final_zip + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = FinalZipDownloader(env=env, display=display) + downloader.download(finished_study) + + # Check the result: one ZIP file is downloaded + assert finished_study.local_final_zipfile_path + assert display.show_message.call_count == 2 # two messages + assert display.show_error.call_count == 0 # no error + output_dir = Path(finished_study.output_dir) + assert output_dir.is_dir() + zip_files = list(output_dir.iterdir()) + assert len(zip_files) == 1 @pytest.mark.unit_test - def test_remote_env_not_called_if_final_zip_already_downloaded(self): - self.remote_env.download_final_zip = mock.Mock() - downloaded_study = StudyDTO("hello") - downloaded_study.local_final_zipfile_path = "results.zip" + def test_download__finished_study__reentrancy(self, finished_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.download_final_zip = download_final_zip + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download twice + downloader = FinalZipDownloader(env=env, display=display) + downloader.download(finished_study) + + output_dir1 = Path(finished_study.output_dir) + zip_files1 = set(output_dir1.iterdir()) + downloader.download(finished_study) - new_study = self.final_zip_downloader.download(downloaded_study) + # Check the result: one ZIP file is downloaded + assert finished_study.local_final_zipfile_path + assert display.show_message.call_count == 2 + assert display.show_error.call_count == 0 - self.remote_env.download_final_zip.assert_not_called() - assert new_study == downloaded_study + # ZIP files are not duplicated + output_dir2 = Path(finished_study.output_dir) + zip_files2 = set(output_dir2.iterdir()) + assert zip_files1 == zip_files2 @pytest.mark.unit_test - @pytest.mark.parametrize( - "finished,with_error", - [ - (False, False), - (True, True), - ], - ) - def test_remote_env_not_called_if_final_zip_not_successfully_finished( - self, finished, with_error - ): - self.remote_env.download_final_zip = mock.Mock() - not_finished_study = StudyDTO("hello", finished=finished, with_error=with_error) - - new_study = self.final_zip_downloader.download(not_finished_study) - - self.remote_env.download_final_zip.assert_not_called() - assert new_study == not_finished_study + def test_download__finished_study__download_nothing(self, finished_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.download_final_zip = lambda _: [] + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = FinalZipDownloader(env=env, display=display) + downloader.download(finished_study) + + # Check the result: no ZIP file is downloaded + assert not finished_study.local_final_zipfile_path + assert display.show_message.call_count == 1 # only the first message + assert display.show_error.call_count == 1 + output_dir = Path(finished_study.output_dir) + assert output_dir.is_dir() + zip_files = list(output_dir.iterdir()) + assert not zip_files @pytest.mark.unit_test - def test_remote_env_is_called_if_final_zip_not_yet_downloaded( - self, successfully_finished_zip_study - ): - final_zipfile_path = "results.zip" - self.remote_env.download_final_zip = mock.Mock( - return_value=Path(final_zipfile_path) - ) - - new_study = self.final_zip_downloader.download(successfully_finished_zip_study) - - self.remote_env.download_final_zip.assert_called_once() - first_call = self.remote_env.download_final_zip.call_args_list[0] - first_argument = first_call[0][0] - assert first_argument == successfully_finished_zip_study - - expected_final_study = copy(successfully_finished_zip_study) - expected_final_study.local_final_zipfile_path = final_zipfile_path - assert new_study == expected_final_study + def test_download__finished_study__download_error(self, finished_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.download_final_zip.side_effect = Exception("Connection error") + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = FinalZipDownloader(env=env, display=display) + with pytest.raises(Exception, match=r"Connection\s+error"): + downloader.download(finished_study) + + # Check the result: the exception is not managed + assert not finished_study.local_final_zipfile_path + assert display.show_message.call_count == 1 # only the first message + display.show_error.assert_not_called() + output_dir = Path(finished_study.output_dir) + assert output_dir.is_dir() + zip_files = list(output_dir.iterdir()) + assert not zip_files diff --git a/tests/unit/retriever/test_final_zip_extractor.py b/tests/unit/retriever/test_final_zip_extractor.py index a7dffc4..7817dc0 100644 --- a/tests/unit/retriever/test_final_zip_extractor.py +++ b/tests/unit/retriever/test_final_zip_extractor.py @@ -1,72 +1,204 @@ +import dataclasses +import zipfile +from pathlib import Path from unittest import mock import pytest -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.file_manager.file_manager import FileManager +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.study_dto import StudyDTO -from antareslauncher.use_cases.retrieve.final_zip_extractor import ( - FinalZipExtractor, - ResultNotExtractedException, -) +from antareslauncher.use_cases.retrieve.final_zip_extractor import FinalZipExtractor + + +def create_final_zip(study: StudyDTO, *, scenario: str = "nominal_study") -> str: + """Prepare a final ZIP.""" + dst_dir = Path(study.output_dir) # must exist + dst_dir.mkdir(parents=True, exist_ok=True) + out_path = dst_dir.joinpath(f"finished_{study.name}_{study.job_id}.zip") + if scenario == "nominal_study": + with zipfile.ZipFile( + out_path, + mode="w", + compression=zipfile.ZIP_DEFLATED, + ) as zf: + zf.writestr( + f"{study.name}/input/study.antares", + data=b"[antares]\nversion = 860\n", + ) + zf.writestr( + f"{study.name}/output/20230922-1601eco/simulation.log", + data=b"Simulation OK", + ) + elif scenario == "nominal_results": + with zipfile.ZipFile( + out_path, + mode="w", + compression=zipfile.ZIP_DEFLATED, + ) as zf: + zf.writestr("simulation.log", data=b"Simulation OK") + elif scenario == "corrupted": + out_path.write_bytes(b"PK corrupted content") + elif scenario == "missing": + pass + else: + raise NotImplementedError(scenario) + return str(out_path) class TestFinalZipExtractor: - def setup_method(self): - self.file_manager = mock.Mock(spec_set=FileManager) - self.display_mock = mock.Mock(spec_set=IDisplay) - self.zip_extractor = FinalZipExtractor(self.file_manager, self.display_mock) - - @pytest.fixture(scope="function") - def study_to_extract(self): - local_zip = "results.zip" - study = StudyDTO( - path="hello", - local_final_zipfile_path=local_zip, - final_zip_extracted=False, - ) - return study + @pytest.mark.unit_test + def test_extract_final_zip__pending_study(self, pending_study: StudyDTO) -> None: + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the ZIP extraction + extractor = FinalZipExtractor(display=display) + extractor.extract_final_zip(pending_study) + + # Check the result + display.show_message.assert_not_called() + display.show_error.assert_not_called() + assert not pending_study.final_zip_extracted + + @pytest.mark.unit_test + def test_extract_final_zip__started_study(self, started_study: StudyDTO) -> None: + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the ZIP extraction + extractor = FinalZipExtractor(display=display) + extractor.extract_final_zip(started_study) + + # Check the result + display.show_message.assert_not_called() + display.show_error.assert_not_called() + assert not started_study.final_zip_extracted + + @pytest.mark.unit_test + def test_extract_final_zip__finished_study__no_output(self, finished_study: StudyDTO) -> None: + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the ZIP extraction + extractor = FinalZipExtractor(display=display) + extractor.extract_final_zip(finished_study) + + # Check the result + display.show_message.assert_not_called() + display.show_error.assert_not_called() + assert not finished_study.final_zip_extracted + + @pytest.mark.unit_test + def test_extract_final_zip__finished_study__nominal_study(self, finished_study: StudyDTO) -> None: + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = lambda names, *args, **kwargs: names + + # Prepare a valid final ZIP + finished_study.local_final_zipfile_path = create_final_zip(finished_study, scenario="nominal_study") + + # Initialize and execute the ZIP extraction + extractor = FinalZipExtractor(display=display) + extractor.extract_final_zip(finished_study) + + # Check the result + display.show_message.assert_called_once() + display.show_error.assert_not_called() + + assert finished_study.final_zip_extracted + assert not finished_study.with_error + + result_dir = Path(finished_study.output_dir).joinpath(finished_study.name) + expected_files = [ + "input/study.antares", + "output/20230922-1601eco/simulation.log", + ] + for file in expected_files: + assert result_dir.joinpath(file).is_file() @pytest.mark.unit_test - def test_extract_zip_show_message_if_zip_succeeds(self, study_to_extract): - self.file_manager.unzip = mock.Mock(return_value=True) + def test_extract_final_zip__finished_study__nominal_results(self, finished_study: StudyDTO) -> None: + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = lambda names, *args, **kwargs: names + + # Prepare a valid final ZIP + finished_study.local_final_zipfile_path = create_final_zip(finished_study, scenario="nominal_results") + + # Initialize and execute the ZIP extraction + extractor = FinalZipExtractor(display=display) + extractor.extract_final_zip(finished_study) - self.zip_extractor.extract_final_zip(study_to_extract) + # Check the result + display.show_message.assert_called_once() + display.show_error.assert_not_called() - expected_message = f'"hello": Final zip extracted' - self.display_mock.show_message.assert_called_once_with( - expected_message, mock.ANY - ) + assert finished_study.final_zip_extracted + assert not finished_study.with_error + + result_dir = Path(finished_study.local_final_zipfile_path).with_suffix("") + assert result_dir.joinpath("simulation.log").is_file() @pytest.mark.unit_test - def test_extract_zip_show_error_and_raises_exception_if_zip_fails( - self, study_to_extract - ): - self.file_manager.unzip = mock.Mock(return_value=False) + def test_extract_final_zip__finished_study__reentrancy(self, finished_study: StudyDTO) -> None: + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = lambda names, *args, **kwargs: names + + # Prepare a valid final ZIP + finished_study.local_final_zipfile_path = create_final_zip(finished_study) - with pytest.raises(ResultNotExtractedException): - self.zip_extractor.extract_final_zip(study_to_extract) + # Initialize and execute the ZIP extraction twice + extractor = FinalZipExtractor(display=display) + extractor.extract_final_zip(finished_study) + study_state1 = dataclasses.asdict(finished_study) - expected_error = f'"hello": Final zip not extracted' - self.display_mock.show_error.assert_called_once_with(expected_error, mock.ANY) + extractor = FinalZipExtractor(display=display) + extractor.extract_final_zip(finished_study) + study_state2 = dataclasses.asdict(finished_study) + + assert study_state1 == study_state2 @pytest.mark.unit_test - def test_file_manager_not_called_if_study_should_not_be_extracted(self): - self.file_manager.unzip = mock.Mock() - empty_study = StudyDTO("hello") + def test_extract_final_zip__finished_study__missing(self, finished_study: StudyDTO) -> None: + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = lambda names, *args, **kwargs: names + + # Prepare a missing final ZIP + finished_study.local_final_zipfile_path = create_final_zip(finished_study, scenario="missing") - new_study = self.zip_extractor.extract_final_zip(empty_study) - self.file_manager.unzip.assert_not_called() - self.display_mock.show_error.assert_not_called() - self.display_mock.show_message.assert_not_called() - assert new_study == empty_study + # Initialize and execute the ZIP extraction + extractor = FinalZipExtractor(display=display) + extractor.extract_final_zip(finished_study) + + # Check the result + display.show_message.assert_not_called() + display.show_error.assert_called_once() + + assert not finished_study.final_zip_extracted + assert finished_study.with_error + + result_dirs = [ + Path(finished_study.output_dir).joinpath(finished_study.name), + Path(finished_study.local_final_zipfile_path).with_suffix(""), + ] + assert not any(result_dir.exists() for result_dir in result_dirs) @pytest.mark.unit_test - def test_file_manager_is_called_if_study_is_ready(self, study_to_extract): - self.file_manager.unzip = mock.Mock(return_value=True) - - new_study = self.zip_extractor.extract_final_zip(study_to_extract) - self.file_manager.unzip.assert_called_once_with( - study_to_extract.local_final_zipfile_path - ) - assert new_study.final_zip_extracted is True + def test_extract_final_zip__finished_study__corrupted(self, finished_study: StudyDTO) -> None: + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = lambda names, *args, **kwargs: names + + # Prepare a corrupted final ZIP + finished_study.local_final_zipfile_path = create_final_zip(finished_study, scenario="corrupted") + + # Initialize and execute the ZIP extraction + extractor = FinalZipExtractor(display=display) + extractor.extract_final_zip(finished_study) + + # Check the result + display.show_message.assert_not_called() + display.show_error.assert_called_once() + + assert not finished_study.final_zip_extracted + assert finished_study.with_error + + result_dirs = [ + Path(finished_study.output_dir).joinpath(finished_study.name), + Path(finished_study.local_final_zipfile_path).with_suffix(""), + ] + assert not any(result_dir.exists() for result_dir in result_dirs) diff --git a/tests/unit/retriever/test_log_downloader.py b/tests/unit/retriever/test_log_downloader.py index 2f729b8..b6335b8 100644 --- a/tests/unit/retriever/test_log_downloader.py +++ b/tests/unit/retriever/test_log_downloader.py @@ -1,110 +1,118 @@ -from copy import copy +import typing as t from pathlib import Path from unittest import mock import pytest -import antareslauncher.remote_environnement.remote_environment_with_slurm -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.retrieve.log_downloader import LogDownloader +def download_logs(study: StudyDTO) -> t.List[Path]: + """Simulate the download of logs.""" + dst_dir = Path(study.job_log_dir) # must exist + out_path = dst_dir.joinpath(f"antares-out-{study.job_id}.txt") + out_path.write_text("Quitting the solver gracefully.") + err_path = dst_dir.joinpath(f"antares-err-{study.job_id}.txt") + err_path.write_text("No error") + return [out_path, err_path] + + class TestLogDownloader: - def setup_method(self): - self.remote_env_mock = mock.Mock(spec=RemoteEnvironmentWithSlurm) - self.file_manager = mock.Mock() - self.display_mock = mock.Mock(spec_set=IDisplay) - self.log_downloader = LogDownloader( - self.remote_env_mock, self.file_manager, self.display_mock - ) - - @pytest.fixture(scope="function") - def started_study(self): - study = StudyDTO(path="path/hello") - study.started = True - study.job_id = 42 - study.job_log_dir = "ROOT_LOG_DIR" - return study - - def test_download_shows_message_if_successful(self, started_study): - self.remote_env_mock.download_logs = mock.Mock(return_value=True) - self.log_downloader.run(started_study) - - expected_message = '"hello": Logs downloaded' - self.display_mock.show_message.assert_called_once_with( - expected_message, mock.ANY - ) - - def test_download_shows_error_if_fails_and_only_study_logdir_is_changed( - self, started_study - ): - self.remote_env_mock.download_logs = mock.Mock(return_value=False) - log_dir_name = f"{started_study.name}_{started_study.job_id}" - expected_job_log_dir = str(Path(started_study.job_log_dir) / log_dir_name) - expected_study = copy(started_study) - expected_study.job_log_dir = expected_job_log_dir - - new_study = self.log_downloader.run(started_study) - - expected_message = '"hello": Logs not downloaded' - self.display_mock.show_error.assert_called_once_with(expected_message, mock.ANY) - assert new_study == expected_study - - def test_file_manager_and_remote_env_not_called_if_study_not_started(self): - self.remote_env_mock.download_logs = mock.Mock() - self.file_manager.make_dir = mock.Mock() - study = StudyDTO(path="hello") - study.started = False - self.log_downloader.run(study) - - self.remote_env_mock.download_logs.assert_not_called() - self.file_manager.make_dir.assert_not_called() - - def test_file_manager_is_called_to_create_logdir_if_study_started( - self, started_study - ): - self.remote_env_mock.download_logs = mock.Mock(return_value=True) - self.file_manager.make_dir = mock.Mock() - - self.log_downloader.run(started_study) - - log_dir_name = f"{started_study.name}_{started_study.job_id}" - expected_job_log_dir = str(Path(started_study.job_log_dir) / log_dir_name) - self.file_manager.make_dir.assert_called_once_with(expected_job_log_dir) - - def test_make_manager_is_called_properly_even_if_logdir_was_already_previously_set( - self, started_study - ): - self.remote_env_mock.download_logs = mock.Mock(return_value=True) - self.file_manager.make_dir = mock.Mock() - log_dir_name = f"{started_study.name}_{started_study.job_id}" - expected_job_log_dir = str(Path(started_study.job_log_dir) / log_dir_name) - started_study.job_log_dir = expected_job_log_dir - - expected_study = copy(started_study) - expected_study.job_log_dir = expected_job_log_dir - - self.log_downloader.run(started_study) - - self.file_manager.make_dir.assert_called_once_with(expected_job_log_dir) - - def test_environment_download_logs_is_called_if_study_started(self, started_study): - log_dir_name = f"{started_study.name}_{started_study.job_id}" - log_path = Path(started_study.job_log_dir) / log_dir_name - self.remote_env_mock.download_logs = mock.Mock(return_value=[log_path]) - - expected_job_log_dir = str(log_path) - expected_study = copy(started_study) - expected_study.job_log_dir = expected_job_log_dir - - new_study = self.log_downloader.run(started_study) - - first_call = self.remote_env_mock.download_logs.call_args_list[0] - first_argument = first_call[0][0] - assert first_argument == expected_study - assert new_study.job_log_dir == expected_job_log_dir - assert new_study.logs_downloaded is True + @pytest.mark.unit_test + def test_run__pending_study(self, pending_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = LogDownloader(env=env, display=display) + downloader.run(pending_study) + + # Check the result + env.download_logs.assert_not_called() + display.show_message.assert_not_called() + display.show_error.assert_not_called() + + @pytest.mark.unit_test + def test_run__started_study__download_ok(self, started_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.download_logs = download_logs + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = LogDownloader(env=env, display=display) + downloader.run(started_study) + + # Check the result: two log files are downloaded + assert started_study.logs_downloaded + display.show_message.assert_called_once() + display.show_error.assert_not_called() + job_log_dir = Path(started_study.job_log_dir) + assert job_log_dir.is_dir() + log_files = list(job_log_dir.iterdir()) + assert len(log_files) == 2 + + @pytest.mark.unit_test + def test_run__started_study__reentrancy(self, started_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.download_logs = download_logs + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download twice + downloader = LogDownloader(env=env, display=display) + downloader.run(started_study) + + job_log_dir1 = Path(started_study.job_log_dir) + log_files1 = set(job_log_dir1.iterdir()) + downloader.run(started_study) + + # Check the result: two log files are downloaded + assert started_study.logs_downloaded + assert display.show_message.call_count == 2 + assert display.show_error.call_count == 0 + + # Log files are not duplicated + job_log_dir2 = Path(started_study.job_log_dir) + log_files2 = set(job_log_dir2.iterdir()) + assert log_files1 == log_files2 + + @pytest.mark.unit_test + def test_run__started_study__download_nothing(self, started_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.download_logs = lambda _: [] + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = LogDownloader(env=env, display=display) + downloader.run(started_study) + + # Check the result: no log file is downloaded + assert not started_study.logs_downloaded + display.show_message.assert_not_called() + display.show_error.assert_called_once() + job_log_dir = Path(started_study.job_log_dir) + assert job_log_dir.is_dir() + log_files = list(job_log_dir.iterdir()) + assert not log_files + + @pytest.mark.unit_test + def test_run__started_study__download_error(self, started_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.download_logs.side_effect = Exception("Connection error") + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = LogDownloader(env=env, display=display) + with pytest.raises(Exception, match=r"Connection\s+error"): + downloader.run(started_study) + + # Check the result: the exception is not managed + assert not started_study.logs_downloaded + display.show_message.assert_not_called() + display.show_error.assert_not_called() + job_log_dir = Path(started_study.job_log_dir) + assert job_log_dir.is_dir() + log_files = list(job_log_dir.iterdir()) + assert not log_files diff --git a/tests/unit/retriever/test_retrieve_controller.py b/tests/unit/retriever/test_retrieve_controller.py index 2a970a5..c28372f 100644 --- a/tests/unit/retriever/test_retrieve_controller.py +++ b/tests/unit/retriever/test_retrieve_controller.py @@ -4,12 +4,8 @@ import pytest -import antareslauncher -import antareslauncher.remote_environnement.remote_environment_with_slurm -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.retrieve.retrieve_controller import RetrieveController from antareslauncher.use_cases.retrieve.state_updater import StateUpdater @@ -17,73 +13,24 @@ class TestRetrieveController: def setup_method(self): - self.remote_env_mock = mock.Mock(spec=RemoteEnvironmentWithSlurm) - self.file_manager = mock.Mock() + self.env = mock.Mock(spec=RemoteEnvironmentWithSlurm) self.data_repo = mock.Mock() - self.display = mock.Mock() - self.state_updater_mock = StateUpdater(self.remote_env_mock, self.display) - - @pytest.fixture(scope="function") - def my_study(self): - return antareslauncher.study_dto.StudyDTO("") - - @pytest.fixture(scope="function") - def my_running_study(self): - study = antareslauncher.study_dto.StudyDTO( - job_id=42, - started=True, - finished=False, - with_error=False, - path="path", - ) - return study - - @pytest.fixture(scope="function") - def my_finished_study(self): - study = antareslauncher.study_dto.StudyDTO( - job_id=42, - started=True, - finished=True, - with_error=False, - path="path", - ) - return study - - @pytest.fixture(scope="function") - def my_downloaded_study(self): - study = antareslauncher.study_dto.StudyDTO( - job_id=42, - started=True, - finished=True, - with_error=False, - local_final_zipfile_path="local_final_zipfile_path", - path="path", - ) - return study + self.display = mock.Mock(spec=DisplayTerminal) + self.state_updater_mock = StateUpdater(self.env, self.display) @pytest.mark.unit_test - def test_given_one_study_when_retrieve_all_studies_call_then_study_retriever_is_called_once( - self, my_study - ): + def test_given_one_study_when_retrieve_all_studies_call_then_study_retriever_is_called_once(self, started_study): # given - list_of_studies = [my_study] + list_of_studies = [started_study] self.data_repo.get_list_of_studies = mock.Mock(return_value=list_of_studies) - my_retriever = RetrieveController( - self.data_repo, - self.remote_env_mock, - self.file_manager, - self.display, - self.state_updater_mock, - ) + my_retriever = RetrieveController(self.data_repo, self.env, self.display, self.state_updater_mock) my_retriever.study_retriever.retrieve = mock.Mock() self.display.show_message = mock.Mock() # when my_retriever.retrieve_all_studies() # then - self.display.show_message.assert_called_once_with( - "Retrieving all studies", mock.ANY - ) - my_retriever.study_retriever.retrieve.assert_called_once_with(my_study) + self.display.show_message.assert_called_once_with("Retrieving all studies...", mock.ANY) + my_retriever.study_retriever.retrieve.assert_called_once_with(started_study) @pytest.mark.unit_test def test_given_a_list_of_done_studies_when_all_studies_done_called_then_return_true( @@ -93,13 +40,7 @@ def test_given_a_list_of_done_studies_when_all_studies_done_called_then_return_t study = StudyDTO("path") study.done = True study_list = [deepcopy(study), deepcopy(study)] - my_retriever = RetrieveController( - self.data_repo, - self.remote_env_mock, - self.file_manager, - self.display, - self.state_updater_mock, - ) + my_retriever = RetrieveController(self.data_repo, self.env, self.display, self.state_updater_mock) my_retriever.repo.get_list_of_studies = mock.Mock(return_value=study_list) # when output = my_retriever.all_studies_done @@ -114,24 +55,18 @@ def test_given_a_list_of_done_studies_when_retrieve_all_studies_called_then_mess study = StudyDTO("path") study.done = True study_list = [deepcopy(study), deepcopy(study)] - display_mock = mock.Mock(spec=IDisplay) - my_retriever = RetrieveController( - self.data_repo, - self.remote_env_mock, - self.file_manager, - display_mock, - self.state_updater_mock, - ) + display_mock = mock.Mock(spec=DisplayTerminal) + my_retriever = RetrieveController(self.data_repo, self.env, display_mock, self.state_updater_mock) my_retriever.repo.get_list_of_studies = mock.Mock(return_value=study_list) display_mock.show_message = mock.Mock() # when output = my_retriever.retrieve_all_studies() # then - expected_message1 = "Retrieving all studies" - expected_message2 = "Everything is done" + expected_message1 = "Retrieving all studies..." + expected_message2 = "All retrievals are done." calls = [ call(expected_message1, mock.ANY), call(expected_message2, mock.ANY), - ] # , call(my_study3)] + ] # , call(started_study3)] display_mock.show_message.assert_has_calls(calls) assert output is True diff --git a/tests/unit/retriever/test_server_cleaner.py b/tests/unit/retriever/test_server_cleaner.py index 4e0c77f..9cc51a7 100644 --- a/tests/unit/retriever/test_server_cleaner.py +++ b/tests/unit/retriever/test_server_cleaner.py @@ -1,102 +1,111 @@ -from copy import copy -from pathlib import Path from unittest import mock import pytest -import antareslauncher.remote_environnement.remote_environment_with_slurm -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO -from antareslauncher.use_cases.retrieve.clean_remote_server import ( - RemoteServerCleaner, - RemoteServerNotCleanException, -) +from antareslauncher.use_cases.retrieve.clean_remote_server import RemoteServerCleaner class TestServerCleaner: - def setup_method(self): - self.remote_env_mock = mock.Mock(spec=RemoteEnvironmentWithSlurm) - self.display_mock = mock.Mock(spec_set=IDisplay) - self.remote_server_cleaner = RemoteServerCleaner( - self.remote_env_mock, self.display_mock - ) - - @pytest.fixture(scope="function") - def downloaded_zip_study(self): - study = StudyDTO( - path=Path("path") / "hello", - started=True, - finished=True, - job_id=42, - local_final_zipfile_path=str(Path("final") / "zip" / "path.zip"), - ) - return study - @pytest.mark.unit_test - def test_clean_server_show_message_if_successful(self, downloaded_zip_study): - self.remote_env_mock.clean_remote_server = mock.Mock(return_value=True) - self.remote_server_cleaner.clean(downloaded_zip_study) + def test_clean__finished_study__nominal(self, finished_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.clean_remote_server.return_value = True + display = mock.Mock(spec=DisplayTerminal) + + # Prepare a fake + finished_study.local_final_zipfile_path = "/path/to/result.zip" + + # Initialize and execute the cleaning + cleaner = RemoteServerCleaner(env, display) + cleaner.clean(finished_study) + + # Check the result + env.clean_remote_server.assert_called_once() + display.show_message.assert_called() + display.show_error.assert_not_called() - expected_message = ( - f'"{downloaded_zip_study.name}": Clean remote server finished' - ) - self.display_mock.show_message.assert_called_once_with( - expected_message, mock.ANY - ) + assert finished_study.remote_server_is_clean @pytest.mark.unit_test - def test_clean_server_show_error_and_raise_exception_if_fails( - self, downloaded_zip_study - ): - self.remote_env_mock.clean_remote_server = mock.Mock(return_value=False) + def test_clean__finished_study__no_result(self, finished_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.clean_remote_server.return_value = True + display = mock.Mock(spec=DisplayTerminal) - with pytest.raises(RemoteServerNotCleanException): - self.remote_server_cleaner.clean(downloaded_zip_study) + # Prepare a fake + finished_study.local_final_zipfile_path = "" - expected_error = f'"{downloaded_zip_study.name}": Clean remote server failed' - self.display_mock.show_error.assert_called_once_with(expected_error, mock.ANY) + # Initialize and execute the cleaning + cleaner = RemoteServerCleaner(env, display) + cleaner.clean(finished_study) + + # Check the result + env.clean_remote_server.assert_not_called() + display.show_message.assert_not_called() + display.show_error.assert_not_called() + + assert not finished_study.remote_server_is_clean @pytest.mark.unit_test - def test_remote_environment_not_called_if_final_zip_not_downloaded(self): - self.remote_env_mock.clean_remote_server = mock.Mock() - study = StudyDTO(path="hello") - study.local_final_zipfile_path = "" - new_study = self.remote_server_cleaner.clean(study) + def test_clean__finished_study__reentrancy(self, finished_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.clean_remote_server.return_value = True + display = mock.Mock(spec=DisplayTerminal) - self.remote_env_mock.clean_remote_server.assert_not_called() - assert new_study == study + # Prepare a fake + finished_study.local_final_zipfile_path = "/path/to/result.zip" - study.local_final_zipfile_path = None - new_study = self.remote_server_cleaner.clean(study) + # Initialize and execute the cleaning twice + cleaner = RemoteServerCleaner(env, display) + cleaner.clean(finished_study) + cleaner.clean(finished_study) - self.remote_env_mock.clean_remote_server.assert_not_called() - assert new_study == study + # Check the result + env.clean_remote_server.assert_called_once() + display.show_message.assert_called() + display.show_error.assert_not_called() + + assert finished_study.remote_server_is_clean @pytest.mark.unit_test - def test_remote_environment_not_called_if_remote_server_is_already_clean( - self, - ): - self.remote_env_mock.clean_remote_server = mock.Mock() - study = StudyDTO(path="hello") - study.local_final_zipfile_path = "hello.zip" - study.remote_server_is_clean = True - new_study = self.remote_server_cleaner.clean(study) - - self.remote_env_mock.clean_remote_server.assert_not_called() - assert new_study == study + def test_clean__finished_study__cleaning_failed(self, finished_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.clean_remote_server.return_value = False + display = mock.Mock(spec=DisplayTerminal) + + # Prepare a fake + finished_study.local_final_zipfile_path = "/path/to/result.zip" + + # Initialize and execute the cleaning + cleaner = RemoteServerCleaner(env, display) + cleaner.clean(finished_study) + + # Check the result + env.clean_remote_server.assert_called_once() + display.show_message.assert_not_called() + display.show_error.assert_called() + + assert finished_study.remote_server_is_clean @pytest.mark.unit_test - def test_remote_environment_is_called_if_final_zip_is_downloaded( - self, downloaded_zip_study - ): - self.remote_env_mock.clean_remote_server = mock.Mock(return_value=True) - expected_study = copy(downloaded_zip_study) - - new_study = self.remote_server_cleaner.clean(downloaded_zip_study) - first_call = self.remote_env_mock.clean_remote_server.call_args_list[0] - first_argument = first_call[0][0] - assert first_argument == expected_study - assert new_study.remote_server_is_clean + def test_clean__finished_study__cleaning_raise(self, finished_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.clean_remote_server.side_effect = Exception("cleaning error") + display = mock.Mock(spec=DisplayTerminal) + + # Prepare a fake + finished_study.local_final_zipfile_path = "/path/to/result.zip" + + # Initialize and execute the cleaning + cleaner = RemoteServerCleaner(env, display) + cleaner.clean(finished_study) + + # Check the result + env.clean_remote_server.assert_called_once() + display.show_message.assert_not_called() + display.show_error.assert_called() + + assert finished_study.remote_server_is_clean diff --git a/tests/unit/retriever/test_state_updater.py b/tests/unit/retriever/test_state_updater.py index a85a7d8..4a9d636 100644 --- a/tests/unit/retriever/test_state_updater.py +++ b/tests/unit/retriever/test_state_updater.py @@ -1,9 +1,10 @@ -from pathlib import Path from unittest import mock from unittest.mock import call import pytest +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.retrieve.state_updater import StateUpdater @@ -18,133 +19,114 @@ (None, None, True, "Ended with error"), ], ) -def test_given_a_submitted_study_then_study_flags_are_updated( - started_flag, finished_flag, with_error_flag, status -): - # given +def test_given_a_submitted_study_then_study_flags_are_updated(started_flag, finished_flag, with_error_flag, status): + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.get_job_state_flags = mock.Mock(return_value=(started_flag, finished_flag, with_error_flag)) + display = mock.Mock(spec=DisplayTerminal) + my_study = StudyDTO(path="study_path", job_id=42) - remote_env_mock = mock.Mock() - display = mock.Mock() - remote_env_mock.get_job_state_flags = mock.Mock( - return_value=(started_flag, finished_flag, with_error_flag) - ) - display.show_message = mock.Mock() - state_updater = StateUpdater(remote_env_mock, display) - message = f'"{Path(my_study.path).name}" (JOBID={my_study.job_id}): {status}' - # when - study_test = state_updater.run(my_study) - # then - remote_env_mock.get_job_state_flags.assert_called_once_with(my_study) + state_updater = StateUpdater(env, display) + state_updater.run(my_study) + + message = f'"{my_study.name}": (JOBID={my_study.job_id}): {status}' + display.show_message.assert_called_once_with(message, mock.ANY) - assert study_test.started == started_flag - assert study_test.finished == finished_flag - assert study_test.with_error == with_error_flag - assert study_test.job_state == status + assert my_study.started == started_flag + assert my_study.finished == finished_flag + assert my_study.with_error == with_error_flag + assert my_study.job_state == status @pytest.mark.unit_test def test_given_a_non_submitted_study_then_get_job_state_flags_is_not_called(): # given + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + display = mock.Mock(spec=DisplayTerminal) + my_study = StudyDTO(path="study_path", job_id=None) - remote_env_mock = mock.Mock() - remote_env_mock.get_job_state_flags = mock.Mock() - display = mock.Mock() - display.show_error = mock.Mock() - state_updater = StateUpdater(remote_env_mock, display) - message = f'"{Path(my_study.path).name}": Job was not submitted' - # when + state_updater = StateUpdater(env, display) state_updater.run(my_study) - # then - remote_env_mock.get_job_state_flags.assert_not_called() + + message = f'"{my_study.name}": Job was NOT submitted' + env.get_job_state_flags.assert_not_called() display.show_error.assert_called_once_with(message, mock.ANY) @pytest.mark.unit_test def test_given_a_done_study_then_get_job_state_flags_is_not_called(): # given + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.get_job_state_flags = mock.Mock(return_value=(True, False, False)) + display = mock.Mock(spec=DisplayTerminal) + my_study = StudyDTO(path="study_path", job_id=42, done=True) - remote_env_mock = mock.Mock() - remote_env_mock.get_job_state_flags = mock.Mock(return_value=(True, False, False)) - display = mock.Mock() - display.show_message = mock.Mock() - state_updater = StateUpdater(remote_env_mock, display) - message = ( - f'"{Path(my_study.path).name}" (JOBID={my_study.job_id}): everything is done' - ) - # when + state_updater = StateUpdater(env, display) state_updater.run(my_study) - # then - remote_env_mock.get_job_state_flags.assert_not_called() + + message = f'"{my_study.name}": (JOBID={my_study.job_id}): everything is done' + env.get_job_state_flags.assert_not_called() display.show_message.assert_called_once_with(message, mock.ANY) @pytest.mark.unit_test def test_state_updater_run_on_empty_list_of_studies_write_one_message(): - # given - study_list = [] - remote_env_mock = mock.Mock() - display = mock.Mock() - state_updater = StateUpdater(remote_env_mock, display) + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + display = mock.Mock(spec=DisplayTerminal) + + state_updater = StateUpdater(env, display) + state_updater.run_on_list([]) + message = "Checking status of the studies:" - # when - state_updater.run_on_list(study_list) - # then display.show_message.assert_called_once_with(message, mock.ANY) @pytest.mark.unit_test def test_with_a_list_of_one_submitted_study_run_on_list_calls_run_once_on_study(): - # given + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.get_job_state_flags = mock.Mock(return_value=(1, 2, 3)) + display = mock.Mock(spec=DisplayTerminal) + my_study1 = StudyDTO(path="study_path1", job_id=1) study_list = [my_study1] - remote_env_mock = mock.Mock() - remote_env_mock.get_job_state_flags = mock.Mock(return_value=(1, 2, 3)) - display = mock.Mock() - state_updater = StateUpdater(remote_env_mock, display) - # when + state_updater = StateUpdater(env, display) state_updater.run_on_list(study_list) - # then - remote_env_mock.get_job_state_flags.assert_called_once_with(my_study1) + + env.get_job_state_flags.assert_called_once_with(my_study1) @pytest.mark.unit_test def test_run_on_list_calls_run_on_all_submitted_studies_of_the_list(): - # given + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.get_job_state_flags = mock.Mock(return_value=(1, 2, 3)) + display = mock.Mock(spec=DisplayTerminal) + my_study1 = StudyDTO(path="study_path1", job_id=1) my_study2 = StudyDTO(path="study_path2", job_id=None) my_study3 = StudyDTO(path="study_path3", job_id=2) study_list = [my_study1, my_study2, my_study3] - remote_env_mock = mock.Mock() - remote_env_mock.get_job_state_flags = mock.Mock(return_value=(1, 2, 3)) - display = mock.Mock() - state_updater = StateUpdater(remote_env_mock, display) - # when + state_updater = StateUpdater(env, display) state_updater.run_on_list(study_list) - # then + calls = [call(my_study1), call(my_study3)] - remote_env_mock.get_job_state_flags.assert_has_calls(calls) + env.get_job_state_flags.assert_has_calls(calls) @pytest.mark.unit_test def test_run_on_list_calls_run_start__processing_studies_that_are_done(): - # given + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.get_job_state_flags = mock.Mock(return_value=(True, False, False)) + display = mock.Mock(spec=DisplayTerminal) + my_study1 = StudyDTO(path="study_path1", job_id=1, done=False) my_study2 = StudyDTO(path="study_path2", job_id=None) my_study3 = StudyDTO(path="study_path3", job_id=2, done=True) study_list = [my_study1, my_study2, my_study3] - remote_env_mock = mock.Mock() - remote_env_mock.get_job_state_flags = mock.Mock(return_value=(True, False, False)) - display = mock.Mock() - display.show_message = mock.Mock() - state_updater = StateUpdater(remote_env_mock, display) - # when + state_updater = StateUpdater(env, display) state_updater.run_on_list(study_list) - # then + welcome_message = "Checking status of the studies:" - message1 = f'"{Path(my_study1.path).name}" (JOBID={my_study1.job_id}): Running' - message3 = ( - f'"{Path(my_study3.path).name}" (JOBID={my_study3.job_id}): everything is done' - ) + message1 = f'"{my_study1.name}": (JOBID={my_study1.job_id}): Running' + message3 = f'"{my_study3.name}": (JOBID={my_study3.job_id}): everything is done' calls = [ call(welcome_message, mock.ANY), call(message3, mock.ANY), diff --git a/tests/unit/retriever/test_study_retriever.py b/tests/unit/retriever/test_study_retriever.py index 3caab2d..4d9c506 100644 --- a/tests/unit/retriever/test_study_retriever.py +++ b/tests/unit/retriever/test_study_retriever.py @@ -1,16 +1,11 @@ -from copy import copy from unittest import mock -from unittest.mock import call import pytest +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.data_repo.data_reporter import DataReporter -from antareslauncher.data_repo.idata_repo import IDataRepo -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.file_manager.file_manager import FileManager -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.retrieve.clean_remote_server import RemoteServerCleaner from antareslauncher.use_cases.retrieve.download_final_zip import FinalZipDownloader @@ -23,15 +18,14 @@ class TestStudyRetriever: def setup_method(self): env = mock.Mock(spec_set=RemoteEnvironmentWithSlurm) - display = mock.Mock(spec_set=IDisplay) - file_manager = mock.Mock(spec_set=FileManager) - repo = mock.Mock(spec_set=IDataRepo) + display = mock.Mock(spec_set=DisplayTerminal) + repo = mock.Mock(spec_set=DataRepoTinydb) self.reporter = DataReporter(repo) self.state_updater = StateUpdater(env, display) - self.logs_downloader = LogDownloader(env, file_manager, display) + self.logs_downloader = LogDownloader(env, display) self.final_zip_downloader = FinalZipDownloader(env, display) self.remote_server_cleaner = RemoteServerCleaner(env, display) - self.zip_extractor = FinalZipExtractor(file_manager, display) + self.zip_extractor = FinalZipExtractor(display) self.study_retriever = StudyRetriever( self.state_updater, self.logs_downloader, @@ -61,70 +55,67 @@ def test_given_done_study_nothing_is_done(self): self.zip_extractor.extract_final_zip.assert_not_called() @pytest.mark.unit_test - def test_given_a_not_done_studies_everything_is_called(self): + def test_retrieve_study(self): + """ + This test function simulates the retrieval process of a study and verifies + that various components and states are updated correctly. + + Test cases covered: + - State updater + - Logs downloader + - Final zip downloader + - Remote server cleaner + - Zip extractor + - Reporter + """ study = StudyDTO(path="hello") - study1 = StudyDTO( + + def state_updater_run(study_: StudyDTO): + study_.job_id = 42 + study_.started = True + study_.finished = True + study_.with_error = False + return study_ + + self.state_updater.run = mock.Mock(side_effect=state_updater_run) + + def logs_downloader_run(study_: StudyDTO): + study_.logs_downloaded = True + return study_ + + self.logs_downloader.run = mock.Mock(side_effect=logs_downloader_run) + + def final_zip_downloader_download(study_: StudyDTO): + study_.local_final_zipfile_path = "final-zipfile.zip" + return study_ + + self.final_zip_downloader.download = mock.Mock(side_effect=final_zip_downloader_download) + + def remote_server_cleaner_clean(study_: StudyDTO): + study_.remote_server_is_clean = True + return study_ + + self.remote_server_cleaner.clean = mock.Mock(side_effect=remote_server_cleaner_clean) + + def zip_extractor_extract_final_zip(study_: StudyDTO): + study_.final_zip_extracted = True + return study_ + + self.zip_extractor.extract_final_zip = mock.Mock(side_effect=zip_extractor_extract_final_zip) + self.reporter.save_study = mock.Mock(return_value=True) + + self.study_retriever.retrieve(study) + + expected = StudyDTO( path="hello", job_id=42, + done=True, started=True, finished=True, with_error=False, + logs_downloaded=True, + local_final_zipfile_path="final-zipfile.zip", + remote_server_is_clean=True, + final_zip_extracted=True, ) - self.state_updater.run = mock.Mock(return_value=study1) - study2 = copy(study1) - study2.logs_downloaded = True - self.logs_downloader.run = mock.Mock(return_value=study2) - study3 = copy(study2) - study3.local_final_zipfile_path = "final-zipfile.zip" - self.final_zip_downloader.download = mock.Mock(return_value=study3) - study4 = copy(study3) - study4.remote_server_is_clean = True - self.remote_server_cleaner.clean = mock.Mock(return_value=study4) - study5 = copy(study4) - study5.final_zip_extracted = True - self.zip_extractor.extract_final_zip = mock.Mock(return_value=study5) - study6 = copy(study5) - study6.done = True - self.reporter.save_study = mock.Mock() - - self.study_retriever.retrieve(study) - - self.state_updater.run.assert_called_once_with(study) - self.logs_downloader.run.assert_called_once_with(study1) - self.final_zip_downloader.download.assert_called_once_with(study2) - self.remote_server_cleaner.clean.assert_called_once_with(study3) - self.zip_extractor.extract_final_zip.assert_called_once_with(study4) - assert self.reporter.save_study.call_count == 6 - calls = self.reporter.save_study.call_args_list - assert calls[0] == call(study1) - assert calls[1] == call(study2) - assert calls[2] == call(study3) - assert calls[3] == call(study4) - assert calls[4] == call(study5) - assert calls[5] == call(study6) - - @staticmethod - @pytest.mark.unit_test - @pytest.mark.parametrize( - "result, with_error,logs_downloaded, local_final_zipfile_path, remote_server_is_clean, final_zip_extracted", - [ - (False, False, False, None, False, False), - (True, True, False, None, False, False), - (True, False, True, "path.zip", True, True), - ], - ) - def test_when_study_finished_with_error_check_if_study_is_done_returns_true( - result, - with_error, - logs_downloaded, - local_final_zipfile_path, - remote_server_is_clean, - final_zip_extracted, - ): - my_study = StudyDTO(path="hello", job_id=42, started=True, finished=True) - my_study.with_error = with_error - my_study.logs_downloaded = logs_downloaded - my_study.local_final_zipfile_path = local_final_zipfile_path - my_study.remote_server_is_clean = remote_server_is_clean - my_study.final_zip_extracted = final_zip_extracted - assert StudyRetriever.check_if_study_is_done(my_study) is result + self.reporter.save_study.assert_called_once_with(expected) diff --git a/tests/unit/test_antares_launcher.py b/tests/unit/test_antares_launcher.py index f8188a8..4b9c875 100644 --- a/tests/unit/test_antares_launcher.py +++ b/tests/unit/test_antares_launcher.py @@ -1,12 +1,10 @@ from unittest import mock -from unittest.mock import PropertyMock, Mock +from unittest.mock import Mock, PropertyMock import pytest from antareslauncher.antares_launcher import AntaresLauncher -from antareslauncher.use_cases.wait_loop_controller.wait_controller import ( - WaitController, -) +from antareslauncher.use_cases.wait_loop_controller.wait_controller import WaitController class TestAntaresLauncher: @@ -33,9 +31,7 @@ def test_given_job_id_to_kill_when_run_then_job_kill_controller_kills_job_with_g # when antares_launcher.run() # then - antares_launcher.job_kill_controller.kill_job.assert_called_once_with( - job_id_to_kill - ) + antares_launcher.job_kill_controller.kill_job.assert_called_once_with(job_id_to_kill) @pytest.mark.unit_test def test_given_true_check_queue_bool_when_run_then_check_queue_controller_checks_queue( diff --git a/tests/unit/test_check_queue_controller.py b/tests/unit/test_check_queue_controller.py index d1f53d7..5195228 100644 --- a/tests/unit/test_check_queue_controller.py +++ b/tests/unit/test_check_queue_controller.py @@ -2,20 +2,16 @@ import pytest -from antareslauncher.data_repo.idata_repo import IDataRepo +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.study_dto import StudyDTO -from antareslauncher.use_cases.check_remote_queue.check_queue_controller import ( - CheckQueueController, -) -from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import ( - SlurmQueueShow, -) +from antareslauncher.use_cases.check_remote_queue.check_queue_controller import CheckQueueController +from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import SlurmQueueShow from antareslauncher.use_cases.retrieve.state_updater import StateUpdater class TestCheckQueueController: def setup_method(self): - self.repo_mock = mock.Mock(spec=IDataRepo) + self.repo_mock = mock.Mock(spec=DataRepoTinydb) self.env = mock.Mock() self.display = mock.Mock() self.slurm_queue_show = SlurmQueueShow(env=self.env, display=self.display) @@ -30,9 +26,7 @@ def setup_method(self): def test_check_queue_controller_calls_slurm_queue_show_once(self): # given self.slurm_queue_show.run = mock.Mock() - self.repo_mock.get_list_of_studies = ( - mock.MagicMock() - ) # mock.Mock(return_value=[]) + self.repo_mock.get_list_of_studies = mock.MagicMock() # mock.Mock(return_value=[]) # when self.check_queue_controller.check_queue() # then diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 5850686..111601b 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -1,12 +1,12 @@ import contextlib import getpass import json -import pathlib from pathlib import Path from unittest.mock import patch import pytest import yaml + from antareslauncher.config import ( APP_AUTHOR, APP_NAME, @@ -19,17 +19,13 @@ get_user_config_dir, parse_config, ) -from antareslauncher.exceptions import ( - ConfigFileNotFoundError, - InvalidConfigValueError, - UnknownFileSuffixError, -) +from antareslauncher.exceptions import ConfigFileNotFoundError, InvalidConfigValueError, UnknownFileSuffixError class TestParseConfig: @pytest.mark.parametrize("suffix", [".yaml", ".yml", ".json", ".py"]) @pytest.mark.parametrize("casing", [None, str.upper, str.title]) - def test_parse_config(self, tmp_path, suffix, casing): + def test_parse_config(self, tmp_path: Path, suffix, casing) -> None: data = {"key1": "value1", "key2": 56} # noinspection PyArgumentList new_suffix = suffix if casing is None else casing(suffix) @@ -52,7 +48,7 @@ def test_parse_config(self, tmp_path, suffix, casing): class TestSaveConfig: @pytest.mark.parametrize("suffix", [".yaml", ".yml", ".json", ".py"]) @pytest.mark.parametrize("casing", [None, str.upper, str.title]) - def test_save_config(self, tmp_path, suffix, casing): + def test_save_config(self, tmp_path: Path, suffix, casing) -> None: data = {"key1": "value1", "key2": 56} # noinspection PyArgumentList new_suffix = suffix if casing is None else casing(suffix) @@ -72,7 +68,7 @@ def test_save_config(self, tmp_path, suffix, casing): class TestSSHConfig: - def test_load_config__with_private_key_file(self, tmp_path): + def test_load_config__with_private_key_file(self, tmp_path) -> None: data = { "username": "john.doe", "hostname": "localhost", @@ -91,7 +87,7 @@ def test_load_config__with_private_key_file(self, tmp_path): assert config.key_password == data["key_password"] assert config.password == "" - def test_load_config__with_password(self, tmp_path): + def test_load_config__with_password(self, tmp_path) -> None: data = { "username": "john.doe", "hostname": "localhost", @@ -110,7 +106,7 @@ def test_load_config__with_password(self, tmp_path): assert config.password == data["password"] @pytest.mark.parametrize("required", ["username", "hostname"]) - def test_load_config__missing_parameter(self, tmp_path, required): + def test_load_config__missing_parameter(self, tmp_path: Path, required) -> None: data = { "username": "john.doe", "hostname": "localhost", @@ -123,14 +119,14 @@ def test_load_config__missing_parameter(self, tmp_path, required): with pytest.raises(InvalidConfigValueError): SSHConfig.load_config(config_path) - def test_save_config__with_private_key_file(self, tmp_path): + def test_save_config__with_private_key_file(self, tmp_path) -> None: config_path = tmp_path.joinpath("my_ssh_config.json") config = SSHConfig( config_path=config_path, username="john.doe", hostname="localhost", port=22, - private_key_file=pathlib.Path("path/to/private.key"), + private_key_file=Path("path/to/private.key"), key_password="key_password", ) config.save_config(config_path) @@ -143,7 +139,7 @@ def test_save_config__with_private_key_file(self, tmp_path): assert actual["key_password"] == config.key_password assert "password" not in actual - def test_save_config__with_password(self, tmp_path): + def test_save_config__with_password(self, tmp_path) -> None: config_path = tmp_path.joinpath("my_ssh_config.json") config = SSHConfig( config_path=config_path, @@ -165,7 +161,7 @@ def test_save_config__with_password(self, tmp_path): class TestConfig: @pytest.fixture(name="ssh_config_path") - def fixture_ssh_config_path(self, tmp_path) -> pathlib.Path: + def fixture_ssh_config_path(self, tmp_path) -> Path: data = { "username": "john.doe", "hostname": "localhost", @@ -187,7 +183,7 @@ def fixture_ssh_config(self, tmp_path) -> SSHConfig: password="S3Cr3T", ) - def test_load_config__nominal(self, tmp_path, ssh_config_path): + def test_load_config__nominal(self, tmp_path: Path, ssh_config_path) -> None: log_dir = tmp_path.joinpath("log_dir") json_dir = tmp_path.joinpath("json_dir") studies_in_dir = tmp_path.joinpath("studies_in_dir") @@ -222,11 +218,9 @@ def test_load_config__nominal(self, tmp_path, ssh_config_path): assert config.db_primary_key == data["db_primary_key"] assert config.ssh_config_file_is_required == data["ssh_config_file_is_required"] assert config.slurm_script_path == slurm_script_path - assert ( - config.remote_solver_versions == data["antares_versions_on_remote_server"] - ) + assert config.remote_solver_versions == data["antares_versions_on_remote_server"] - def test_save_config__nominal(self, tmp_path, ssh_config): + def test_save_config__nominal(self, tmp_path: Path, ssh_config) -> None: config_path = tmp_path.joinpath("configuration.yaml") log_dir = tmp_path.joinpath("log_dir") json_dir = tmp_path.joinpath("json_dir") @@ -261,13 +255,9 @@ def test_save_config__nominal(self, tmp_path, ssh_config): assert actual["default_n_cpu"] == config.default_n_cpu assert actual["default_wait_time"] == config.default_wait_time assert actual["db_primary_key"] == config.db_primary_key - assert ( - actual["ssh_config_file_is_required"] == config.ssh_config_file_is_required - ) + assert actual["ssh_config_file_is_required"] == config.ssh_config_file_is_required assert actual["slurm_script_path"] == slurm_script_path.as_posix() - assert ( - actual["antares_versions_on_remote_server"] == config.remote_solver_versions - ) + assert actual["antares_versions_on_remote_server"] == config.remote_solver_versions assert "ssh_config" not in actual @pytest.mark.parametrize( @@ -287,7 +277,7 @@ def test_save_config__nominal(self, tmp_path, ssh_config): "antares_versions_on_remote_server", ], ) - def test_load_config__missing_parameter(self, tmp_path, ssh_config_path, required): + def test_load_config__missing_parameter(self, tmp_path: Path, ssh_config_path, required) -> None: log_dir = tmp_path.joinpath("log_dir") json_dir = tmp_path.joinpath("json_dir") studies_in_dir = tmp_path.joinpath("studies_in_dir") @@ -333,7 +323,7 @@ class TestGetUserConfigDir: ), ], ) - def test_get_user_config_dir(self, system, expected, monkeypatch): + def test_get_user_config_dir(self, system, expected, monkeypatch) -> None: # ignore error `XDG_CONFIG_HOME` environment variable monkeypatch.delenv("XDG_CONFIG_HOME", raising=False) # ignore error: "cannot instantiate 'WindowsPath'/'PosixPath' on your system" @@ -350,25 +340,21 @@ def test_get_user_config_dir(self, system, expected, monkeypatch): class TestGetConfigPath: - def test_get_config_path__from_env(self, monkeypatch, tmp_path): + def test_get_config_path__from_env(self, monkeypatch, tmp_path) -> None: config_path = tmp_path.joinpath("my_config.yaml") config_path.touch() monkeypatch.setenv("ANTARES_LAUNCHER_CONFIG_PATH", str(config_path)) actual = get_config_path() assert actual == config_path - def test_get_config_path__from_env__not_found(self, monkeypatch, tmp_path): + def test_get_config_path__from_env__not_found(self, monkeypatch, tmp_path) -> None: config_path = tmp_path.joinpath("my_config.yaml") monkeypatch.setenv("ANTARES_LAUNCHER_CONFIG_PATH", str(config_path)) with pytest.raises(ConfigFileNotFoundError): get_config_path() - @pytest.mark.parametrize( - "config_name", [None, CONFIGURATION_YAML, "my_config.yaml"] - ) - def test_get_config_path__from_user_config_dir( - self, monkeypatch, tmp_path, config_name - ): + @pytest.mark.parametrize("config_name", [None, CONFIGURATION_YAML, "my_config.yaml"]) + def test_get_config_path__from_user_config_dir(self, monkeypatch, tmp_path: Path, config_name) -> None: config_path = tmp_path.joinpath(config_name or CONFIGURATION_YAML) config_path.touch() monkeypatch.delenv("ANTARES_LAUNCHER_CONFIG_PATH", raising=False) @@ -379,17 +365,11 @@ def test_get_config_path__from_user_config_dir( assert actual == config_path @pytest.mark.parametrize("relpath", ["", "data"]) - @pytest.mark.parametrize( - "config_name", [None, CONFIGURATION_YAML, "my_config.yaml"] - ) - def test_get_config_path__from_curr_dir( - self, monkeypatch, tmp_path, relpath, config_name - ): + @pytest.mark.parametrize("config_name", [None, CONFIGURATION_YAML, "my_config.yaml"]) + def test_get_config_path__from_curr_dir(self, monkeypatch, tmp_path: Path, relpath, config_name) -> None: data_dir = tmp_path.joinpath(relpath) data_dir.mkdir(exist_ok=True) - config_path: pathlib.Path = tmp_path.joinpath( - data_dir, config_name or CONFIGURATION_YAML - ) + config_path: Path = tmp_path.joinpath(data_dir, config_name or CONFIGURATION_YAML) config_path.touch() monkeypatch.delenv("ANTARES_LAUNCHER_CONFIG_PATH", raising=False) monkeypatch.chdir(tmp_path) @@ -398,9 +378,7 @@ def test_get_config_path__from_curr_dir( assert actual == config_path.relative_to(tmp_path) @pytest.mark.parametrize("relpath", ["", "data"]) - def test_get_config_path__from_curr_dir__not_found( - self, monkeypatch, tmp_path, relpath - ): + def test_get_config_path__from_curr_dir__not_found(self, monkeypatch, tmp_path: Path, relpath) -> None: data_dir = tmp_path.joinpath(relpath) data_dir.mkdir(exist_ok=True) monkeypatch.delenv("ANTARES_LAUNCHER_CONFIG_PATH", raising=False) diff --git a/tests/unit/test_data_provider.py b/tests/unit/test_data_provider.py deleted file mode 100644 index bdb7f23..0000000 --- a/tests/unit/test_data_provider.py +++ /dev/null @@ -1,17 +0,0 @@ -from unittest.mock import Mock - -from antareslauncher.data_repo.data_provider import DataProvider -from antareslauncher.data_repo.idata_repo import IDataRepo -from antareslauncher.study_dto import StudyDTO - - -def test_data_provider_return_list_of_studies_obtained_from_repo(): - # given - data_repo = Mock(spec_set=IDataRepo) - study = StudyDTO(path="empty_path") - data_repo.get_list_of_studies = Mock(return_value=[study]) - data_provider = DataProvider(data_repo) - # when - list_of_studies = data_provider.get_list_of_studies() - # then - assert list_of_studies == [study] diff --git a/tests/unit/test_data_repo_tinydb.py b/tests/unit/test_data_repo_tinydb.py index 48a2b8b..52ad688 100644 --- a/tests/unit/test_data_repo_tinydb.py +++ b/tests/unit/test_data_repo_tinydb.py @@ -1,4 +1,7 @@ +import random +from pathlib import Path from unittest import mock +from uuid import uuid4 import pytest import tinydb @@ -7,144 +10,53 @@ from antareslauncher.study_dto import StudyDTO -class TestDataRepoTinydb: - @pytest.mark.unit_test - def test_given_data_repo_when_save_study_is_called_then_is_study_inside_database_is_called( - self, - ): - # given - repo_mock = DataRepoTinydb("", "name") - type(repo_mock).db = mock.PropertyMock() - repo_mock.is_study_inside_database = mock.Mock() - study_dto = StudyDTO(path="path") - # when - repo_mock.save_study(study_dto) - # then - repo_mock.is_study_inside_database.assert_called_with(study=study_dto) - - @pytest.mark.unit_test - def test_given_data_repo_if_study_is_inside_database_then_db_update_is_called( - self, - ): - # given - repo = DataRepoTinydb("", "name") - repo.is_study_inside_database = mock.Mock(return_value=True) - type(repo).db = mock.PropertyMock() - study_dto = StudyDTO(path="path") - # when - repo.save_study(study_dto) - # then - repo.db.update.assert_called_once() - - @pytest.mark.unit_test - def test_integration_given_data_repo_if_study_is_found_once_in_database_then_db_update_is_called( - self, - ): - # given - repo = DataRepoTinydb("", "name") - type(repo).db = mock.PropertyMock() - repo.db.search = mock.Mock(return_value=["A"]) - study_dto = StudyDTO(path="path") - # when - repo.save_study(study_dto) - # then - repo.db.update.assert_called_once() - - @pytest.mark.unit_test - def test_given_data_repo_if_study_is_not_inside_database_then_db_insert_is_called( - self, - ): - # given - repo = DataRepoTinydb("", "name") - repo.is_study_inside_database = mock.Mock(return_value=False) - type(repo).db = mock.PropertyMock() - study_dto = StudyDTO(path="path") - # when - repo.save_study(study_dto) - # then - repo.db.insert.assert_called_once() - - @pytest.mark.unit_test - def test_given_db_when_get_list_of_studies_is_called_then_db_all_is_called( - self, - ): - # given - repo = DataRepoTinydb("", "name") - repo.doc_to_study = mock.Mock(return_value=42) - type(repo).db = mock.PropertyMock() - repo.db.all = mock.Mock(return_value=[]) - # when - repo.get_list_of_studies() - # then - repo.db.all_assert_calles_once() +@pytest.fixture(name="repo") +def repo_fixture(tmp_path: Path) -> DataRepoTinydb: + return DataRepoTinydb( + database_file_path=tmp_path.joinpath("repo.json"), + db_primary_key="name", + ) - @pytest.mark.unit_test - def test_given_db_of_n_elements_when_get_list_of_studies_is_called_then_doc_to_study_is_called_n_times( - self, - ): - # given - n = 5 - repo = DataRepoTinydb("", "name") - repo.doc_to_study = mock.Mock(return_value=42) - type(repo).db = mock.PropertyMock() - repo.db.all = mock.Mock(return_value=[""] * n) - # when - repo.get_list_of_studies() - # then - assert repo.doc_to_study.call_count == n - - @pytest.mark.unit_test - def test_given_tinydb_document_when_doc_to_study_called_then_return_corresponding_study( - self, - ): - # given - expected_study = StudyDTO(path="path") - expected_study.job_id = 42 - expected_study.n_cpu = 999 - doc = tinydb.database.Document(expected_study.__dict__, 42) - # when - output_study = DataRepoTinydb.doc_to_study(doc) - # then - assert expected_study.__dict__ == output_study.__dict__ - - @pytest.mark.unit_test - def test_is_study_inside_database_returns_true_only_if_one_study_is_found( - self, - ): - # given - repo = DataRepoTinydb("", "name") - type(repo).db = mock.PropertyMock() - dummy_study = StudyDTO(path="path") - repo.db.search = mock.Mock(return_value=["first_element"]) - # when - output = repo.is_study_inside_database(study=dummy_study) - # then - assert output is True - - repo.db.search = mock.Mock(return_value=["first_element", "second_element"]) - # when - output = repo.is_study_inside_database(study=dummy_study) - # then - assert output is False - - repo.db.search = mock.Mock(return_value=[]) - # when - output = repo.is_study_inside_database(study=dummy_study) - # then - assert output is False +class TestDataRepoTinydb: @pytest.mark.unit_test - def test_is_job_id_inside_database_returns_true_only_if_one_job_id_is_found( - self, - ): - # given - repo = DataRepoTinydb("", "name") - type(repo).db = mock.PropertyMock() - study_dto = StudyDTO(path="path") - study_dto.job_id = 6381 - repo.get_list_of_studies = mock.Mock(return_value=[study_dto]) - repo.save_study(study_dto) - # when - output = repo.is_job_id_inside_database(6381) - # then - assert output is True + def test_save_study__insert_and_update(self, repo: DataRepoTinydb): + """ + Test that the 'save_study' method in DataRepoTinydb correctly adds a study to the database + and that 'is_study_inside_database' is called with the same study object. + """ + study = StudyDTO(path="path/to/my_study") + repo.save_study(study) + assert repo.is_study_inside_database(study) + studies = repo.get_list_of_studies() + assert {s.name for s in studies} == {"my_study"} + + study.started = True + repo.save_study(study) + assert repo.is_study_inside_database(study) + studies = repo.get_list_of_studies() + assert {s.name for s in studies} == {"my_study"} + actual_study = next(iter(studies)) + assert actual_study.started is True + + @pytest.mark.unit_test + def test_remove_study__nominal_case(self, repo: DataRepoTinydb): + study = StudyDTO(path="path/to/my_study") + repo.save_study(study) + repo.remove_study("my_study") + studies = repo.get_list_of_studies() + assert not studies + + @pytest.mark.unit_test + def test_remove_study__missing(self, repo: DataRepoTinydb): + repo.remove_study("missing_study") + studies = repo.get_list_of_studies() + assert not studies + + @pytest.mark.unit_test + def test_is_job_id_inside_database(self, repo: DataRepoTinydb): + job_id = random.randint(1, 1000) + study = StudyDTO(path="path/to/my_study", job_id=job_id) + repo.save_study(study) + assert repo.is_job_id_inside_database(job_id) + assert not repo.is_job_id_inside_database(9999) diff --git a/tests/unit/test_data_reporter.py b/tests/unit/test_data_reporter.py index 19eac00..3d55c7b 100644 --- a/tests/unit/test_data_reporter.py +++ b/tests/unit/test_data_reporter.py @@ -1,13 +1,13 @@ from unittest.mock import Mock +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.data_repo.data_reporter import DataReporter -from antareslauncher.data_repo.idata_repo import IDataRepo from antareslauncher.study_dto import StudyDTO def test_data_reporter_calls_repo_to_save_study(): # given - data_repo = Mock(spec_set=IDataRepo) + data_repo = Mock(spec_set=DataRepoTinydb) data_repo.save_study = Mock() data_reporter = DataReporter(data_repo) study = StudyDTO(path="empty_path") diff --git a/tests/unit/test_file_manager.py b/tests/unit/test_file_manager.py deleted file mode 100644 index 64f15a4..0000000 --- a/tests/unit/test_file_manager.py +++ /dev/null @@ -1,83 +0,0 @@ -import os -import shutil -from pathlib import Path -from unittest import mock - -import pytest - -from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.file_manager import file_manager -from antareslauncher.study_dto import StudyDTO -from tests.data import DATA_DIR - -DIR_TO_ZIP = DATA_DIR / "file-manager-test" / "to-zip" -DIR_REF = DATA_DIR / "file-manager-test" / "reference-without-output" / "to-zip" - - -def get_dict_from_path(path: Path): - if os.path.isdir(path): - return { - "name": path.name, - "type": "directory", - "children": list(map(get_dict_from_path, path.iterdir())), - } - else: - return { - "name": path.name, - "type": "file", - } - - -class TestFileManager: - @pytest.mark.unit_test - def test_golden_master_for_zip_study_excluding_output_dir(self, tmp_path): - dir_to_zip = DIR_TO_ZIP - zip_name = str(dir_to_zip) + ".zip" - display_terminal = DisplayTerminal() - my_file_manager = file_manager.FileManager(display_terminal) - - my_file_manager.zip_dir_excluding_subdir(dir_to_zip, zip_name, "output") - - shutil.unpack_archive(zip_name, tmp_path) - results = tmp_path / dir_to_zip.name - results_dict = get_dict_from_path(results) - reference_dict = get_dict_from_path(DIR_REF) - - assert results_dict == reference_dict - result_zip_file = Path(zip_name) - assert result_zip_file.is_file() - result_zip_file.unlink() - - @pytest.mark.unit_test - def test_unzip(self): - """ - Tests the scenario where the specified zip file does not exist. The expected outcome is - that the function returns False, indicating that the zip file could not be unzipped. - This test is checking that the function correctly handles the case where the input file is not present. - """ - # given - study = StudyDTO("path") - display_terminal = DisplayTerminal() - my_file_manager = file_manager.FileManager(display_terminal) - # when - output = my_file_manager.unzip(study.local_final_zipfile_path) - # then - assert output is False - - @pytest.mark.unit_test - def test__get_list_dir_without_subdir(self): - """ - Tests the case where a directory path and a subdirectory name are given as inputs to the function. - The function is expected to return a list of items in the directory, excluding the specified subdirectory. - """ - # given - display_terminal = DisplayTerminal() - my_file_manager = file_manager.FileManager(display_terminal) - listdir = ["dir1", "dir2", "dir42"] - my_file_manager.listdir_of = mock.Mock(return_value=listdir.copy()) - subdir_to_exclude = "dir42" - listdir.remove(subdir_to_exclude) - # when - output = my_file_manager._get_list_dir_without_subdir("", subdir_to_exclude) - # then - assert listdir == output diff --git a/tests/unit/test_job_kill_controller.py b/tests/unit/test_job_kill_controller.py index 7167577..f12e8c4 100644 --- a/tests/unit/test_job_kill_controller.py +++ b/tests/unit/test_job_kill_controller.py @@ -2,9 +2,7 @@ import pytest -from antareslauncher.use_cases.kill_job.job_kill_controller import ( - JobKillController, -) +from antareslauncher.use_cases.kill_job.job_kill_controller import JobKillController class TestJobKillController: diff --git a/tests/unit/test_main_option_parser.py b/tests/unit/test_main_option_parser.py index c31dfe8..128cfac 100644 --- a/tests/unit/test_main_option_parser.py +++ b/tests/unit/test_main_option_parser.py @@ -2,11 +2,7 @@ import pytest -from antareslauncher.main_option_parser import ( - MainOptionParser, - ParserParameters, -) -from antareslauncher.main_option_parser import look_for_default_ssh_conf_file +from antareslauncher.main_option_parser import MainOptionParser, ParserParameters, look_for_default_ssh_conf_file class TestMainOptionParser: @@ -34,9 +30,7 @@ def setup_method(self): "n_cpu": 42, "job_id_to_kill": None, "post_processing": False, - "json_ssh_config": look_for_default_ssh_conf_file( - self.main_options_parameters - ), + "json_ssh_config": look_for_default_ssh_conf_file(self.main_options_parameters), } @pytest.fixture(scope="function") @@ -46,7 +40,7 @@ def parser(self): @pytest.mark.unit_test def test_check_all_default_values_are_present(self, parser): parser.add_basic_arguments() - output = parser.parse_args([]) + output = parser.parser.parse_args([]) out_dict = vars(output) for key, value in self.DEFAULT_VALUES.items(): assert out_dict[key] == value @@ -54,7 +48,7 @@ def test_check_all_default_values_are_present(self, parser): @pytest.mark.unit_test def test_given_add_basic_arguments_all_default_values_are_present(self, parser): parser.add_basic_arguments() - output = parser.parse_args([]) + output = parser.parser.parse_args([]) out_dict = vars(output) for key, value in self.DEFAULT_VALUES.items(): assert out_dict[key] == value @@ -62,5 +56,5 @@ def test_given_add_basic_arguments_all_default_values_are_present(self, parser): @pytest.mark.unit_test def test_studies_in_get_correctly_set(self, parser): parser.add_basic_arguments() - output = parser.parse_args(["--studies-in-dir=hello"]) + output = parser.parser.parse_args(["--studies-in-dir=hello"]) assert output.studies_in == "hello" diff --git a/tests/unit/test_parameters_reader.py b/tests/unit/test_parameters_reader.py index 042f9f8..58acf2b 100644 --- a/tests/unit/test_parameters_reader.py +++ b/tests/unit/test_parameters_reader.py @@ -3,13 +3,16 @@ from pathlib import Path import pytest +import yaml -from antareslauncher.parameters_reader import ParametersReader +from antareslauncher.parameters_reader import MissingValueException, ParametersReader class TestParametersReader: def setup_method(self): self.SLURM_SCRIPT_PATH = "/path/to/launchAntares_v1.1.3.sh" + self.PARTITION = "compute1" + self.QUALITY_OF_SERVICE = "user1_qos" self.SSH_CONFIG_FILE_IS_REQUIRED = False self.DEFAULT_SSH_CONFIGFILE_NAME = "ssh_config.json" self.DB_PRIMARY_KEY = "name" @@ -22,40 +25,43 @@ def setup_method(self): self.JSON_DIR = "JSON" self.ANTARES_SUPPORTED_VERSIONS = ["610", "700"] - self.yaml_compulsory_content = ( - f'LOG_DIR : "{self.LOG_DIR}"\n' - f'JSON_DIR : "{self.JSON_DIR}"\n' - f'STUDIES_IN_DIR : "{self.STUDIES_IN_DIR}"\n' - f'FINISHED_DIR : "{self.FINISHED_DIR}"\n' - f"DEFAULT_TIME_LIMIT : {self.DEFAULT_TIME_LIMIT}\n" - f"DEFAULT_N_CPU : {self.DEFAULT_N_CPU}\n" - f"DEFAULT_WAIT_TIME : {self.DEFAULT_WAIT_TIME}\n" - f'DB_PRIMARY_KEY : "{self.DB_PRIMARY_KEY}"\n' - f'DEFAULT_SSH_CONFIGFILE_NAME: "{self.DEFAULT_SSH_CONFIGFILE_NAME}"\n' - f"SSH_CONFIG_FILE_IS_REQUIRED : {self.SSH_CONFIG_FILE_IS_REQUIRED}\n" - f'SLURM_SCRIPT_PATH : "{self.SLURM_SCRIPT_PATH}"\n' - f"ANTARES_VERSIONS_ON_REMOTE_SERVER :\n" - f' - "{self.ANTARES_SUPPORTED_VERSIONS[0]}"\n' - f' - "{self.ANTARES_SUPPORTED_VERSIONS[1]}"\n' + self.yaml_compulsory_content = yaml.dump( + { + "LOG_DIR": self.LOG_DIR, + "JSON_DIR": self.JSON_DIR, + "STUDIES_IN_DIR": self.STUDIES_IN_DIR, + "FINISHED_DIR": self.FINISHED_DIR, + "DEFAULT_TIME_LIMIT": self.DEFAULT_TIME_LIMIT, + "DEFAULT_N_CPU": self.DEFAULT_N_CPU, + "DEFAULT_WAIT_TIME": self.DEFAULT_WAIT_TIME, + "DB_PRIMARY_KEY": self.DB_PRIMARY_KEY, + "DEFAULT_SSH_CONFIGFILE_NAME": self.DEFAULT_SSH_CONFIGFILE_NAME, + "SSH_CONFIG_FILE_IS_REQUIRED": self.SSH_CONFIG_FILE_IS_REQUIRED, + "SLURM_SCRIPT_PATH": self.SLURM_SCRIPT_PATH, + "PARTITION": self.PARTITION, + "QUALITY_OF_SERVICE": self.QUALITY_OF_SERVICE, + "ANTARES_VERSIONS_ON_REMOTE_SERVER": self.ANTARES_SUPPORTED_VERSIONS, + }, + default_flow_style=False, ) - self.DEFAULT_JSON_DB_NAME = "db_file.json" - self.yaml_opt_content = ( - f'DEFAULT_JSON_DB_NAME : "{self.DEFAULT_JSON_DB_NAME}\n' - f'DEFAULT_SSH_CONFIGFILE_NAME: "{self.DEFAULT_SSH_CONFIGFILE_NAME}"\n' + + self.yaml_opt_content = yaml.dump( + { + "DEFAULT_JSON_DB_NAME": "db_file.json", + "DEFAULT_SSH_CONFIGFILE_NAME": self.DEFAULT_SSH_CONFIGFILE_NAME, + }, + default_flow_style=False, ) - self.USER = "user" - self.HOST = "host" - self.KEY = "C:\\home\\hello" - self.KEY_PSWD = "hello" + self.json_dict = { - "username": self.USER, - "hostname": self.HOST, - "private_key_file": self.KEY, - "key_password": self.KEY_PSWD, + "username": "user", + "hostname": "host", + "private_key_file": "C:\\home\\hello", + "key_password": "hello", } @pytest.mark.unit_test - def test_ParametersReader_raises_exception_with_no_file(self, tmp_path): + def test_parameters_reader_raises_exception_with_no_file(self, tmp_path): with pytest.raises(FileNotFoundError): ParametersReader(Path(tmp_path), Path("empty.yaml")) @@ -64,7 +70,7 @@ def test_get_option_parameters_raises_exception_with_empty_file(self, tmp_path): empty_json = tmp_path / "dummy.json" empty_yaml = tmp_path / "empty.yaml" empty_yaml.write_text("") - with pytest.raises(ParametersReader.MissingValueException): + with pytest.raises(MissingValueException): ParametersReader(empty_json, empty_yaml).get_parser_parameters() @pytest.mark.unit_test @@ -72,13 +78,11 @@ def test_get_main_parameters_raises_exception_with_empty_file(self, tmp_path): empty_json = tmp_path / "dummy.json" empty_yaml = tmp_path / "empty.yaml" empty_yaml.write_text("") - with pytest.raises(ParametersReader.MissingValueException): + with pytest.raises(MissingValueException): ParametersReader(empty_json, empty_yaml).get_main_parameters() @pytest.mark.unit_test - def test_get_option_parameters_raises_exception_if_params_are_missing( - self, tmp_path - ): + def test_get_option_parameters_raises_exception_if_params_are_missing(self, tmp_path): empty_json = tmp_path / "dummy.json" config_yaml = tmp_path / "empty.yaml" config_yaml.write_text( @@ -89,7 +93,7 @@ def test_get_option_parameters_raises_exception_if_params_are_missing( "DEFAULT_TIME_LIMIT : 172800\n" "DEFAULT_N_CPU : 2\n" ) - with pytest.raises(ParametersReader.MissingValueException): + with pytest.raises(MissingValueException): ParametersReader(empty_json, config_yaml).get_parser_parameters() @pytest.mark.unit_test @@ -104,7 +108,7 @@ def test_get_main_parameters_raises_exception_if_params_are_missing(self, tmp_pa "DEFAULT_TIME_LIMIT : 172800\n" "DEFAULT_N_CPU : 2\n" ) - with pytest.raises(ParametersReader.MissingValueException): + with pytest.raises(MissingValueException): ParametersReader(empty_json, config_yaml).get_main_parameters() @pytest.mark.unit_test @@ -113,23 +117,16 @@ def test_get_option_parameters_initializes_parameters_correctly(self, tmp_path): empty_json.write_text("{}") config_yaml = tmp_path / "empty.yaml" config_yaml.write_text(self.yaml_compulsory_content) - options_parameters = ParametersReader( - empty_json, config_yaml - ).get_parser_parameters() + options_parameters = ParametersReader(empty_json, config_yaml).get_parser_parameters() assert options_parameters.log_dir == self.LOG_DIR assert options_parameters.studies_in_dir == self.STUDIES_IN_DIR assert options_parameters.finished_dir == self.FINISHED_DIR assert options_parameters.default_time_limit == self.DEFAULT_TIME_LIMIT assert options_parameters.default_n_cpu == self.DEFAULT_N_CPU assert options_parameters.default_wait_time == self.DEFAULT_WAIT_TIME - assert ( - options_parameters.ssh_config_file_is_required - == self.SSH_CONFIG_FILE_IS_REQUIRED - ) + assert options_parameters.ssh_config_file_is_required == self.SSH_CONFIG_FILE_IS_REQUIRED alternate1 = Path.cwd() / self.DEFAULT_SSH_CONFIGFILE_NAME - alternate2 = ( - Path.home() / "antares_launcher_settings" / self.DEFAULT_SSH_CONFIGFILE_NAME - ) + alternate2 = Path.home() / "antares_launcher_settings" / self.DEFAULT_SSH_CONFIGFILE_NAME assert options_parameters.ssh_configfile_path_alternate1 == alternate1 assert options_parameters.ssh_configfile_path_alternate2 == alternate2 @@ -140,21 +137,15 @@ def test_get_main_parameters_initializes_parameters_correctly(self, tmp_path): config_yaml.write_text(self.yaml_compulsory_content) empty_json = tmp_path / "dummy.json" empty_json.write_text("{}") - main_parameters = ParametersReader( - empty_json, config_yaml - ).get_main_parameters() + main_parameters = ParametersReader(empty_json, config_yaml).get_main_parameters() assert main_parameters.json_dir == Path(self.JSON_DIR) assert main_parameters.slurm_script_path == self.SLURM_SCRIPT_PATH - assert ( - main_parameters.default_json_db_name - == f"{getpass.getuser()}_antares_launcher_db.json" - ) + assert main_parameters.default_json_db_name == f"{getpass.getuser()}_antares_launcher_db.json" + assert main_parameters.partition == self.PARTITION + assert main_parameters.quality_of_service == self.QUALITY_OF_SERVICE assert main_parameters.db_primary_key == self.DB_PRIMARY_KEY assert not main_parameters.default_ssh_dict - assert ( - main_parameters.antares_versions_on_remote_server - == self.ANTARES_SUPPORTED_VERSIONS - ) + assert main_parameters.antares_versions_on_remote_server == self.ANTARES_SUPPORTED_VERSIONS @pytest.mark.unit_test def test_get_main_parameters_initializes_default_ssh_dict_correctly(self, tmp_path): @@ -164,7 +155,5 @@ def test_get_main_parameters_initializes_default_ssh_dict_correctly(self, tmp_pa with open(ssh_json, "w") as file: json.dump(self.json_dict, file) - main_parameters = ParametersReader( - json_ssh_conf=ssh_json, yaml_filepath=config_yaml - ).get_main_parameters() + main_parameters = ParametersReader(json_ssh_conf=ssh_json, yaml_filepath=config_yaml).get_main_parameters() assert main_parameters.default_ssh_dict == self.json_dict diff --git a/tests/unit/test_remote_environment_with_slurm.py b/tests/unit/test_remote_environment_with_slurm.py index 8116ec0..b472eed 100644 --- a/tests/unit/test_remote_environment_with_slurm.py +++ b/tests/unit/test_remote_environment_with_slurm.py @@ -17,10 +17,7 @@ RemoteEnvironmentWithSlurm, SubmitJobError, ) -from antareslauncher.remote_environnement.slurm_script_features import ( - ScriptParametersDTO, - SlurmScriptFeatures, -) +from antareslauncher.remote_environnement.slurm_script_features import ScriptParametersDTO, SlurmScriptFeatures from antareslauncher.study_dto import Modes, StudyDTO @@ -53,7 +50,7 @@ def study(self) -> StudyDTO: path="path/to/study/91f1f911-4f4a-426f-b127-d0c2a2465b5f", n_cpu=42, zipfile_path="path/to/study/91f1f911-4f4a-426f-b127-d0c2a2465b5f-foo.zip", - antares_version="700", + antares_version=700, local_final_zipfile_path="local_final_zipfile_path", run_mode=Modes.antares, ) @@ -64,7 +61,11 @@ def remote_env(self) -> RemoteEnvironmentWithSlurm: remote_home_dir = "remote_home_dir" connection = mock.Mock(home_dir="path/to/home") connection.home_dir = remote_home_dir - slurm_script_features = SlurmScriptFeatures("slurm_script_path") + slurm_script_features = SlurmScriptFeatures( + "slurm_script_path", + partition="fake_partition", + quality_of_service="user1_qos", + ) return RemoteEnvironmentWithSlurm(connection, slurm_script_features) @pytest.mark.unit_test @@ -73,14 +74,16 @@ def test_initialise_remote_path_calls_connection_make_dir_with_correct_arguments ): # given remote_home_dir = "remote_home_dir" - remote_base_dir = ( - f"{remote_home_dir}/REMOTE_{getpass.getuser()}_{socket.gethostname()}" - ) + remote_base_dir = f"{remote_home_dir}/REMOTE_{getpass.getuser()}_{socket.gethostname()}" connection = mock.Mock(home_dir="path/to/home") connection.home_dir = remote_home_dir connection.make_dir = mock.Mock(return_value=True) connection.check_file_not_empty = mock.Mock(return_value=True) - slurm_script_features = SlurmScriptFeatures("slurm_script_path") + slurm_script_features = SlurmScriptFeatures( + "slurm_script_path", + partition="fake_partition", + quality_of_service="user1_qos", + ) # when RemoteEnvironmentWithSlurm(connection, slurm_script_features) # then @@ -92,7 +95,11 @@ def test_when_constructor_is_called_and_remote_base_path_cannot_be_created_then_ ): # given connection = mock.Mock(home_dir="path/to/home") - slurm_script_features = SlurmScriptFeatures("slurm_script_path") + slurm_script_features = SlurmScriptFeatures( + "slurm_script_path", + partition="fake_partition", + quality_of_service="user1_qos", + ) # when connection.make_dir = mock.Mock(return_value=False) # then @@ -107,7 +114,11 @@ def test_when_constructor_is_called_then_connection_check_file_not_empty_is_call connection = mock.Mock(home_dir="path/to/home") connection.make_dir = mock.Mock(return_value=True) connection.check_file_not_empty = mock.Mock(return_value=True) - slurm_script_features = SlurmScriptFeatures("slurm_script_path") + slurm_script_features = SlurmScriptFeatures( + "slurm_script_path", + partition="fake_partition", + quality_of_service="user1_qos", + ) # when RemoteEnvironmentWithSlurm(connection, slurm_script_features) # then @@ -123,7 +134,11 @@ def test_when_constructor_is_called_and_connection_check_file_not_empty_is_false connection = mock.Mock(home_dir="path/to/home") connection.home_dir = remote_home_dir connection.make_dir = mock.Mock(return_value=True) - slurm_script_features = SlurmScriptFeatures("slurm_script_path") + slurm_script_features = SlurmScriptFeatures( + "slurm_script_path", + partition="fake_partition", + quality_of_service="user1_qos", + ) # when connection.check_file_not_empty = mock.Mock(return_value=False) # then @@ -131,9 +146,7 @@ def test_when_constructor_is_called_and_connection_check_file_not_empty_is_false RemoteEnvironmentWithSlurm(connection, slurm_script_features) @pytest.mark.unit_test - def test_get_queue_info_calls_connection_execute_command_with_correct_argument( - self, remote_env - ): + def test_get_queue_info_calls_connection_execute_command_with_correct_argument(self, remote_env): # given username = "username" host = "host" @@ -149,9 +162,7 @@ def test_get_queue_info_calls_connection_execute_command_with_correct_argument( remote_env.connection.execute_command.assert_called_with(command) @pytest.mark.unit_test - def test_when_connection_exec_command_has_an_error_then_get_queue_info_returns_the_error_string( - self, remote_env - ): + def test_when_connection_exec_command_has_an_error_then_get_queue_info_returns_the_error_string(self, remote_env): # given username = "username" remote_env.connection.username = username @@ -174,9 +185,7 @@ def test_kill_remote_job_execute_scancel_command(self, remote_env): remote_env.connection.execute_command.assert_called_with(command) @pytest.mark.unit_test - def test_when_kill_remote_job_is_called_and_exec_command_returns_error_exception_is_raised( - self, remote_env - ): + def test_when_kill_remote_job_is_called_and_exec_command_returns_error_exception_is_raised(self, remote_env): # when output = None error = "error" @@ -204,15 +213,11 @@ def test_when_submit_job_is_called_then_execute_command_is_called_with_specific_ post_processing=study.post_processing, other_options="", ) - command = remote_env.slurm_script_features.compose_launch_command( - remote_env.remote_base_path, script_params - ) + command = remote_env.slurm_script_features.compose_launch_command(remote_env.remote_base_path, script_params) remote_env.connection.execute_command.assert_called_once_with(command) @pytest.mark.unit_test - def test_when_submit_job_is_called_and_receives_submitted_420_returns_job_id_420( - self, remote_env, study - ): + def test_when_submit_job_is_called_and_receives_submitted_420_returns_job_id_420(self, remote_env, study): # when output = "Submitted 420" error = None @@ -221,9 +226,7 @@ def test_when_submit_job_is_called_and_receives_submitted_420_returns_job_id_420 assert remote_env.submit_job(study) == 420 @pytest.mark.unit_test - def test_when_submit_job_is_called_and_receives_error_then_exception_is_raised( - self, remote_env, study - ): + def test_when_submit_job_is_called_and_receives_error_then_exception_is_raised(self, remote_env, study): # when output = "" error = "error" @@ -356,6 +359,7 @@ def execute_command_mock(cmd: str): call(command), ] + # noinspection SpellCheckingInspection @pytest.mark.unit_test @pytest.mark.parametrize( "state, expected", @@ -367,12 +371,11 @@ def execute_command_mock(cmd: str): ("CANCELLED by 123456", (True, True, True)), ("TIMEOUT", (True, True, True)), ("COMPLETED", (True, True, False)), + ("COMPLETING", (True, False, False)), ("FAILED", (True, True, True)), ], ) - def test_get_job_state_flags__sacct_nominal_case( - self, remote_env, study, state, expected - ): + def test_get_job_state_flags__sacct_nominal_case(self, remote_env, study, state, expected): """ Check that the "get_job_state_flags" method is correctly returning the status flags ("started", "finished", and "with_error") @@ -530,9 +533,7 @@ def test_given_a_study_with_input_zipfile_removed_when_remove_input_zipfile_then assert output is True @pytest.mark.unit_test - def test_given_a_study_when_remove_input_zipfile_then_connection_remove_file_is_called( - self, remote_env, study - ): + def test_given_a_study_when_remove_input_zipfile_then_connection_remove_file_is_called(self, remote_env, study): # given study.input_zipfile_removed = False study.zipfile_path = "zipfile_path" @@ -564,9 +565,7 @@ def test_given_a_study_when_remove_remote_final_zipfile_then_connection_remove_f # given study.input_zipfile_removed = False study.zipfile_path = "zipfile_path" - command = ( - f"{remote_env.remote_base_path}/{Path(study.local_final_zipfile_path).name}" - ) + command = f"{remote_env.remote_base_path}/{Path(study.local_final_zipfile_path).name}" remote_env.connection.execute_command = mock.Mock(return_value=("", "")) # when remote_env.remove_remote_final_zipfile(study) @@ -586,9 +585,7 @@ def test_given_a_study_with_clean_remote_server_when_clean_remote_server_called_ assert output is False @pytest.mark.unit_test - def test_given_a_study_when_clean_remote_server_called_then_remove_zip_methods_are_called( - self, remote_env, study - ): + def test_given_a_study_when_clean_remote_server_called_then_remove_zip_methods_are_called(self, remote_env, study): # given study.remote_server_is_clean = False remote_env.remove_remote_final_zipfile = mock.Mock(return_value=False) @@ -600,9 +597,7 @@ def test_given_a_study_when_clean_remote_server_called_then_remove_zip_methods_a remote_env.remove_input_zipfile.assert_called_once_with(study) @pytest.mark.unit_test - def test_given_a_study_when_clean_remote_server_called_then_return_correct_result( - self, remote_env, study - ): + def test_given_a_study_when_clean_remote_server_called_then_return_correct_result(self, remote_env, study): # given study.remote_server_is_clean = False remote_env.remove_remote_final_zipfile = mock.Mock(return_value=False) @@ -643,9 +638,7 @@ def test_given_time_limit_lower_than_min_duration_when_convert_time_is_called_re # given time_lim_sec = 42 # when - output = RemoteEnvironmentWithSlurm.convert_time_limit_from_seconds_to_minutes( - time_lim_sec - ) + output = RemoteEnvironmentWithSlurm.convert_time_limit_from_seconds_to_minutes(time_lim_sec) # then assert output == 1 @@ -689,11 +682,13 @@ def test_compose_launch_command( change_dir = f"cd {remote_env.remote_base_path}" reference_submit_command = ( f"sbatch" - f' --job-name="{Path(study.path).name}"' + " --partition=fake_partition" + " --qos=user1_qos" + f" --job-name={Path(study.path).name}" f" --time={study.time_limit // 60}" f" --cpus-per-task={study.n_cpu}" f" {filename_launch_script}" - f' "{Path(study.zipfile_path).name}"' + f" {Path(study.zipfile_path).name}" f" {study.antares_version}" f" {job_type}" f" {post_processing}" diff --git a/tests/unit/test_slurm_queue_show.py b/tests/unit/test_slurm_queue_show.py index f6b42a2..87a12d2 100644 --- a/tests/unit/test_slurm_queue_show.py +++ b/tests/unit/test_slurm_queue_show.py @@ -2,9 +2,7 @@ import pytest -from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import ( - SlurmQueueShow, -) +from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import SlurmQueueShow @pytest.mark.unit_test diff --git a/tests/unit/test_ssh_connection.py b/tests/unit/test_ssh_connection.py index 4086db3..340d689 100644 --- a/tests/unit/test_ssh_connection.py +++ b/tests/unit/test_ssh_connection.py @@ -7,12 +7,13 @@ import paramiko import pytest +from paramiko.sftp_attr import SFTPAttributes + from antareslauncher.remote_environnement.ssh_connection import ( ConnectionFailedException, DownloadMonitor, SshConnection, ) -from paramiko.sftp_attr import SFTPAttributes LOGGER = DownloadMonitor.__module__ diff --git a/tests/unit/test_study_list_composer.py b/tests/unit/test_study_list_composer.py index b31ecf1..427bb28 100644 --- a/tests/unit/test_study_list_composer.py +++ b/tests/unit/test_study_list_composer.py @@ -1,442 +1,120 @@ from pathlib import Path -from unittest import mock -from unittest.mock import call import pytest -from antareslauncher.data_repo.idata_repo import IDataRepo -from antareslauncher.file_manager.file_manager import FileManager -from antareslauncher.study_dto import Modes, StudyDTO -from antareslauncher.use_cases.create_list.study_list_composer import ( - StudyListComposer, - StudyListComposerParameters, -) -from tests.data import DATA_DIR +from antareslauncher.use_cases.create_list.study_list_composer import StudyListComposer, get_solver_version + +CONFIG_NOMINAL_VERSION = """\ +[antares] +version = 800 +caption = Sample Study +created = 1688740888 +lastsave = 1688740888 +author = john.doe +""" + +CONFIG_NOMINAL_SOLVER_VERSION = """\ +[antares] +version = 800 +caption = Sample Study +created = 1688740888 +lastsave = 1688740888 +author = john.doe +solver_version = 850 +""" + +CONFIG_MISSING_SECTION = """\ +[polaris] +version = 800 +caption = Sample Study +created = 1688740888 +lastsave = 1688740888 +author = john.doe +""" + +CONFIG_MISSING_VERSION = """\ +[antares] +caption = Sample Study +created = 1688740888 +lastsave = 1688740888 +author = john.doe +""" + + +class TestGetSolverVersion: + @pytest.mark.parametrize( + "config_ini, expected", + [ + pytest.param(CONFIG_NOMINAL_VERSION, 800, id="with_version"), + pytest.param(CONFIG_NOMINAL_SOLVER_VERSION, 850, id="with_solver_version"), + pytest.param(CONFIG_MISSING_SECTION, 999, id="bad_missing_section"), + pytest.param(CONFIG_MISSING_VERSION, 999, id="bad_missing_version"), + ], + ) + def test_get_solver_version( + self, + config_ini: str, + expected: int, + tmp_path: Path, + ) -> None: + study_path = tmp_path.joinpath("study.antares") + study_path.write_text(config_ini, encoding="utf-8") + actual = get_solver_version(tmp_path, default=999) + assert actual == expected class TestStudyListComposer: - def setup_method(self): - self.parameters = StudyListComposerParameters( - studies_in_dir="", - time_limit=0, - n_cpu=1, - log_dir="job_log_dir", - xpansion_mode=None, - output_dir="output_dir", - post_processing=False, - antares_versions_on_remote_server=["610", "700", "800"], - other_options="", - ) - - @pytest.fixture(scope="function") - def study_mock(self): - study = mock.Mock() - return study - - @pytest.mark.unit_test - def test_given_repo_when_get_list_of_studies_called_then_repo_get_list_of_studies_is_called( - self, - ): - - # given - repo_mock = mock.Mock() - repo_mock.get_list_of_studies = mock.Mock() - study_list_composer = StudyListComposer( - repo=repo_mock, - file_manager=None, - display=None, - parameters=self.parameters, - ) - # when - study_list_composer.get_list_of_studies() - # then - repo_mock.get_list_of_studies.assert_called_once() - - @pytest.mark.unit_test - def test_given_repo_when_get_list_of_studies_called_then_repo_get_list_of_studies_is_called( - self, - ): - # given - - repo_mock = mock.Mock() - repo_mock.get_list_of_studies = mock.Mock() - study_list_composer = StudyListComposer( - repo=repo_mock, - file_manager=None, - display=None, - parameters=self.parameters, - ) - # when - study_list_composer.get_list_of_studies() - # then - repo_mock.get_list_of_studies.assert_called_once() - - @pytest.mark.unit_test - def test_when_is_dir_an_antares_study_is_called_then_the_file_study_antares_is_checked( - self, - ): - # given - - dir_path = "dir_path" - expected_config_file_path = Path(dir_path) / "study.antares" - study_list_composer = StudyListComposer( - repo=None, - file_manager=mock.Mock(), - display=None, - parameters=self.parameters, - ) - # when - study_list_composer._file_manager.get_config_from_file = mock.Mock( - return_value={} - ) - return_value = study_list_composer.get_antares_version(dir_path) - # then - study_list_composer._file_manager.get_config_from_file.assert_called_once_with( - expected_config_file_path - ) - assert not return_value - - @pytest.mark.unit_test - def test_when_antares_is_in_the_config_file_then_is_dir_an_antares_study_return_true( - self, - ): - # given - - dir_path = "dir_path" - expected_config_file_path = Path(dir_path) / "study.antares" - study_list_composer = StudyListComposer( - repo=None, - file_manager=mock.Mock(), - display=None, - parameters=self.parameters, - ) - # when - study_list_composer._file_manager.get_config_from_file = mock.Mock( - return_value={"antares": {"version": 42}} - ) - return_value = study_list_composer.get_antares_version(dir_path) - # then - study_list_composer._file_manager.get_config_from_file.assert_called_once_with( - expected_config_file_path - ) - assert return_value - - @pytest.mark.unit_test - def test_given_existing_db_when_no_new_study_then_do_nothing_and_show_message( + @pytest.mark.parametrize("xpansion_mode", ["r", "cpp", ""]) + def test_update_study_database__xpansion_mode( self, + study_list_composer: StudyListComposer, + xpansion_mode: str, ): - # given - self.parameters.studies_in_dir = "studies_in_dir" - - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(listdir_of=mock.Mock(return_value=["study"])), - display=mock.Mock(), - parameters=self.parameters, - ) - study_list_composer.get_antares_version = mock.Mock(return_value=True) - study_list_composer._repo.is_study_inside_database = mock.Mock( - return_value=True - ) - - # when + study_list_composer.xpansion_mode = xpansion_mode study_list_composer.update_study_database() - - # then - assert study_list_composer._display.show_message.call_count == 3 - - @pytest.mark.unit_test - def test_given_existing_db_when_new_study_then_save_new_study_and_show_message( - self, - ): - self.parameters.studies_in_dir = "studies_in_dir" - self.parameters.time_limit = 24 - self.parameters.n_cpu = 42 - file_manager = mock.create_autospec(FileManager) - file_manager.file_exists = mock.create_autospec( - FileManager.file_exists, return_value=False - ) - file_manager.listdir_of = mock.Mock(return_value=["study_path"]) - repo = mock.create_autospec(IDataRepo, instance=True) - repo.is_study_inside_database = mock.Mock(return_value=False) - # given - study_list_composer = StudyListComposer( - repo=repo, - file_manager=file_manager, - display=mock.Mock(), - parameters=self.parameters, - ) - study_list_composer.get_antares_version = mock.Mock(return_value="700") - study_list_composer._file_manager.is_dir = mock.Mock(return_value=True) - expected_save_study = StudyDTO( - path=str(Path(self.parameters.studies_in_dir) / "study_path"), - antares_version="700", - job_log_dir=str(Path(self.parameters.log_dir) / "JOB_LOGS"), - output_dir=self.parameters.output_dir, - time_limit=self.parameters.time_limit, - n_cpu=self.parameters.n_cpu, - other_options="", - ) - # when + studies = study_list_composer.get_list_of_studies() + + # check the found studies + actual_names = {s.name for s in studies} + expected_names = { + "": { + "013 TS Generation - Solar power", + "024 Hurdle costs - 1", + "SMTA-case", + }, + "r": {"SMTA-case"}, + "cpp": {"SMTA-case"}, + }[study_list_composer.xpansion_mode or ""] + assert actual_names == expected_names + + @pytest.mark.parametrize("antares_version", [0, 850, 990]) + def test_update_study_database__antares_version( + self, + study_list_composer: StudyListComposer, + antares_version: int, + ): + study_list_composer.antares_version = antares_version study_list_composer.update_study_database() - - # then - calls = study_list_composer._repo.save_study.call_args_list - assert calls[0] == call(expected_save_study) - assert study_list_composer._display.show_message.call_count == 2 - - @pytest.mark.unit_test - def test_given_empty_study_dir_list_when_update_study_database_called_then_display_show_two_messages( - self, - ): - # given - study_list_composer = StudyListComposer( - repo=None, - file_manager=mock.Mock(listdir_of=mock.Mock(return_value=[])), - display=mock.Mock(), - parameters=self.parameters, - ) - # when - study_list_composer.update_study_database() - # then - assert study_list_composer._display.show_message.call_count == 2 - - @pytest.mark.unit_test - def test_given_two_new_studies_when_update_study_database_called_then_display_show_three_messages( - self, - ): - # given - self.parameters.studies_in_dir = "studies_in_dir" - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock( - listdir_of=mock.Mock(return_value=["study1", "study2"]) - ), - display=mock.Mock(), - parameters=self.parameters, - ) - study_list_composer.get_antares_version = mock.Mock(return_value="700") - study_list_composer._repo.is_study_inside_database = mock.Mock( - return_value=False - ) - study_list_composer._file_manager.is_dir = mock.Mock(return_value=True) - # when - study_list_composer.update_study_database() - # then - assert study_list_composer._display.show_message.call_count == 3 - - @pytest.mark.unit_test - def test_given_directory_path_when_create_study_is_called_then_return_study_dto_with_righ_values( - self, - ): - # given - self.parameters.studies_in_dir = "studies_in_dir" - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(), - display=mock.Mock(), - parameters=self.parameters, - ) - - study_dir = study_list_composer._studies_in_dir - antares_version = 700 - - is_xpansion_study = None - # when - new_study_dto = study_list_composer._create_study( - study_dir, antares_version, is_xpansion_study - ) - # then - assert new_study_dto.path == study_list_composer._studies_in_dir - assert new_study_dto.n_cpu == study_list_composer.n_cpu - assert new_study_dto.time_limit == study_list_composer.time_limit - assert new_study_dto.antares_version == antares_version - assert new_study_dto.job_log_dir == str( - Path(study_list_composer.log_dir) / "JOB_LOGS" - ) - - @pytest.mark.unit_test - def test_given_an_antares_version_when_is_valid_antares_study_is_called_return_boolean_value( - self, - ): - # given - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(), - display=mock.Mock(), - parameters=self.parameters, - ) - antares_version_700 = "700" - wrong_antares_version = "137" - antares_version_none = None - # when - is_valid_antares_study_expected_true = ( - study_list_composer._is_valid_antares_study(antares_version_700) - ) - is_valid_antares_study_expected_false = ( - study_list_composer._is_valid_antares_study(wrong_antares_version) - ) - is_valid_antares_study_expected_false2 = ( - study_list_composer._is_valid_antares_study(antares_version_none) - ) - # then - assert is_valid_antares_study_expected_true is True - assert is_valid_antares_study_expected_false is False - assert is_valid_antares_study_expected_false2 is False - - @pytest.mark.unit_test - def test_given_a_none_antares_version_when_is_antares_study_is_called_return_false_and_message( - self, - ): - # given - display_mock = mock.Mock() - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(), - display=display_mock, - parameters=self.parameters, - ) - display_mock.show_message = mock.Mock() - antares_version = None - # when - is_antares_study = study_list_composer._is_valid_antares_study(antares_version) - # then - assert is_antares_study is False - display_mock.show_message.assert_called_once_with( - "... not a valid Antares study", mock.ANY - ) - - @pytest.mark.unit_test - def test_given_a_non_supported_antares_version_when_is_antares_study_is_called_return_false_and_message( - self, - ): - # given - display_mock = mock.Mock() - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(), - display=display_mock, - parameters=self.parameters, - ) - display_mock.show_message = mock.Mock() - antares_version = "600" - message = f"... Antares version ({antares_version}) is not supported (supported versions: {self.parameters.antares_versions_on_remote_server})" - # when - is_antares_study = study_list_composer._is_valid_antares_study(antares_version) - # then - assert is_antares_study is False - display_mock.show_message.assert_called_once_with(message, mock.ANY) - - @pytest.mark.unit_test - def test_given_xpansion_study_path_when_is_xpansion_study_is_called_return_true( - self, - ): - # given - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(), - display=mock.Mock(), - parameters=self.parameters, - ) - xpansion_study_path = Path("xpansion_study_path") - study_list_composer._is_there_candidates_file = mock.Mock(return_value=True) - # when - is_xpansion_study = study_list_composer._is_xpansion_study(xpansion_study_path) - # then - assert is_xpansion_study is True - - @pytest.mark.unit_test - def test_given_xpansion_study_path_when_create_study_is_called_then_xpansion_value_of_dto_is_true( - self, - ): - # given - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(), - display=mock.Mock(), - parameters=self.parameters, - ) - - study_dir = study_list_composer._studies_in_dir - antares_version = 700 - - is_xpansion_study = "r" - # when - new_study_dto = study_list_composer._create_study( - study_dir, antares_version, is_xpansion_study - ) - # then - assert new_study_dto.xpansion_mode == "r" - - @pytest.mark.unit_test - def test_given_xpansion_mode_option_when_create_study_is_called_then_run_mode_value_of_dto_is_xpansion_mode( - self, - ): - # given - self.parameters.xpansion_mode = "r" - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(), - display=mock.Mock(), - parameters=self.parameters, - ) - - study_dir = study_list_composer._studies_in_dir - antares_version = 700 - is_xpansion_study = "r" - # when - new_study_dto = study_list_composer._create_study( - study_dir, antares_version, is_xpansion_study - ) - # then - assert new_study_dto.run_mode == Modes.xpansion_r - - @pytest.mark.unit_test - def test_given_xpansion_mode_option_when_update_study_only_xpansion_studies_are_saved_in_database( - self, - ): - # given - self.parameters.xpansion_mode = "r" - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(), - display=mock.Mock(), - parameters=self.parameters, - ) - study_list_composer._update_database_with_study = mock.Mock() - study_list_composer.get_antares_version = mock.Mock(return_value="610") - - study_dir = study_list_composer._studies_in_dir - is_xpansion_study = True - study_list_composer._is_xpansion_study = mock.Mock( - return_value=is_xpansion_study - ) - study_list_composer._update_database_with_directory(study_dir) - - isnot_xpansion_study = False - study_list_composer._is_xpansion_study = mock.Mock( - return_value=isnot_xpansion_study - ) - # when - study_list_composer._update_database_with_directory(study_dir) - # then - study_list_composer._update_database_with_study.assert_called_once() - - @pytest.mark.unit_test - def test_given_a_study_path_when_is_there_candidates_file_is_called_return_true_if_present( - self, - ): - # given - directory_path = DATA_DIR.joinpath("xpansion-reference") - file_manager = FileManager(display_terminal=mock.Mock()) - my_study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=file_manager, - display=mock.Mock(), - parameters=self.parameters, - ) - - # when - output = my_study_list_composer._is_there_candidates_file(directory_path) - # then - assert output + studies = study_list_composer.get_list_of_studies() + + # check the versions + actual_versions = {s.name: s.antares_version for s in studies} + if antares_version == 0: + expected_versions = { + "013 TS Generation - Solar power": 850, # solver_version + "024 Hurdle costs - 1": 840, # versions + "SMTA-case": 810, # version + } + elif antares_version in study_list_composer.ANTARES_VERSIONS_ON_REMOTE_SERVER: + study_names = { + "013 TS Generation - Solar power", + "024 Hurdle costs - 1", + "069 Hydro Reservoir Model", + "BAD Study Section", + "MISSING Study version", + "SMTA-case", + } + expected_versions = dict.fromkeys(study_names, antares_version) + else: + expected_versions = {} + assert actual_versions == {n: expected_versions[n] for n in actual_versions} diff --git a/tests/unit/test_wait_controller.py b/tests/unit/test_wait_controller.py index e7b452c..b0a479e 100644 --- a/tests/unit/test_wait_controller.py +++ b/tests/unit/test_wait_controller.py @@ -2,10 +2,8 @@ import pytest -from antareslauncher.display.idisplay import IDisplay -from antareslauncher.use_cases.wait_loop_controller.wait_controller import ( - WaitController, -) +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.use_cases.wait_loop_controller.wait_controller import WaitController class TestWaitController: @@ -13,7 +11,7 @@ class TestWaitController: def test_countdown_calls_display_message_4_times_if_it_waits_1_seconds( self, ): - display = IDisplay + display = DisplayTerminal() display.show_message = mock.Mock() display.show_message_no_newline = mock.Mock() wait_controller = WaitController(display)