diff --git a/src/cubitpy/conf.py b/src/cubitpy/conf.py index f018e95..71af0c0 100644 --- a/src/cubitpy/conf.py +++ b/src/cubitpy/conf.py @@ -26,8 +26,12 @@ import glob import os import shutil +import warnings +from pathlib import Path from sys import platform +import yaml + from cubitpy.cubitpy_types import ( BoundaryConditionType, CubitItems, @@ -53,6 +57,8 @@ def get_path(environment_variable, test_function, *, throw_error=True): class CubitOptions(object): """Object for types in cubitpy.""" + _config = None + def __init__(self): # Temporary directory for cubitpy. self.temp_dir = os.path.join( @@ -83,14 +89,115 @@ def __init__(self): self.eps_pos = 1e-10 @staticmethod - def get_cubit_root_path(**kwargs): - """Get Path to cubit root directory.""" - return get_path("CUBIT_ROOT", os.path.isdir, **kwargs) + def get_cubit_config_filepath(): + """Return path to remote config if it exists, else None.""" + return get_path("CUBITPY_CONFIG_PATH", os.path.isfile, throw_error=False) + + @classmethod + def validate_cubit_config(cls): + """Validate the already loaded config dict and raise helpful errors.""" + + config = cls._config + if config is None: + raise RuntimeError( + "Config not loaded yet. Call cupy.get_cubit_config(...) first." + ) + + TEMPLATE = ( + "\n\nCorrect YAML structure:\n" + "----------------------------------------\n" + 'cubitpy_mode: "remote" # or "local"\n' + "\n" + "remote_config:\n" + ' user: ""\n' + ' host: ""\n' + ' cubit_path: ""\n' + "\n" + "local_config:\n" + ' cubit_path: ""\n' + "----------------------------------------\n" + "- If mode = 'remote': remote_config MUST exist and contain user, host, cubit_path.\n" + "- If mode = 'local' : local_config MUST exist and contain cubit_path.\n" + "- The unused section may be omitted.\n" + "----------------------------------------\n" + ) + + def fail(msg: str): + """Helper to raise a RuntimeError with template.""" + raise RuntimeError(msg + TEMPLATE) + + # Check mode + if "cubitpy_mode" not in config: + fail("Missing required key: 'cubitpy_mode'.") + + mode = config["cubitpy_mode"] + if mode not in ("remote", "local"): + fail(f"Invalid cubitpy_mode '{mode}'. Expected 'remote' or 'local'.") + + if mode == "remote": + if "remote_config" not in config: + fail("cubitpy_mode='remote' requires a 'remote_config' section.") + + remote_config = config["remote_config"] + required = ["user", "host", "cubit_path"] + missing = [ + k for k in required if k not in remote_config or not remote_config[k] + ] + if missing: + fail("remote_config is missing required fields: " + ", ".join(missing)) + + if mode == "local": + if "local_config" not in config: + fail("cubitpy_mode='local' requires a 'local_config' section.") + + local_config = config["local_config"] + if "cubit_path" not in local_config or not local_config["cubit_path"]: + fail("local_config must contain a non-empty 'cubit_path'.") + + local_cubit_path = local_config["cubit_path"] + if not Path(local_cubit_path).expanduser().exists(): + raise FileNotFoundError( + f"local_config.cubit_path '{local_cubit_path}' does not exist." + ) + + @classmethod + def load_cubit_config(cls, config_path: Path | None = None): + """Read the CubitPy YAML config.""" + + if config_path is None: + config_path = cls.get_cubit_config_filepath() + + if not config_path: + warnings.warn( + "CubitPy configuration file not found." "Using default config: local", + DeprecationWarning, + ) + root_path = get_path("CUBIT_ROOT", os.path.isdir, throw_error=True) + + default_config = { + "cubitpy_mode": "local", + "local_config": {"cubit_path": root_path}, + "remote_config": {}, + } + + cubit_config_dict = default_config + else: + try: + with open(config_path, "r") as f: + cubit_config_dict = yaml.safe_load(f) + except Exception as e: + raise ImportError(f"Failed to read YAML at '{config_path}': {e}") + + if not isinstance(cubit_config_dict, dict): + raise ImportError("YAML top level must be a mapping (dict).") + + cls._config = cubit_config_dict + cls.validate_cubit_config() @classmethod def get_cubit_exe_path(cls, **kwargs): """Get Path to cubit executable.""" - cubit_root = cls.get_cubit_root_path(**kwargs) + cubit_root = cls._config["local_config"]["cubit_path"] if platform == "linux" or platform == "linux2": if cupy.is_coreform(): return os.path.join(cubit_root, "bin", "coreform_cubit") @@ -108,7 +215,7 @@ def get_cubit_exe_path(cls, **kwargs): @classmethod def get_cubit_lib_path(cls, **kwargs): """Get Path to cubit lib directory.""" - cubit_root = cls.get_cubit_root_path(**kwargs) + cubit_root = cls._config["local_config"]["cubit_path"] if platform == "linux" or platform == "linux2": return os.path.join(cubit_root, "bin") elif platform == "darwin": @@ -122,7 +229,7 @@ def get_cubit_lib_path(cls, **kwargs): @classmethod def get_cubit_interpreter(cls): """Get the path to the python interpreter to be used for CubitPy.""" - cubit_root = cls.get_cubit_root_path() + cubit_root = cls._config["local_config"]["cubit_path"] if cls.is_coreform(): pattern = "**/python3" full_pattern = os.path.join(cubit_root, pattern) @@ -153,12 +260,22 @@ def get_cubit_interpreter(cls): @classmethod def is_coreform(cls): """Return if the given path is a path to cubit coreform.""" - cubit_root = cls.get_cubit_root_path() - if "15.2" in cubit_root: + cubit_root = cls._config["local_config"]["cubit_path"] + if "15.2" in cubit_root and not cls.is_remote(): return False else: return True + @classmethod + def is_remote(cls) -> bool: + """Return True if cubit is running remotely based on the loaded + config.""" + if cls._config is None: + raise RuntimeError( + "Config not loaded yet. Call load_cubit_config() first use of is_remote." + ) + return cls._config.get("cubitpy_mode") == "remote" + # Global object with options for cubitpy. cupy = CubitOptions() diff --git a/src/cubitpy/cubit_wrapper/cubit_wrapper_host.py b/src/cubitpy/cubit_wrapper/cubit_wrapper_host.py index adc1a88..d680da6 100644 --- a/src/cubitpy/cubit_wrapper/cubit_wrapper_host.py +++ b/src/cubitpy/cubit_wrapper/cubit_wrapper_host.py @@ -64,67 +64,73 @@ def __init__( Python interpreter to be used for running cubit. """ - if interpreter is None: - interpreter = f"popen//python={cupy.get_cubit_interpreter()}" + # Remote mode – run cubit on a remote machine via SSH + if cupy.is_remote(): + raise NotImplementedError("Remote cubit mode is not yet implemented.") - if cubit_lib is None: - cubit_lib = cupy.get_cubit_lib_path() + # Local mode – run cubit on the local machine + else: + if interpreter is None: + interpreter = f"popen//python={cupy.get_cubit_interpreter()}" - # Set up the client python interpreter - self.gw = execnet.makegateway(interpreter) - self.gw.reconfigure(py3str_as_py2str=True) + if cubit_lib is None: + cubit_lib = cupy.get_cubit_lib_path() - # Load the main code in the client python interpreter - client_python_file = os.path.join( - os.path.dirname(__file__), "cubit_wrapper_client.py" - ) - with open(client_python_file, "r") as myfile: - data = myfile.read() - - # Set up the connection channel - self.channel = self.gw.remote_exec(data) - - # Send parameters to the client interpreter - parameters = {} - parameters["__file__"] = __file__ - parameters["cubit_lib_path"] = cubit_lib - - # Arguments for cubit - if cubit_args is None: - arguments = [ - "cubit", - # "-log", # Write the log to a file - # "dev/null", - "-information", # Do not output information of cubit - "Off", - "-nojournal", # Do write a journal file - "-noecho", # Do not output commands used in cubit - ] - else: - arguments = ["cubit"] + cubit_args + # Set up the client python interpreter + self.gw = execnet.makegateway(interpreter) + self.gw.reconfigure(py3str_as_py2str=True) - # Check if a log file was given in the cubit arguments - for arg in arguments: - if arg.startswith("-log="): - log_given = True - break - else: - log_given = False + # Load the main code in the client python interpreter + client_python_file = os.path.join( + os.path.dirname(__file__), "cubit_wrapper_client.py" + ) + with open(client_python_file, "r") as myfile: + data = myfile.read() + + # Set up the connection channel + self.channel = self.gw.remote_exec(data) + + # Send parameters to the client interpreter + parameters = {} + parameters["__file__"] = __file__ + parameters["cubit_lib_path"] = cubit_lib + + # Arguments for cubit + if cubit_args is None: + arguments = [ + "cubit", + # "-log", # Write the log to a file + # "dev/null", + "-information", # Do not output information of cubit + "Off", + "-nojournal", # Do write a journal file + "-noecho", # Do not output commands used in cubit + ] + else: + arguments = ["cubit"] + cubit_args + + # Check if a log file was given in the cubit arguments + for arg in arguments: + if arg.startswith("-log="): + log_given = True + break + else: + log_given = False - self.log_check = False + self.log_check = False - if not log_given: - # Write the log to a temporary file and check the contents after each call to cubit - arguments.extend(["-log", cupy.temp_log]) - parameters["tty"] = cupy.temp_log - self.log_check = True + if not log_given: + # Write the log to a temporary file and check the contents after each call to cubit + arguments.extend(["-log", cupy.temp_log]) + parameters["tty"] = cupy.temp_log + self.log_check = True - # Send the parameters to the client interpreter - self.send_and_return(parameters) + # Send the parameters to the client interpreter + self.send_and_return(parameters) - # Initialize cubit in the client and create the linking object here - cubit_id = self.send_and_return(["init", arguments]) - self.cubit = CubitObjectMain(self, cubit_id) + # Initialize cubit in the client and create the linking object here + cubit_id = self.send_and_return(["init", arguments]) + self.cubit = CubitObjectMain(self, cubit_id) def cleanup_execnet_gateway(): """We need to register a function called at interpreter shutdown diff --git a/src/cubitpy/cubitpy.py b/src/cubitpy/cubitpy.py index abf1a74..5697419 100644 --- a/src/cubitpy/cubitpy.py +++ b/src/cubitpy/cubitpy.py @@ -25,6 +25,7 @@ import subprocess # nosec B404 import time import warnings +from pathlib import Path import netCDF4 from fourcipp.fourc_input import FourCInput @@ -62,26 +63,31 @@ def _get_and_check_ids(name, container, id_list, given_id): class CubitPy(object): """A wrapper class with additional functionality for cubit.""" - def __init__(self, *, cubit_exe=None, **kwargs): + def __init__(self, *, cubit_config_path: Path | None = None, **kwargs): """Initialize CubitPy. Args ---- - cubit_exe: str - Path to the cubit executable + cubit_config_path: Path + Path to the cubitpy configuration file. kwargs: Arguments passed on to the creation of the python wrapper """ - - # Set paths - if cubit_exe is None: - cubit_exe = cupy.get_cubit_exe_path() - self.cubit_exe = cubit_exe + # load config + cupy.load_cubit_config(cubit_config_path) # Set the "real" cubit object self.cubit = CubitConnect(**kwargs).cubit + # Set remote paths + if cupy.is_remote(): + raise NotImplementedError( + "Remote cubit connections are not yet supported in CubitPy." + ) + else: + self.cubit_exe = cupy.get_cubit_exe_path() + # Reset cubit self.cubit.cmd("reset") self.cubit.cmd("set geometry engine acis")