Skip to content

Commit a29e66b

Browse files
authored
Merge branch 'develop' into testing/pixi_extra_tests
2 parents 0e20f2f + 2ad1722 commit a29e66b

12 files changed

Lines changed: 103 additions & 34 deletions

File tree

.github/workflows/basic.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
- name: Checkout lockfile
4747
run: git lfs checkout
4848

49-
- uses: prefix-dev/setup-pixi@v0.9.3
49+
- uses: prefix-dev/setup-pixi@v0.9.4
5050
with:
5151
pixi-version: v0.55.0
5252
frozen: true
@@ -92,4 +92,4 @@ jobs:
9292
runs-on: ubuntu-latest
9393
steps:
9494
- uses: actions/checkout@v6
95-
- uses: crate-ci/typos@v1.42.2
95+
- uses: crate-ci/typos@v1.43.0

.github/workflows/extra.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,4 @@ jobs:
110110
runs-on: ubuntu-latest
111111
steps:
112112
- uses: actions/checkout@v6
113-
- uses: crate-ci/typos@v1.42.2
113+
- uses: crate-ci/typos@v1.43.0

.readthedocs.yml

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,31 @@ build:
44
os: "ubuntu-22.04"
55
tools:
66
python: "3.10"
7+
commands:
8+
# from https://docs.readthedocs.com/platform/stable/build-customization.html#support-git-lfs-large-file-storage
9+
# Download and uncompress the binary
10+
# https://git-lfs.github.com/
11+
- wget https://github.com/git-lfs/git-lfs/releases/download/v3.1.4/git-lfs-linux-amd64-v3.1.4.tar.gz
12+
- tar xvfz git-lfs-linux-amd64-v3.1.4.tar.gz git-lfs
13+
# Modify LFS config paths to point where git-lfs binary was downloaded
14+
- git config filter.lfs.process "`pwd`/git-lfs filter-process"
15+
- git config filter.lfs.smudge "`pwd`/git-lfs smudge -- %f"
16+
- git config filter.lfs.clean "`pwd`/git-lfs clean -- %f"
17+
# Make LFS available in current repository
18+
- ./git-lfs install
19+
# Download content from remote
20+
- ./git-lfs fetch
21+
# Make local files to have the real content on them
22+
- ./git-lfs checkout
23+
- asdf plugin add pixi
24+
- asdf install pixi latest
25+
- asdf global pixi latest
26+
- pixi run -e docs build-docs
27+
- mkdir -p $READTHEDOCS_OUTPUT/html/
28+
- cp -r docs/_build/html/** $READTHEDOCS_OUTPUT/html/
729

830
sphinx:
931
configuration: docs/conf.py
1032

1133
formats:
1234
- pdf
13-
14-
python:
15-
install:
16-
- requirements: docs/requirements.txt
17-
- method: pip
18-
path: .
19-
extra_requirements:
20-
- docs

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class AxParameterWarning(Warning): # Ensure it's a real warning subclass
101101
"sphinxcontrib.autodoc_pydantic",
102102
"sphinx_design",
103103
"sphinx_copybutton",
104+
"sphinx_lfs_content",
104105
]
105106

106107
spelling_word_list_filename = "spelling_wordlist.txt"

docs/requirements.txt

Lines changed: 0 additions & 8 deletions
This file was deleted.

libensemble/executors/executor.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
# To change logging level for just this module
3131
# logger.setLevel(logging.DEBUG)
3232

33+
# Placeholder for container support - replaced with simulation directory at runtime
34+
LIBE_SIM_DIR_PLACEHOLDER = "%LIBENSEMBLE_SIM_DIR%"
35+
3336
STATES = """
3437
UNKNOWN
3538
CREATED
@@ -431,6 +434,7 @@ def __init__(self) -> None:
431434
self.workerID = None
432435
self.comm = None
433436
self.last_task = 0
437+
self.base_dir = os.getcwd()
434438
Executor.executor = self
435439

436440
def __enter__(self):
@@ -522,6 +526,10 @@ def register_app(
522526
523527
precedent: str, Optional
524528
Any str that should directly precede the application full path.
529+
Supports the placeholder ``%LIBENSEMBLE_SIM_DIR%`` which is replaced
530+
at runtime with the simulation directory as a relative path from
531+
where the executor was created. This is useful for container exec
532+
commands.
525533
"""
526534

527535
if not app_name:
@@ -673,10 +681,26 @@ def set_worker_info(self, comm=None, workerid=None) -> None:
673681
self.workerID = workerid
674682
self.comm = comm
675683

676-
def _check_app_exists(self, full_path: str) -> None:
684+
def _check_app_exists(self, app: Application) -> None:
677685
"""Allows submit function to check if app exists and error if not"""
678-
if not os.path.isfile(full_path):
679-
raise ExecutorException(f"Application does not exist {full_path}")
686+
if app.precedent:
687+
# Could be a container call in precedent. In that case,
688+
# the executable is not available on the host system and
689+
# we just forward what the user provided.
690+
return
691+
692+
if not os.path.isfile(app.full_path):
693+
raise ExecutorException(f"Application does not exist {app.full_path}")
694+
695+
def _set_sim_dir_env(self, task: Task, run_cmd: list[str]) -> list[str]:
696+
"""Replace simulation directory placeholder in run command if present.
697+
698+
Supports container-based execution where the simulation directory needs to be
699+
passed to container exec commands (e.g., podman-hpc exec --workdir).
700+
"""
701+
sim_dir = os.path.relpath(task.workdir, self.base_dir)
702+
task._add_to_env("LIBENSEMBLE_SIM_DIR", sim_dir)
703+
return [arg.replace(LIBE_SIM_DIR_PLACEHOLDER, sim_dir) for arg in run_cmd]
680704

681705
def submit(
682706
self,
@@ -745,12 +769,15 @@ def submit(
745769
task = Task(app, app_args, default_workdir, stdout, stderr, self.workerID, dry_run)
746770

747771
if not dry_run:
748-
self._check_app_exists(task.app.full_path)
772+
self._check_app_exists(task.app)
749773

750774
runline = task.app.app_cmd.split()
751775
if task.app_args is not None:
752776
runline.extend(task.app_args.split())
753777

778+
runline = self._set_sim_dir_env(task, runline)
779+
task.runline = " ".join(runline)
780+
754781
if dry_run:
755782
logger.info(f"Test (No submit) Runline: {' '.join(runline)}")
756783
else:

libensemble/executors/mpi_executor.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@ class MPIExecutor(Executor):
7171
7272
from libensemble.executors.mpi_executor import MPIExecutor
7373
exctr = MPIExecutor(custom_info=customizer)
74-
75-
7674
"""
7775

7876
def __init__(self, custom_info: dict = {}) -> None:
@@ -317,7 +315,7 @@ def submit(
317315
task = Task(app, app_args, default_workdir, stdout, stderr, self.workerID, dry_run)
318316

319317
if not dry_run:
320-
self._check_app_exists(task.app.full_path)
318+
self._check_app_exists(task.app)
321319

322320
if stage_inout is not None:
323321
logger.warning("stage_inout option ignored in this " "executor - runs in-place")
@@ -363,7 +361,8 @@ def submit(
363361
if task.app_args is not None:
364362
runline.extend(task.app_args.split())
365363

366-
task.runline = " ".join(runline) # Allow to be queried
364+
runline = self._set_sim_dir_env(task, runline)
365+
task.runline = " ".join(runline)
367366

368367
if env_script is not None:
369368
run_cmd = Executor._process_env_script(task, runline, env_script)

libensemble/tests/unit_tests/test_executor.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,25 @@ def test_non_existent_app_mpi():
936936
assert 0
937937

938938

939+
def test_non_existent_app_precedent():
940+
"""Tests exception on non-existent app is not thrown if precedent is set.
941+
This is common when running apps in containers, where the executable is not
942+
check-able from the host system."""
943+
from libensemble.executors.executor import Executor
944+
945+
print(f"\nTest: {sys._getframe().f_code.co_name}\n")
946+
947+
exctr = Executor()
948+
949+
# Can register a non-existent app in case created as part of workflow.
950+
exctr.register_app(full_path=non_existent_app, app_name="nonexist")
951+
952+
w_exctr = Executor.executor # simulate on worker
953+
954+
# all should be ok
955+
w_exctr.submit(app_name="nonexist", dry_run=True)
956+
957+
939958
def test_man_signal_unrec_tag():
940959
print(f"\nTest: {sys._getframe().f_code.co_name}\n")
941960

@@ -990,5 +1009,6 @@ def test_man_signal_unrec_tag():
9901009
test_dry_run()
9911010
test_non_existent_app()
9921011
test_non_existent_app_mpi()
1012+
test_non_existent_app_precedent()
9931013
test_man_signal_unrec_tag()
9941014
teardown_module(__file__)

libensemble/tests/unit_tests/test_executor_gpus.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@ def run_check(exp_env, exp_cmd, **kwargs):
118118
args_for_sim = "sleep 0"
119119
exp_runline = exp_cmd + " simdir/my_simtask.x sleep 0"
120120
task = exctr.submit(calc_type="sim", app_args=args_for_sim, dry_run=True, **kwargs)
121-
assert task.env == exp_env, f"Task env does not match expected:\n Received: {task.env}\n Expected: {exp_env}"
121+
for key, value in exp_env.items():
122+
assert key in task.env, f"Expected env key '{key}' not found in task.env: {task.env}"
123+
assert task.env[key] == value, f"Env key '{key}' has value '{task.env[key]}', expected '{value}'"
122124
assert (
123125
task.runline == exp_runline
124126
), f"Run line does not match expected.\n Received: {task.runline}\n Expected: {exp_runline}"

libensemble/tools/test_support.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,16 @@ def check_gpu_setting(task, assert_setting=True, print_setting=False, resources=
242242
print(f"Worker {task.workerID}: {desc}GPU setting ({stype}): {gpu_setting} {addon}", flush=True)
243243

244244
if assert_setting:
245-
assert (
246-
gpu_setting == expected
247-
), f"Worker {task.workerID}: Found GPU setting: {gpu_setting}, Expected: {expected}"
245+
if isinstance(expected, dict):
246+
for key, value in expected.items():
247+
assert key in gpu_setting, (
248+
f"Worker {task.workerID}: Expected env key '{key}' not found in GPU setting: {gpu_setting}"
249+
)
250+
assert gpu_setting[key] == value, (
251+
f"Worker {task.workerID}: GPU setting key '{key}' has value '{gpu_setting[key]}', "
252+
f"expected '{value}'"
253+
)
254+
else:
255+
assert (
256+
gpu_setting == expected
257+
), f"Worker {task.workerID}: Found GPU setting: {gpu_setting}, Expected: {expected}"

0 commit comments

Comments
 (0)