import functools import inspect import os import sys import importlib import click class VerosSetting(click.ParamType): name = "setting" current_key = None def convert(self, value, param, ctx): from veros.settings import SETTINGS assert param.nargs == 2 if self.current_key is None: if value not in SETTINGS: self.fail(f"Unknown setting {value}") self.current_key = value return value assert self.current_key in SETTINGS setting = SETTINGS[self.current_key] self.current_key = None if setting.type is bool: return click.BOOL(value) return setting.type(value) def _import_from_file(path): module = os.path.basename(path).split(".py")[0] spec = importlib.util.spec_from_file_location(module, path) mod = importlib.util.module_from_spec(spec) sys.modules[spec.name] = mod spec.loader.exec_module(mod) return mod def run(setup_file, *args, **kwargs): """Runs a Veros setup from given file""" from veros import runtime_settings, VerosSetup, __version__ as veros_version kwargs["override"] = dict(kwargs["override"]) runtime_setting_kwargs = ( "backend", "profile_mode", "num_proc", "loglevel", "device", "float_type", "diskless_mode", "force_overwrite", ) for setting in runtime_setting_kwargs: setattr(runtime_settings, setting, kwargs.pop(setting)) runtime_settings.setup_file = setup_file # determine setup class from given Python file setup_module = _import_from_file(setup_file) SetupClass = None for obj in vars(setup_module).values(): if inspect.isclass(obj) and issubclass(obj, VerosSetup) and obj is not VerosSetup: if SetupClass is not None and SetupClass is not obj: raise RuntimeError("Veros setups can only define one VerosSetup class") SetupClass = obj from veros import logger target_version = getattr(setup_module, "__VEROS_VERSION__", None) if target_version and target_version != veros_version: logger.warning( f"This is Veros v{veros_version}, but the given setup was generated with v{target_version}. " "Consider switching to this version of Veros or updating your setup file.\n" ) sim = SetupClass(*args, **kwargs) sim.setup() sim.run() @click.command("veros-run") @click.argument("SETUP_FILE", type=click.Path(readable=True, dir_okay=False, resolve_path=True, exists=True)) @click.option( "-b", "--backend", default="numpy", type=click.Choice(["numpy", "jax"]), help="Backend to use for computations", show_default=True, ) @click.option( "--device", default="cpu", type=click.Choice(["cpu", "gpu"]), help="Hardware device to use (JAX backend only)", show_default=True, ) @click.option( "-v", "--loglevel", default="info", type=click.Choice(["trace", "debug", "info", "warning", "error"]), help="Log level used for output", show_default=True, ) @click.option( "-s", "--override", nargs=2, multiple=True, metavar="SETTING VALUE", type=VerosSetting(), default=tuple(), help="Override model setting, may be specified multiple times", ) @click.option( "-p", "--profile-mode", is_flag=True, default=False, type=click.BOOL, envvar="VEROS_PROFILE", help="Write a performance profile for debugging", show_default=True, ) @click.option("--force-overwrite", is_flag=True, help="Silently overwrite existing outputs") @click.option("--diskless-mode", is_flag=True, help="Supress all output to disk") @click.option( "--float-type", default="float64", type=click.Choice(["float64", "float32"]), help="Floating point precision to use", show_default=True, ) @click.option( "-n", "--num-proc", nargs=2, default=[1, 1], type=click.INT, help="Number of processes in x and y dimension" ) @functools.wraps(run) def cli(setup_file, *args, **kwargs): if not setup_file.endswith(".py"): raise click.UsageError(f"The given setup file {setup_file} does not appear to be a Python file.") return run(setup_file, *args, **kwargs)