Skip to content
Open
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
55 changes: 51 additions & 4 deletions src/aiida_quantumespresso/cli/setup/codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from aiida.cmdline.utils import echo
from aiida.common.exceptions import NotExistent

from aiida_quantumespresso.tools.setup import get_code_label, get_executable_paths
from aiida_quantumespresso.tools.setup import get_code_label, get_executable_paths, VALID_QE_EXECUTABLES

PREPEND_APPEND_TEMPLATE = (
'#==========================================================================\n'
Expand All @@ -19,7 +19,7 @@
@click.argument(
'executables',
nargs=-1,
required=True,
required=False,
)
@click.option(
'--directory',
Expand Down Expand Up @@ -62,6 +62,24 @@
is_flag=True,
help='Open an editor to edit the prepend and append text.',
)
@click.option(
'--all',
'-a',
'setup_all',
is_flag=True,
help='Set up codes for all supported Quantum ESPRESSO executables.',
)
@click.option(
'--ignore-missing',
is_flag=True,
help='Ignore missing executables and do not raise an error.',
)
@click.option(
'--skip-existing',
'-s',
is_flag=True,
help='Skip creating codes if they already exist on the computer.',
)
def setup_codes_cmd(
computer,
executables,
Expand All @@ -70,19 +88,27 @@ def setup_codes_cmd(
prepend_text,
append_text,
interactive,
setup_all,
ignore_missing,
skip_existing,
):
"""Set up codes for Quantum ESPRESSO executables.

Specify the target `orm.Computer` and a single executable or a list of Quantum ESPRESSO executables to
create codes for. You can provide multiple executables separated by a
space, e.g. `pw.x dos.x`.
space, e.g. `pw.x dos.x`. To set up codes for all supported Quantum ESPRESSO executables, use the `--all`/`-a` option.

Use `--directory`/`-d` to point to the directory containing the executables.
If not provided, the command will try to find the executables in
the `PATH` on the (remote) computer. Consider adding a `--prepend-text` if modules
need to be loaded to find the executables. This can also be done with an editor via
the `--interactive` option.
"""
if bool(executables) == setup_all:
if setup_all:
echo.echo_critical('Please provide either executables or use the `--all/-a` option, not both.')
echo.echo_critical('Please provide at least one executable or use the `--all/-a` option to set up all codes.')

if interactive:
prepend_text = click.edit(prepend_text or PREPEND_APPEND_TEMPLATE.format('PREPEND')) or prepend_text
prepend_text = '\n'.join([line for line in prepend_text.splitlines() if not line.strip().startswith('#=')])
Expand All @@ -96,14 +122,35 @@ def setup_codes_cmd(
except NotExistent:
echo.echo_critical(f'Computer<{computer.label}> is not yet configured for user<{user.email}>')

if setup_all:
ignore_missing = True
executables = VALID_QE_EXECUTABLES

try:
executable_path_mapping = get_executable_paths(executables, computer, prepend_text, directory)
executable_path_mapping = get_executable_paths(executables, computer, prepend_text, directory, ignore_missing)
except (FileNotFoundError, ValueError) as exc:
echo.echo_critical(exc)

if ignore_missing:
missing = False
for exe in executables:
if exe not in executable_path_mapping:
echo.echo_warning(
f'Executable<{exe}> not found on Computer<{computer.label}>. Skipping setup for this executable.'
)
missing = True
if missing:
echo.echo('')

for executable, exec_path in executable_path_mapping.items():
existing_label, label = get_code_label(label_template=label_template, executable=executable, computer=computer)
if existing_label:
if skip_existing:
echo.echo_warning(
f'Code<{existing_label}> for {executable} already exists on Computer<{computer.label}>. '
'Skipping setup for this executable.'
)
continue
echo.echo_warning(f'Code with label<{existing_label}> already exists on Computer<{computer.label}>.')
if not click.confirm(f'Do you want to add another instance with label {label}?'):
continue
Expand Down
46 changes: 41 additions & 5 deletions src/aiida_quantumespresso/tools/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,34 @@

from aiida import orm

VALID_QE_EXECUTABLES = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you also forgot open_grid.x here. :)

One issue with this approach is that (besides forgetting to update this list), there might be other codes that are supported in other plugin packages that aren't in this list, but where I think our code setup command would be able to set up the code just fine (e.g. hp.x, possibly others). I suppose we could add those here as well, but then the list for --all becomes even longer and we're adding logic for external packages that we don't depend upon.

I was also going to add epw.x and xspectra.x here as well, but then I realised that their entry points are epw.epw and xspectra.xspectra. So sadly I won't be able to use our beautiful command to set up these codes. 🥲 I'm wondering if it would be worth considering supporting these executables still somehow (e.g. via a mapping), or it's also too messy because they are external non-dependencies.

If we keep this validation, we should adapt the language though: e.g. use "supported" executables instead of "valid". xspectra.x is a perfectly valid QE executable. I'm even a bit worried that for users of Xing's plugin package this won't be confusing: why can't I install xspectra.x with this command?

'bands.x',
'cp.x',
'dos.x',
'matdyn.x',
'neb.x',
'ph.x',
'pp.x',
'projwfc.x',
'pw.x',
'pw2gw.x',
'pw2wannier90.x',
'q2r.x',
]


def validate_executable_names(executables: tuple[str]) -> list[str]:
"""Validate that the provided executable names are recognized Quantum ESPRESSO executables.

:param executables: single executable name or list of executable names
:return: list of invalid executable names, empty list if all are valid
"""
if isinstance(executables, str):
executables = [executables]

invalid_executables = [exe for exe in executables if exe not in VALID_QE_EXECUTABLES]
return invalid_executables


def setup_codes(
computer: orm.Computer,
Expand Down Expand Up @@ -80,12 +108,23 @@ def get_executable_paths(
computer: orm.Computer,
prepend_text: str = '',
directory: Union[str, None] = None,
ignore_missing: bool = False,
) -> dict:
"""Return a mapping from executable names to absolute paths on the given computer.

If `directory` is provided, each path is constructed as `directory`/`executable`.
Otherwise, the `prepend_text` is combined with `which` to locate executables in the PATH.
"""
if directory is not None:
directory = PurePosixPath(directory)

if not directory.is_absolute():
raise ValueError(f'Directory<{directory}> is not an absolute path.')

invalid_executables = validate_executable_names(executable_tuple)
if invalid_executables:
raise ValueError(f'Invalid Quantum ESPRESSO executables: {invalid_executables}')

executable_paths = {}

with computer.get_transport() as transport:
Expand All @@ -106,15 +145,12 @@ def get_executable_paths(
'\nDouble-check the `prepend_text` and executables and/or specify the full path with the '
'`directory` input.'
)
if ignore_missing:
continue
raise FileNotFoundError(msg)

executable_paths[executable] = PurePosixPath(stdout.strip()).as_posix()
else:
directory = PurePosixPath(directory)

if not directory.is_absolute():
raise ValueError(f'Directory<{directory}> is not an absolute path.')

exec_path = (directory / executable).as_posix()

if not transport.path_exists(exec_path):
Expand Down
Loading