Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 125 additions & 8 deletions src/cubitpy/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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: "<username>"\n'
' host: "<hostname_or_ip>"\n'
' cubit_path: "<remote_cubit_install_path>"\n'
"\n"
"local_config:\n"
' cubit_path: "<local_cubit_install_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")
Expand All @@ -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":
Expand All @@ -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)
Expand Down Expand Up @@ -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()
112 changes: 59 additions & 53 deletions src/cubitpy/cubit_wrapper/cubit_wrapper_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 14 additions & 8 deletions src/cubitpy/cubitpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import subprocess # nosec B404
import time
import warnings
from pathlib import Path

import netCDF4
from fourcipp.fourc_input import FourCInput
Expand Down Expand Up @@ -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")
Expand Down