diff --git a/smartsim/entity/application.py b/smartsim/entity/application.py index 32fffb6f5..a8302fc1f 100644 --- a/smartsim/entity/application.py +++ b/smartsim/entity/application.py @@ -26,6 +26,7 @@ from __future__ import annotations +import collections import copy import textwrap import typing as t @@ -262,7 +263,7 @@ def _build_exe_args(exe_args: t.Union[str, t.Sequence[str], None]) -> t.List[str if not ( isinstance(exe_args, str) or ( - isinstance(exe_args, list) + isinstance(exe_args, collections.abc.Sequence) and all(isinstance(arg, str) for arg in exe_args) ) ): @@ -271,7 +272,7 @@ def _build_exe_args(exe_args: t.Union[str, t.Sequence[str], None]) -> t.List[str if isinstance(exe_args, str): return exe_args.split() - return exe_args + return list(exe_args) def print_attached_files(self) -> None: """Print a table of the attached files on std out""" diff --git a/smartsim/entity/ensemble.py b/smartsim/entity/ensemble.py index bf35a46a9..f228c4a8a 100644 --- a/smartsim/entity/ensemble.py +++ b/smartsim/entity/ensemble.py @@ -32,7 +32,7 @@ import os.path import typing as t -from smartsim.entity import _mock, entity, strategies +from smartsim.entity import entity, strategies from smartsim.entity.application import Application from smartsim.entity.files import EntityFiles from smartsim.entity.strategies import ParamSet @@ -59,23 +59,183 @@ def __init__( max_permutations: int = -1, replicas: int = 1, ) -> None: + """Initialize an ``Ensemble`` of application instances + + :param name: name of the ensemble + :param exe: executable to run + :param exe_args: executable arguments + :param exe_arg_parameters: parameters and values to be used when configuring entities + :param files: files to be copied, symlinked, and/or configured prior to + execution + :param file_parameters: parameters and values to be used when configuring + files + :param permutation_strategy: strategy to control how the param values are applied to the Ensemble + :param max_permutations: max parameter permutations to set for the ensemble + :param replicas: number of identical entities to create within an Ensemble + """ self.name = name - self.exe = os.fspath(exe) + """The name of the ensemble""" + self._exe = os.fspath(exe) + """The executable to run""" self.exe_args = list(exe_args) if exe_args else [] - self.exe_arg_parameters = ( + """The executable arguments""" + self._exe_arg_parameters = ( copy.deepcopy(exe_arg_parameters) if exe_arg_parameters else {} ) - self.files = copy.deepcopy(files) if files else EntityFiles() - self.file_parameters = dict(file_parameters) if file_parameters else {} - self.permutation_strategy = permutation_strategy - self.max_permutations = max_permutations - self.replicas = replicas + """The parameters and values to be used when configuring entities""" + self._files = copy.deepcopy(files) if files else EntityFiles() + """The files to be copied, symlinked, and/or configured prior to execution""" + self._file_parameters = ( + copy.deepcopy(file_parameters) if file_parameters else {} + ) + """The parameters and values to be used when configuring files""" + self._permutation_strategy = permutation_strategy + """The strategy to control how the param values are applied to the Ensemble""" + self._max_permutations = max_permutations + """The maximum number of entities to come out of the permutation strategy""" + self._replicas = replicas + """How many identical entities to create within an Ensemble""" + + @property + def exe(self) -> str: + """Return executable to run. + + :returns: application executable to run + """ + return self._exe + + @exe.setter + def exe(self, value: str | os.PathLike[str]) -> None: + """Set executable to run. + + :param value: executable to run + """ + self._exe = os.fspath(value) + + @property + def exe_args(self) -> t.List[str]: + """Return a list of attached executable arguments. + + :returns: application executable arguments + """ + return self._exe_args + + @exe_args.setter + def exe_args(self, value: t.Sequence[str]) -> None: + """Set the executable arguments. + + :param value: executable arguments + """ + self._exe_args = list(value) + + @property + def exe_arg_parameters(self) -> t.Mapping[str, t.Sequence[t.Sequence[str]]]: + """Return the executable argument parameters + + :returns: executable arguments parameters + """ + return self._exe_arg_parameters + + @exe_arg_parameters.setter + def exe_arg_parameters( + self, value: t.Mapping[str, t.Sequence[t.Sequence[str]]] + ) -> None: + """Set the executable arguments. + + :param value: executable arguments + """ + self._exe_arg_parameters = copy.deepcopy(value) + + @property + def files(self) -> EntityFiles: + """Return files to be copied, symlinked, and/or configured prior to + execution. + + :returns: files + """ + return self._files + + @files.setter + def files(self, value: EntityFiles) -> None: + """Set files to be copied, symlinked, and/or configured prior to + execution. + + :param value: files + """ + self._files = copy.deepcopy(value) + + @property + def file_parameters(self) -> t.Mapping[str, t.Sequence[str]]: + """Return file parameters. + + :returns: application file parameters + """ + return self._file_parameters + + @file_parameters.setter + def file_parameters(self, value: t.Mapping[str, t.Sequence[str]]) -> None: + """Set the file parameters. + + :param value: file parameters + """ + self._file_parameters = dict(value) + + @property + def permutation_strategy(self) -> str | strategies.PermutationStrategyType: + """Return the permutation strategy + + :return: permutation strategy + """ + return self._permutation_strategy + + @permutation_strategy.setter + def permutation_strategy( + self, value: str | strategies.PermutationStrategyType + ) -> None: + """Set the permutation strategy + + :param value: permutation strategy + """ + self._permutation_strategy = value + + @property + def max_permutations(self) -> int: + """Return the maximum permutations + + :return: max permutations + """ + return self._max_permutations + + @max_permutations.setter + def max_permutations(self, value: int) -> None: + """Set the maximum permutations + + :param value: the maxpermutations + """ + self._max_permutations = value + + @property + def replicas(self) -> int: + """Return the number of replicas + + :return: number of replicas + """ + return self._replicas + + @replicas.setter + def replicas(self, value: int) -> None: + """Set the number of replicas + + :return: the number of replicas + """ + self._replicas = value def _create_applications(self) -> tuple[Application, ...]: """Concretize the ensemble attributes into a collection of application instances. """ permutation_strategy = strategies.resolve(self.permutation_strategy) + combinations = permutation_strategy( self.file_parameters, self.exe_arg_parameters, self.max_permutations ) diff --git a/tests/test_ensemble.py b/tests/test_ensemble.py index c22e0e0db..5198681fe 100644 --- a/tests/test_ensemble.py +++ b/tests/test_ensemble.py @@ -26,11 +26,13 @@ import itertools import typing as t +from glob import glob +from os import path as osp import pytest -from smartsim.entity import _mock from smartsim.entity.ensemble import Ensemble +from smartsim.entity.files import EntityFiles from smartsim.entity.strategies import ParamSet from smartsim.settings.launchSettings import LaunchSettings @@ -40,6 +42,54 @@ _2x2_EXE_ARG = {"EXE": [["a"], ["b", "c"]], "ARGS": [["d"], ["e", "f"]]} +@pytest.fixture +def get_gen_configure_dir(fileutils): + yield fileutils.get_test_conf_path(osp.join("generator_files", "tag_dir_template")) + + +def test_exe_property(): + e = Ensemble(name="test", exe="path/to/example_simulation_program") + exe = e.exe + assert exe == e.exe + + +def test_exe_args_property(): + e = Ensemble("test", exe="path/to/example_simulation_program", exe_args="sleepy.py") + exe_args = e.exe_args + assert exe_args == e.exe_args + + +def test_exe_arg_parameters_property(): + exe_arg_parameters = {"-N": 2} + e = Ensemble( + "test", + exe="path/to/example_simulation_program", + exe_arg_parameters=exe_arg_parameters, + ) + exe_arg_parameters = e.exe_arg_parameters + assert exe_arg_parameters == e.exe_arg_parameters + + +def test_files_property(get_gen_configure_dir): + tagged_files = sorted(glob(get_gen_configure_dir + "/*")) + files = EntityFiles(tagged=tagged_files) + e = Ensemble("test", exe="path/to/example_simulation_program", files=files) + files = e.files + assert files == e.files + + +def test_file_parameters_property(): + file_parameters = {"h": [5, 6, 7, 8]} + e = Ensemble( + "test", + exe="path/to/example_simulation_program", + file_parameters=file_parameters, + ) + file_parameters = e.file_parameters + + assert file_parameters == e.file_parameters + + def user_created_function( file_params: t.Mapping[str, t.Sequence[str]], exe_arg_params: t.Mapping[str, t.Sequence[t.Sequence[str]]],