diff --git a/boa/__main__.py b/boa/__main__.py index 8914c09..137d9bf 100644 --- a/boa/__main__.py +++ b/boa/__main__.py @@ -1,218 +1,10 @@ -import os -import sys -import tempfile -from pathlib import Path +def main(): + from boa.cli import main as _main -import click -from attrs import fields_dict -from ax.storage.json_store.decoder import object_from_json - -from boa.config import BOAScriptOptions -from boa.controller import Controller -from boa.storage import scheduler_from_json_file -from boa.wrappers.script_wrapper import ScriptWrapper -from boa.wrappers.wrapper_utils import cd_and_cd_back, load_jsonlike - - -@click.command() -@click.option( - "-c", - "--config-path", - type=click.Path(exists=True, dir_okay=False, path_type=Path), - help="Path to configuration YAML file.", -) -@click.option( - "-sp", - "--scheduler-path", - type=click.Path(), - default="", - help="Path to scheduler json file.", -) -@click.option( - "-wp", - "--wrapper-path", - type=click.Path(), - default="", - help="Path to where file where your wrapper is located. Used when loaded from scheduler json file," - " and the path to your wrapper has changed (such as when loading on a different computer then" - " originally ran from).", -) -@click.option( - "-wn", - "--wrapper-name", - type=str, - default="", - help="Name of the wrapper class to use. Used when loaded from scheduler json file,", -) -@click.option( - "-td", - "--temporary-dir", - is_flag=True, - show_default=True, - default=False, - help="Modify/add to the config file a temporary directory as the experiment_dir that will get deleted after running" - " (useful for testing)." - " This requires your Wrapper to have the ability to take experiment_dir as an argument" - " to ``load_config``. The default ``load_config`` does support this." - " This is also only done for initial run, not for reloading from scheduler json file.", -) -@click.option( - "--rel-to-config/--rel-to-here", # more cli friendly name for config option of rel_to_launch - default=None, - help="Define all path and dir options in your config file relative to where boa is launched from" - " instead of relative to the config file location (the default)" - " ex:" - " given working_dir=path/to/dir" - " if you don't pass --rel-to-here then path/to/dir is defined in terms of where your config file is" - " if you do pass --rel-to-here then path/to/dir is defined in terms of where you launch boa from", -) -def main(config_path, scheduler_path, wrapper_path, wrapper_name, temporary_dir, rel_to_config): - """Run experiment run from config path or scheduler path""" - - if temporary_dir: - with tempfile.TemporaryDirectory() as temp_dir: - experiment_dir = Path(temp_dir) - return run( - config_path, - scheduler_path=scheduler_path, - wrapper_path=wrapper_path, - wrapper_name=wrapper_name, - rel_to_config=rel_to_config, - experiment_dir=experiment_dir, - ) - return run(config_path, scheduler_path=scheduler_path, wrapper_path=wrapper_path, rel_to_config=rel_to_config) - - -def run(config_path, scheduler_path, rel_to_config, wrapper_path=None, wrapper_name=None, experiment_dir=None): - """Run experiment run from config path or scheduler path - - Parameters - ---------- - config_path - Path to configuration YAML file. - scheduler_path - Path to scheduler json file. - wrapper_path - Path to where file where your wrapper is located. Used when loaded from scheduler json file, - and the path to your wrapper has changed (such as when loading on a different computer then - originally ran from). - rel_to_config - Define all path and dir options in your config file relative to to your config file location - or rel_to_here (relative to cli launch) - experiment_dir - experiment output directory to save BOA run to, can only be specified during an initial run - (when passing in a config_path, not a scheduler_path) - - Returns - ------- - Scheduler - """ - config = {} - script_options = {} - if config_path: - config_path = Path(config_path).resolve() - config = load_jsonlike(config_path) - script_options = config.get("script_options", {}) - if rel_to_config is None: - rel_to_config = get_rel_from_script_options(script_options) - if scheduler_path: - scheduler_path = Path(scheduler_path).resolve() - if not config: - sch_jsn = load_jsonlike(scheduler_path) - config = object_from_json(sch_jsn["wrapper"]["config"]) - config_path = object_from_json(sch_jsn["wrapper"]["config_path"]) - script_options = config.get("script_options", {}) - if rel_to_config is None: - rel_to_config = get_rel_from_script_options(script_options) - - if experiment_dir: - experiment_dir = Path(experiment_dir).resolve() - wrapper_path = Path(wrapper_path).resolve() if wrapper_path else None - - if config_path and rel_to_config: - rel_path = config_path.parent - else: - rel_path = os.getcwd() - - options = get_config_options( - experiment_dir=experiment_dir, rel_path=rel_path, script_options=script_options, wrapper_path=wrapper_path - ) - - if wrapper_name: - options["wrapper_name"] = wrapper_name - - with cd_and_cd_back(options["working_dir"]): - if scheduler_path: - scheduler = scheduler_from_json_file(filepath=scheduler_path, wrapper_path=options["wrapper_path"]) - controller = Controller.from_scheduler(scheduler=scheduler, **options) - else: - if options["wrapper_path"] and Path(options["wrapper_path"]).exists(): - options["wrapper"] = options["wrapper_path"] - else: - options["wrapper"] = ScriptWrapper - controller = Controller( - config_path=config_path, - **options, - ) - controller.initialize_scheduler() - - scheduler = controller.run() - return scheduler - - -def get_rel_from_script_options(script_options): - rel_to_config = script_options.get("rel_to_config", None) or not script_options.get("rel_to_launch", None) - if rel_to_config is None: - rel_to_config = ( - fields_dict(BOAScriptOptions)["rel_to_config"].default - or not fields_dict(BOAScriptOptions)["rel_to_launch"].default - ) - return rel_to_config - - -def get_config_options(experiment_dir, rel_path, script_options: dict = None, wrapper_path=None): - script_options = script_options if script_options is not None else {} - wrapper_name = script_options.get("wrapper_name", fields_dict(BOAScriptOptions)["wrapper_name"].default) - append_timestamp = ( - script_options.get("append_timestamp", None) - if script_options.get("append_timestamp", None) is not None - else fields_dict(BOAScriptOptions)["append_timestamp"].default - ) - - wrapper_path = ( - wrapper_path - if wrapper_path is not None - else script_options.get("wrapper_path", fields_dict(BOAScriptOptions)["wrapper_path"].default) - ) - wrapper_path = _prepend_rel_path(rel_path, wrapper_path) if wrapper_path else wrapper_path - - working_dir = script_options.get("working_dir", fields_dict(BOAScriptOptions)["working_dir"].default) - working_dir = _prepend_rel_path(rel_path, working_dir) - - experiment_dir = experiment_dir or script_options.get( - "experiment_dir", fields_dict(BOAScriptOptions)["experiment_dir"].default - ) - experiment_dir = _prepend_rel_path(rel_path, experiment_dir) if experiment_dir else experiment_dir - - if working_dir: - sys.path.append(str(working_dir)) - - return dict( - append_timestamp=append_timestamp, - experiment_dir=experiment_dir, - working_dir=working_dir, - wrapper_name=wrapper_name, - wrapper_path=wrapper_path, - ) - - -def _prepend_rel_path(rel_path, path): - if not path: - return path - path = Path(path) - if not path.is_absolute(): - path = rel_path / path - return path.resolve() + # Main entry point + # Can be invoked with `python -m boa` + # or just `boa` (see pypoject.toml` + _main() if __name__ == "__main__": diff --git a/boa/cli/__init__.py b/boa/cli/__init__.py new file mode 100644 index 0000000..8914c09 --- /dev/null +++ b/boa/cli/__init__.py @@ -0,0 +1,219 @@ +import os +import sys +import tempfile +from pathlib import Path + +import click +from attrs import fields_dict +from ax.storage.json_store.decoder import object_from_json + +from boa.config import BOAScriptOptions +from boa.controller import Controller +from boa.storage import scheduler_from_json_file +from boa.wrappers.script_wrapper import ScriptWrapper +from boa.wrappers.wrapper_utils import cd_and_cd_back, load_jsonlike + + +@click.command() +@click.option( + "-c", + "--config-path", + type=click.Path(exists=True, dir_okay=False, path_type=Path), + help="Path to configuration YAML file.", +) +@click.option( + "-sp", + "--scheduler-path", + type=click.Path(), + default="", + help="Path to scheduler json file.", +) +@click.option( + "-wp", + "--wrapper-path", + type=click.Path(), + default="", + help="Path to where file where your wrapper is located. Used when loaded from scheduler json file," + " and the path to your wrapper has changed (such as when loading on a different computer then" + " originally ran from).", +) +@click.option( + "-wn", + "--wrapper-name", + type=str, + default="", + help="Name of the wrapper class to use. Used when loaded from scheduler json file,", +) +@click.option( + "-td", + "--temporary-dir", + is_flag=True, + show_default=True, + default=False, + help="Modify/add to the config file a temporary directory as the experiment_dir that will get deleted after running" + " (useful for testing)." + " This requires your Wrapper to have the ability to take experiment_dir as an argument" + " to ``load_config``. The default ``load_config`` does support this." + " This is also only done for initial run, not for reloading from scheduler json file.", +) +@click.option( + "--rel-to-config/--rel-to-here", # more cli friendly name for config option of rel_to_launch + default=None, + help="Define all path and dir options in your config file relative to where boa is launched from" + " instead of relative to the config file location (the default)" + " ex:" + " given working_dir=path/to/dir" + " if you don't pass --rel-to-here then path/to/dir is defined in terms of where your config file is" + " if you do pass --rel-to-here then path/to/dir is defined in terms of where you launch boa from", +) +def main(config_path, scheduler_path, wrapper_path, wrapper_name, temporary_dir, rel_to_config): + """Run experiment run from config path or scheduler path""" + + if temporary_dir: + with tempfile.TemporaryDirectory() as temp_dir: + experiment_dir = Path(temp_dir) + return run( + config_path, + scheduler_path=scheduler_path, + wrapper_path=wrapper_path, + wrapper_name=wrapper_name, + rel_to_config=rel_to_config, + experiment_dir=experiment_dir, + ) + return run(config_path, scheduler_path=scheduler_path, wrapper_path=wrapper_path, rel_to_config=rel_to_config) + + +def run(config_path, scheduler_path, rel_to_config, wrapper_path=None, wrapper_name=None, experiment_dir=None): + """Run experiment run from config path or scheduler path + + Parameters + ---------- + config_path + Path to configuration YAML file. + scheduler_path + Path to scheduler json file. + wrapper_path + Path to where file where your wrapper is located. Used when loaded from scheduler json file, + and the path to your wrapper has changed (such as when loading on a different computer then + originally ran from). + rel_to_config + Define all path and dir options in your config file relative to to your config file location + or rel_to_here (relative to cli launch) + experiment_dir + experiment output directory to save BOA run to, can only be specified during an initial run + (when passing in a config_path, not a scheduler_path) + + Returns + ------- + Scheduler + """ + config = {} + script_options = {} + if config_path: + config_path = Path(config_path).resolve() + config = load_jsonlike(config_path) + script_options = config.get("script_options", {}) + if rel_to_config is None: + rel_to_config = get_rel_from_script_options(script_options) + if scheduler_path: + scheduler_path = Path(scheduler_path).resolve() + if not config: + sch_jsn = load_jsonlike(scheduler_path) + config = object_from_json(sch_jsn["wrapper"]["config"]) + config_path = object_from_json(sch_jsn["wrapper"]["config_path"]) + script_options = config.get("script_options", {}) + if rel_to_config is None: + rel_to_config = get_rel_from_script_options(script_options) + + if experiment_dir: + experiment_dir = Path(experiment_dir).resolve() + wrapper_path = Path(wrapper_path).resolve() if wrapper_path else None + + if config_path and rel_to_config: + rel_path = config_path.parent + else: + rel_path = os.getcwd() + + options = get_config_options( + experiment_dir=experiment_dir, rel_path=rel_path, script_options=script_options, wrapper_path=wrapper_path + ) + + if wrapper_name: + options["wrapper_name"] = wrapper_name + + with cd_and_cd_back(options["working_dir"]): + if scheduler_path: + scheduler = scheduler_from_json_file(filepath=scheduler_path, wrapper_path=options["wrapper_path"]) + controller = Controller.from_scheduler(scheduler=scheduler, **options) + else: + if options["wrapper_path"] and Path(options["wrapper_path"]).exists(): + options["wrapper"] = options["wrapper_path"] + else: + options["wrapper"] = ScriptWrapper + controller = Controller( + config_path=config_path, + **options, + ) + controller.initialize_scheduler() + + scheduler = controller.run() + return scheduler + + +def get_rel_from_script_options(script_options): + rel_to_config = script_options.get("rel_to_config", None) or not script_options.get("rel_to_launch", None) + if rel_to_config is None: + rel_to_config = ( + fields_dict(BOAScriptOptions)["rel_to_config"].default + or not fields_dict(BOAScriptOptions)["rel_to_launch"].default + ) + return rel_to_config + + +def get_config_options(experiment_dir, rel_path, script_options: dict = None, wrapper_path=None): + script_options = script_options if script_options is not None else {} + wrapper_name = script_options.get("wrapper_name", fields_dict(BOAScriptOptions)["wrapper_name"].default) + append_timestamp = ( + script_options.get("append_timestamp", None) + if script_options.get("append_timestamp", None) is not None + else fields_dict(BOAScriptOptions)["append_timestamp"].default + ) + + wrapper_path = ( + wrapper_path + if wrapper_path is not None + else script_options.get("wrapper_path", fields_dict(BOAScriptOptions)["wrapper_path"].default) + ) + wrapper_path = _prepend_rel_path(rel_path, wrapper_path) if wrapper_path else wrapper_path + + working_dir = script_options.get("working_dir", fields_dict(BOAScriptOptions)["working_dir"].default) + working_dir = _prepend_rel_path(rel_path, working_dir) + + experiment_dir = experiment_dir or script_options.get( + "experiment_dir", fields_dict(BOAScriptOptions)["experiment_dir"].default + ) + experiment_dir = _prepend_rel_path(rel_path, experiment_dir) if experiment_dir else experiment_dir + + if working_dir: + sys.path.append(str(working_dir)) + + return dict( + append_timestamp=append_timestamp, + experiment_dir=experiment_dir, + working_dir=working_dir, + wrapper_name=wrapper_name, + wrapper_path=wrapper_path, + ) + + +def _prepend_rel_path(rel_path, path): + if not path: + return path + path = Path(path) + if not path.is_absolute(): + path = rel_path / path + return path.resolve() + + +if __name__ == "__main__": + main() diff --git a/boa/logger.py b/boa/logger.py index 078e369..7d4d5d7 100644 --- a/boa/logger.py +++ b/boa/logger.py @@ -1,15 +1,12 @@ import logging import logging.config import logging.handlers -import multiprocessing from boa.definitions import PathLike DEFAULT_LOG_LEVEL: int = logging.INFO ROOT_LOGGER_NAME = "boa" -queue = multiprocessing.Manager().Queue() - def get_logger(name: str = ROOT_LOGGER_NAME, level: int = DEFAULT_LOG_LEVEL, filename=None) -> logging.Logger: """Get a logger. diff --git a/boa/runner.py b/boa/runner.py index 7e26547..bbd46d1 100644 --- a/boa/runner.py +++ b/boa/runner.py @@ -9,6 +9,7 @@ import concurrent.futures import logging +import multiprocessing from collections import defaultdict from typing import Any, Dict, Iterable, Set @@ -16,7 +17,7 @@ from ax.core.runner import Runner from ax.core.trial import Trial -from boa.logger import get_logger, queue +from boa.logger import get_logger from boa.metaclasses import RunnerRegister from boa.utils import serialize_init_args from boa.wrappers.base_wrapper import BaseWrapper @@ -28,6 +29,7 @@ class WrappedJobRunner(Runner, metaclass=RunnerRegister): def __init__(self, wrapper: BaseWrapper = None, *args, **kwargs): self.wrapper = wrapper or BaseWrapper() + self.queue = multiprocessing.Manager().Queue() super().__init__(*args, **kwargs) def run(self, trial: Trial) -> Dict[str, Any]: @@ -42,7 +44,7 @@ def run(self, trial: Trial) -> Dict[str, Any]: Returns: Dict of run metadata from the deployment process. """ - qh = logging.handlers.QueueHandler(queue) + qh = logging.handlers.QueueHandler(self.queue) logger = logging.getLogger() logger.addHandler(qh) ax_logger = logging.getLogger("ax") @@ -119,7 +121,7 @@ def to_dict(self) -> dict: parents = self.__class__.mro()[1:] # index 0 is the class itself - properties = serialize_init_args(self, parents=parents, match_private=True, exclude_fields=["wrapper"]) + properties = serialize_init_args(self, parents=parents, match_private=True, exclude_fields=["wrapper", "queue"]) properties["__type"] = self.__class__.__name__ return properties diff --git a/pyproject.toml b/pyproject.toml index 775de7a..f1813f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,10 @@ dynamic = ["version", "dependencies", "optional-dependencies"] "Bug Tracker" = "https://github.com/madeline-scyphers/boa/issues" Documentation = "http://boa-framework.readthedocs.io" +[project.scripts] +boa = "boa.cli:main" +"boa-plot" = "boa.plot:main" + [tool.setuptools.packages.find] include = ["boa*"]