From 344a946055f5c09735e2f030fcb8df16a2662d0c Mon Sep 17 00:00:00 2001 From: Amanda Richardson Date: Thu, 19 Sep 2024 12:34:07 -0500 Subject: [PATCH 1/5] init push --- smartsim/{entity => _core/builders}/ensemble.py | 2 +- smartsim/entity/__init__.py | 2 +- smartsim/entity/entity.py | 4 ++-- tests/_legacy/test_controller.py | 2 +- tests/_legacy/test_controller_errors.py | 2 +- tests/_legacy/test_output_files.py | 2 +- tests/_legacy/test_symlinking.py | 2 +- tests/test_ensemble.py | 14 +++++++------- tests/test_generator.py | 12 ++++++------ 9 files changed, 21 insertions(+), 21 deletions(-) rename smartsim/{entity => _core/builders}/ensemble.py (99%) diff --git a/smartsim/entity/ensemble.py b/smartsim/_core/builders/ensemble.py similarity index 99% rename from smartsim/entity/ensemble.py rename to smartsim/_core/builders/ensemble.py index 261f22d65..672cfb7f7 100644 --- a/smartsim/entity/ensemble.py +++ b/smartsim/_core/builders/ensemble.py @@ -254,7 +254,7 @@ def _create_applications(self) -> tuple[Application, ...]: for i, permutation in enumerate(permutations_) ) - def as_jobs(self, settings: LaunchSettings) -> tuple[Job, ...]: + def build_jobs(self, settings: LaunchSettings) -> tuple[Job, ...]: apps = self._create_applications() if not apps: raise ValueError("There are no members as part of this ensemble") diff --git a/smartsim/entity/__init__.py b/smartsim/entity/__init__.py index 7ffa290b2..eca9d5143 100644 --- a/smartsim/entity/__init__.py +++ b/smartsim/entity/__init__.py @@ -27,7 +27,7 @@ from .application import Application from .dbnode import FSNode from .dbobject import * -from .ensemble import Ensemble +from .._core.builders.ensemble import Ensemble from .entity import SmartSimEntity, TelemetryConfiguration from .entityList import EntityList, EntitySequence from .files import TaggedFilesHierarchy diff --git a/smartsim/entity/entity.py b/smartsim/entity/entity.py index f3e5b17f3..3f5a9eabd 100644 --- a/smartsim/entity/entity.py +++ b/smartsim/entity/entity.py @@ -135,6 +135,6 @@ class CompoundEntity(abc.ABC): """ @abc.abstractmethod - def as_jobs(self, settings: LaunchSettings) -> t.Collection[Job]: ... + def build_jobs(self, settings: LaunchSettings) -> t.Collection[Job]: ... def as_job_group(self, settings: LaunchSettings) -> JobGroup: - return JobGroup(list(self.as_jobs(settings))) + return JobGroup(list(self.build_jobs(settings))) diff --git a/tests/_legacy/test_controller.py b/tests/_legacy/test_controller.py index 19325c933..29d4efe39 100644 --- a/tests/_legacy/test_controller.py +++ b/tests/_legacy/test_controller.py @@ -31,7 +31,7 @@ from smartsim._core.control.controller import Controller from smartsim._core.launcher.step import Step from smartsim.database.orchestrator import FeatureStore -from smartsim.entity.ensemble import Ensemble +from smartsim._core.builders.ensemble import Ensemble from smartsim.settings.slurmSettings import SbatchSettings, SrunSettings controller = Controller() diff --git a/tests/_legacy/test_controller_errors.py b/tests/_legacy/test_controller_errors.py index 4814ce495..13e3c7a7e 100644 --- a/tests/_legacy/test_controller_errors.py +++ b/tests/_legacy/test_controller_errors.py @@ -32,7 +32,7 @@ from smartsim._core.launcher.step.dragon_step import DragonStep from smartsim.database import FeatureStore from smartsim.entity import Application -from smartsim.entity.ensemble import Ensemble +from smartsim._core.builders.ensemble import Ensemble from smartsim.error import SmartSimError, SSUnsupportedError from smartsim.error.errors import SSUnsupportedError from smartsim.settings import RunSettings, SrunSettings diff --git a/tests/_legacy/test_output_files.py b/tests/_legacy/test_output_files.py index 713001feb..1db7fc6f8 100644 --- a/tests/_legacy/test_output_files.py +++ b/tests/_legacy/test_output_files.py @@ -35,7 +35,7 @@ from smartsim._core.launcher.step import Step from smartsim.database.orchestrator import FeatureStore from smartsim.entity.application import Application -from smartsim.entity.ensemble import Ensemble +from smartsim._core.builders.ensemble import Ensemble from smartsim.settings.base import RunSettings from smartsim.settings.slurmSettings import SbatchSettings, SrunSettings diff --git a/tests/_legacy/test_symlinking.py b/tests/_legacy/test_symlinking.py index 4447a49d1..2b6095fff 100644 --- a/tests/_legacy/test_symlinking.py +++ b/tests/_legacy/test_symlinking.py @@ -34,7 +34,7 @@ from smartsim._core.control.controller import Controller, _AnonymousBatchJob from smartsim.database.orchestrator import FeatureStore from smartsim.entity.application import Application -from smartsim.entity.ensemble import Ensemble +from smartsim._core.builders.ensemble import Ensemble from smartsim.settings.base import RunSettings from smartsim.settings.slurmSettings import SbatchSettings, SrunSettings diff --git a/tests/test_ensemble.py b/tests/test_ensemble.py index 3f0840711..cc3b37f9b 100644 --- a/tests/test_ensemble.py +++ b/tests/test_ensemble.py @@ -31,7 +31,7 @@ import pytest -from smartsim.entity.ensemble import Ensemble +from smartsim._core.builders.ensemble import Ensemble from smartsim.entity.files import EntityFiles from smartsim.entity.strategies import ParamSet from smartsim.settings.launch_settings import LaunchSettings @@ -109,7 +109,7 @@ def test_ensemble_user_created_strategy(mock_launcher_settings, test_dir): "echo", ("hello", "world"), permutation_strategy=user_created_function, - ).as_jobs(mock_launcher_settings) + ).build_jobs(mock_launcher_settings) assert len(jobs) == 1 @@ -125,7 +125,7 @@ def test_ensemble_without_any_members_raises_when_cast_to_jobs( permutation_strategy="random", max_permutations=30, replicas=0, - ).as_jobs(mock_launcher_settings) + ).build_jobs(mock_launcher_settings) def test_strategy_error_raised_if_a_strategy_that_dne_is_requested(test_dir): @@ -208,7 +208,7 @@ def test_all_perm_strategy( permutation_strategy="all_perm", max_permutations=max_perms, replicas=replicas, - ).as_jobs(mock_launcher_settings) + ).build_jobs(mock_launcher_settings) assert len(jobs) == expected_num_jobs @@ -222,7 +222,7 @@ def test_all_perm_strategy_contents(): permutation_strategy="all_perm", max_permutations=16, replicas=1, - ).as_jobs(mock_launcher_settings) + ).build_jobs(mock_launcher_settings) assert len(jobs) == 16 @@ -262,7 +262,7 @@ def test_step_strategy( permutation_strategy="step", max_permutations=max_perms, replicas=replicas, - ).as_jobs(mock_launcher_settings) + ).build_jobs(mock_launcher_settings) assert len(jobs) == expected_num_jobs @@ -301,5 +301,5 @@ def test_random_strategy( permutation_strategy="random", max_permutations=max_perms, replicas=replicas, - ).as_jobs(mock_launcher_settings) + ).build_jobs(mock_launcher_settings) assert len(jobs) == expected_num_jobs diff --git a/tests/test_generator.py b/tests/test_generator.py index ff24018ca..b29c9ca97 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -226,7 +226,7 @@ def test_exp_private_generate_method_ensemble(test_dir, wlmutils, generator_inst """Test that Job directory was created from Experiment.""" ensemble = Ensemble("ensemble-name", "echo", replicas=2) launch_settings = LaunchSettings(wlmutils.get_test_launcher()) - job_list = ensemble.as_jobs(launch_settings) + job_list = ensemble.build_jobs(launch_settings) exp = Experiment(name="exp_name", exp_path=test_dir) for i, job in enumerate(job_list): job_run_path, _, _ = exp._generate(generator_instance, job, i) @@ -239,7 +239,7 @@ def test_exp_private_generate_method_ensemble(test_dir, wlmutils, generator_inst def test_generate_ensemble_directory(wlmutils, generator_instance): ensemble = Ensemble("ensemble-name", "echo", replicas=2) launch_settings = LaunchSettings(wlmutils.get_test_launcher()) - job_list = ensemble.as_jobs(launch_settings) + job_list = ensemble.build_jobs(launch_settings) for i, job in enumerate(job_list): # Call Generator.generate_job path, _, _ = generator_instance.generate_job(job, i) @@ -263,7 +263,7 @@ def test_generate_ensemble_directory_start(test_dir, wlmutils, monkeypatch): ) ensemble = Ensemble("ensemble-name", "echo", replicas=2) launch_settings = LaunchSettings(wlmutils.get_test_launcher()) - job_list = ensemble.as_jobs(launch_settings) + job_list = ensemble.build_jobs(launch_settings) exp = Experiment(name="exp_name", exp_path=test_dir) exp.start(*job_list) run_dir = listdir(test_dir) @@ -285,7 +285,7 @@ def test_generate_ensemble_copy(test_dir, wlmutils, monkeypatch, get_gen_copy_di "ensemble-name", "echo", replicas=2, files=EntityFiles(copy=get_gen_copy_dir) ) launch_settings = LaunchSettings(wlmutils.get_test_launcher()) - job_list = ensemble.as_jobs(launch_settings) + job_list = ensemble.build_jobs(launch_settings) exp = Experiment(name="exp_name", exp_path=test_dir) exp.start(*job_list) run_dir = listdir(test_dir) @@ -310,7 +310,7 @@ def test_generate_ensemble_symlink( files=EntityFiles(symlink=get_gen_symlink_dir), ) launch_settings = LaunchSettings(wlmutils.get_test_launcher()) - job_list = ensemble.as_jobs(launch_settings) + job_list = ensemble.build_jobs(launch_settings) exp = Experiment(name="exp_name", exp_path=test_dir) exp.start(*job_list) run_dir = listdir(test_dir) @@ -341,7 +341,7 @@ def test_generate_ensemble_configure( file_parameters=params, ) launch_settings = LaunchSettings(wlmutils.get_test_launcher()) - job_list = ensemble.as_jobs(launch_settings) + job_list = ensemble.build_jobs(launch_settings) exp = Experiment(name="exp_name", exp_path=test_dir) id = exp.start(*job_list) run_dir = listdir(test_dir) From 380dbb2daff580da4c63309c624709500af21942 Mon Sep 17 00:00:00 2001 From: Amanda Richardson Date: Fri, 20 Sep 2024 14:34:12 -0500 Subject: [PATCH 2/5] styling --- smartsim/builders/ensemble.py | 348 ++++++++++++++++++++++++ smartsim/entity/__init__.py | 2 +- tests/_legacy/test_controller.py | 2 +- tests/_legacy/test_controller_errors.py | 2 +- tests/_legacy/test_output_files.py | 2 +- tests/_legacy/test_symlinking.py | 2 +- 6 files changed, 353 insertions(+), 5 deletions(-) create mode 100644 smartsim/builders/ensemble.py diff --git a/smartsim/builders/ensemble.py b/smartsim/builders/ensemble.py new file mode 100644 index 000000000..48220187b --- /dev/null +++ b/smartsim/builders/ensemble.py @@ -0,0 +1,348 @@ +# BSD 2-Clause License +# +# Copyright (c) 2021-2024, Hewlett Packard Enterprise +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from __future__ import annotations + +import copy +import itertools +import os +import os.path +import typing as t + +from smartsim.entity import entity, strategies +from smartsim.entity.application import Application +from smartsim.entity.files import EntityFiles +from smartsim.entity.strategies import ParamSet +from smartsim.launchable.job import Job + +if t.TYPE_CHECKING: + from smartsim.settings.launch_settings import LaunchSettings + + +class Ensemble(entity.CompoundEntity): + """An Ensemble is a builder class that parameterizes the creation of multiple + Applications. Ensembles can be customized by using specific combinations of the + input settings to align with one of the following creation strategies: parameter + expansion and replica creation. + """ + + def __init__( + self, + name: str, + exe: str | os.PathLike[str], + exe_args: t.Sequence[str] | None = None, + exe_arg_parameters: t.Mapping[str, t.Sequence[t.Sequence[str]]] | None = None, + files: EntityFiles | None = None, + file_parameters: t.Mapping[str, t.Sequence[str]] | None = None, + permutation_strategy: str | strategies.PermutationStrategyType = "all_perm", + max_permutations: int = -1, + replicas: int = 1, + ) -> None: + """Initialize an ``Ensemble`` of Application instances + + An Ensemble can be tailored to align with one of the following + creation strategies: parameter expansion or replicas. + + **Parameter Expansion** + + Parameter expansion allows users to assign different parameter values to + multiple applications. This is done by specifying input to `Ensemble.file_parameters`, + `Ensemble.exe_arg_parameters` and `Ensemble.permutation_strategy`. The `permutation_strategy` + accepts three options: + 1. "all_perm": Generates all possible parameter permutations for exhaustive exploration. + 2. "step": Collects identically indexed values across parameter lists to create parameter sets. + 3. "random": Enables random selection from predefined parameter spaces. + + The example below demonstrates creating an Ensemble via parameter expansion, resulting in + the creation of two Applications: + .. highlight:: python + .. code-block:: python + + file_params={"SPAM": ["a", "b"], "EGGS": ["c", "d"]} + exe_arg_parameters = {"EXE": [["a"], ["b", "c"]], "ARGS": [["d"], ["e", "f"]]} + ensemble = Ensemble(name="name",exe="python",exe_arg_parameters=exe_arg_parameters, + file_parameters=file_params,permutation_strategy="step") + + This configuration will yield the following permutations: + + .. highlight:: python + .. code-block:: python + [ParamSet(params={'SPAM': 'a', 'EGGS': 'c'}, exe_args={'EXE': ['a'], 'ARGS': ['d']}), + ParamSet(params={'SPAM': 'b', 'EGGS': 'd'}, exe_args={'EXE': ['b', 'c'], 'ARGS': ['e', 'f']})] + + Each ParamSet contains the parameters assigned from file_params and the corresponding executable + arguments from exe_arg_parameters. + + **Replication** + The replication strategy involves creating identical Applications within an Ensemble. + This is achieved by specifying the `replicas` argument in the Ensemble. + + For example, by applying the `replicas` argument to the previous parameter expansion + example, we can double our Application output: + + .. highlight:: python + .. code-block:: python + + file_params={"SPAM": ["a", "b"], "EGGS": ["c", "d"]} + exe_arg_parameters = {"EXE": [["a"], ["b", "c"]], "ARGS": [["d"], ["e", "f"]]} + ensemble = Ensemble(name="name",exe="python",exe_arg_parameters=exe_arg_parameters, + file_parameters=file_params,permutation_strategy="step", replicas=2) + + This configuration will result in each ParamSet being replicated, effectively doubling + the number of applications created. + + :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 + """The name of the ensemble""" + self._exe = os.fspath(exe) + """The executable to run""" + self.exe_args = list(exe_args) if exe_args else [] + """The executable arguments""" + self._exe_arg_parameters = ( + copy.deepcopy(exe_arg_parameters) if exe_arg_parameters else {} + ) + """The parameters and values to be used when configuring entities""" + self._files = copy.deepcopy(files) if files else None + """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 the attached executable. + + :return: the executable + """ + return self._exe + + @exe.setter + def exe(self, value: str | os.PathLike[str]) -> None: + """Set the executable. + + :param value: the executable + """ + self._exe = os.fspath(value) + + @property + def exe_args(self) -> t.List[str]: + """Return attached list of executable arguments. + + :return: the executable arguments + """ + return self._exe_args + + @exe_args.setter + def exe_args(self, value: t.Sequence[str]) -> None: + """Set the executable arguments. + + :param value: the executable arguments + """ + self._exe_args = list(value) + + @property + def exe_arg_parameters(self) -> t.Mapping[str, t.Sequence[t.Sequence[str]]]: + """Return attached executable argument parameters. + + :return: the executable argument 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 argument parameters. + + :param value: the executable argument parameters + """ + self._exe_arg_parameters = copy.deepcopy(value) + + @property + def files(self) -> t.Union[EntityFiles, None]: + """Return attached EntityFiles object. + + :return: the EntityFiles object of files to be copied, symlinked, + and/or configured prior to execution + """ + return self._files + + @files.setter + def files(self, value: t.Optional[EntityFiles]) -> None: + """Set the EntityFiles object. + + :param value: the EntityFiles object of files to be copied, symlinked, + and/or configured prior to execution + """ + self._files = copy.deepcopy(value) + + @property + def file_parameters(self) -> t.Mapping[str, t.Sequence[str]]: + """Return the attached file parameters. + + :return: the 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: the file parameters + """ + self._file_parameters = dict(value) + + @property + def permutation_strategy(self) -> str | strategies.PermutationStrategyType: + """Return the permutation strategy + + :return: the permutation strategy + """ + return self._permutation_strategy + + @permutation_strategy.setter + def permutation_strategy( + self, value: str | strategies.PermutationStrategyType + ) -> None: + """Set the permutation strategy + + :param value: the permutation strategy + """ + self._permutation_strategy = value + + @property + def max_permutations(self) -> int: + """Return the maximum permutations + + :return: the max permutations + """ + return self._max_permutations + + @max_permutations.setter + def max_permutations(self, value: int) -> None: + """Set the maximum permutations + + :param value: the max permutations + """ + self._max_permutations = value + + @property + def replicas(self) -> int: + """Return the number of replicas. + + :return: the 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, ...]: + """Generate a collection of Application instances based on the Ensembles attributes. + + This method uses a permutation strategy to create various combinations of file + parameters and executable arguments. Each combination is then replicated according + to the specified number of replicas, resulting in a set of Application instances. + + :return: A tuple of Application instances + """ + permutation_strategy = strategies.resolve(self.permutation_strategy) + + combinations = permutation_strategy( + self.file_parameters, self.exe_arg_parameters, self.max_permutations + ) + combinations = combinations if combinations else [ParamSet({}, {})] + permutations_ = itertools.chain.from_iterable( + itertools.repeat(permutation, self.replicas) for permutation in combinations + ) + return tuple( + Application( + name=f"{self.name}-{i}", + exe=self.exe, + exe_args=self.exe_args, + files=self.files, + file_parameters=permutation.params, + ) + for i, permutation in enumerate(permutations_) + ) + + def build_jobs(self, settings: LaunchSettings) -> tuple[Job, ...]: + """Expand an Ensemble into a list of deployable Jobs and apply + identical LaunchSettings to each Job. + + The number of Jobs returned is controlled by the Ensemble attributes: + - Ensemble.exe_arg_parameters + - Ensemble.file_parameters + - Ensemble.permutation_strategy + - Ensemble.max_permutations + - Ensemble.replicas + + Consider the example below: + + .. highlight:: python + .. code-block:: python + + # Create LaunchSettings + my_launch_settings = LaunchSettings(...) + + # Initialize the Ensemble + ensemble = Ensemble("my_name", "echo", "hello world", replicas=3) + # Expand Ensemble into Jobs + ensemble_as_jobs = ensemble.build_jobs(my_launch_settings) + + By calling `build_jobs` on `ensemble`, three Jobs are returned because + three replicas were specified. Each Job will have the provided LaunchSettings. + + :param settings: LaunchSettings to apply to each Job + :return: Sequence of Jobs with the provided LaunchSettings + """ + apps = self._create_applications() + if not apps: + raise ValueError("There are no members as part of this ensemble") + return tuple(Job(app, settings, app.name) for app in apps) diff --git a/smartsim/entity/__init__.py b/smartsim/entity/__init__.py index 420b7c08d..fcc00201b 100644 --- a/smartsim/entity/__init__.py +++ b/smartsim/entity/__init__.py @@ -24,9 +24,9 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from ..builders.ensemble import Ensemble from .application import Application from .dbnode import FSNode from .dbobject import * -from ..builders.ensemble import Ensemble from .entity import SmartSimEntity, TelemetryConfiguration from .files import TaggedFilesHierarchy diff --git a/tests/_legacy/test_controller.py b/tests/_legacy/test_controller.py index c29be3195..ad0c98fe8 100644 --- a/tests/_legacy/test_controller.py +++ b/tests/_legacy/test_controller.py @@ -30,8 +30,8 @@ from smartsim._core.control.controller import Controller from smartsim._core.launcher.step import Step -from smartsim.database.orchestrator import FeatureStore from smartsim.builders.ensemble import Ensemble +from smartsim.database.orchestrator import FeatureStore from smartsim.settings.slurmSettings import SbatchSettings, SrunSettings controller = Controller() diff --git a/tests/_legacy/test_controller_errors.py b/tests/_legacy/test_controller_errors.py index c053f5d2a..5ae05d70a 100644 --- a/tests/_legacy/test_controller_errors.py +++ b/tests/_legacy/test_controller_errors.py @@ -30,9 +30,9 @@ from smartsim._core.control import Controller, Manifest from smartsim._core.launcher.step import Step from smartsim._core.launcher.step.dragon_step import DragonStep +from smartsim.builders.ensemble import Ensemble from smartsim.database import FeatureStore from smartsim.entity import Application -from smartsim.builders.ensemble import Ensemble from smartsim.error import SmartSimError, SSUnsupportedError from smartsim.error.errors import SSUnsupportedError from smartsim.settings import RunSettings, SrunSettings diff --git a/tests/_legacy/test_output_files.py b/tests/_legacy/test_output_files.py index 8cdb79bf2..55ecfd90a 100644 --- a/tests/_legacy/test_output_files.py +++ b/tests/_legacy/test_output_files.py @@ -33,9 +33,9 @@ from smartsim._core.config import CONFIG from smartsim._core.control.controller import Controller, _AnonymousBatchJob from smartsim._core.launcher.step import Step +from smartsim.builders.ensemble import Ensemble from smartsim.database.orchestrator import FeatureStore from smartsim.entity.application import Application -from smartsim.builders.ensemble import Ensemble from smartsim.settings.base import RunSettings from smartsim.settings.slurmSettings import SbatchSettings, SrunSettings diff --git a/tests/_legacy/test_symlinking.py b/tests/_legacy/test_symlinking.py index 898e63252..95aa187e6 100644 --- a/tests/_legacy/test_symlinking.py +++ b/tests/_legacy/test_symlinking.py @@ -32,9 +32,9 @@ from smartsim import Experiment from smartsim._core.config import CONFIG from smartsim._core.control.controller import Controller, _AnonymousBatchJob +from smartsim.builders.ensemble import Ensemble from smartsim.database.orchestrator import FeatureStore from smartsim.entity.application import Application -from smartsim.builders.ensemble import Ensemble from smartsim.settings.base import RunSettings from smartsim.settings.slurmSettings import SbatchSettings, SrunSettings From fc3631129b7a6799cba2cc2ab2d885eb9b26624f Mon Sep 17 00:00:00 2001 From: Amanda Richardson Date: Fri, 20 Sep 2024 14:35:44 -0500 Subject: [PATCH 3/5] updates --- smartsim/builders/ensemble.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/smartsim/builders/ensemble.py b/smartsim/builders/ensemble.py index 48220187b..526912efe 100644 --- a/smartsim/builders/ensemble.py +++ b/smartsim/builders/ensemble.py @@ -44,9 +44,7 @@ class Ensemble(entity.CompoundEntity): """An Ensemble is a builder class that parameterizes the creation of multiple - Applications. Ensembles can be customized by using specific combinations of the - input settings to align with one of the following creation strategies: parameter - expansion and replica creation. + Applications. """ def __init__( @@ -62,22 +60,24 @@ def __init__( replicas: int = 1, ) -> None: """Initialize an ``Ensemble`` of Application instances - + An Ensemble can be tailored to align with one of the following creation strategies: parameter expansion or replicas. - + **Parameter Expansion** - + Parameter expansion allows users to assign different parameter values to - multiple applications. This is done by specifying input to `Ensemble.file_parameters`, + multiple Applications. This is done by specifying input to `Ensemble.file_parameters`, `Ensemble.exe_arg_parameters` and `Ensemble.permutation_strategy`. The `permutation_strategy` - accepts three options: + argument accepts three options: + 1. "all_perm": Generates all possible parameter permutations for exhaustive exploration. 2. "step": Collects identically indexed values across parameter lists to create parameter sets. 3. "random": Enables random selection from predefined parameter spaces. The example below demonstrates creating an Ensemble via parameter expansion, resulting in the creation of two Applications: + .. highlight:: python .. code-block:: python @@ -87,22 +87,22 @@ def __init__( file_parameters=file_params,permutation_strategy="step") This configuration will yield the following permutations: - + .. highlight:: python .. code-block:: python [ParamSet(params={'SPAM': 'a', 'EGGS': 'c'}, exe_args={'EXE': ['a'], 'ARGS': ['d']}), ParamSet(params={'SPAM': 'b', 'EGGS': 'd'}, exe_args={'EXE': ['b', 'c'], 'ARGS': ['e', 'f']})] - + Each ParamSet contains the parameters assigned from file_params and the corresponding executable arguments from exe_arg_parameters. - - **Replication** + + **Replication** The replication strategy involves creating identical Applications within an Ensemble. This is achieved by specifying the `replicas` argument in the Ensemble. - + For example, by applying the `replicas` argument to the previous parameter expansion example, we can double our Application output: - + .. highlight:: python .. code-block:: python @@ -112,8 +112,8 @@ def __init__( file_parameters=file_params,permutation_strategy="step", replicas=2) This configuration will result in each ParamSet being replicated, effectively doubling - the number of applications created. - + the number of Applications created. + :param name: name of the ensemble :param exe: executable to run :param exe_args: executable arguments @@ -345,4 +345,4 @@ def build_jobs(self, settings: LaunchSettings) -> tuple[Job, ...]: apps = self._create_applications() if not apps: raise ValueError("There are no members as part of this ensemble") - return tuple(Job(app, settings, app.name) for app in apps) + return tuple(Job(app, settings, app.name) for app in apps) \ No newline at end of file From ffcbbe093448769403dfb6d325d917cdd54dd782 Mon Sep 17 00:00:00 2001 From: Amanda Richardson Date: Fri, 20 Sep 2024 15:29:52 -0500 Subject: [PATCH 4/5] styling --- smartsim/builders/ensemble.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/smartsim/builders/ensemble.py b/smartsim/builders/ensemble.py index 526912efe..e8c023f6c 100644 --- a/smartsim/builders/ensemble.py +++ b/smartsim/builders/ensemble.py @@ -60,12 +60,12 @@ def __init__( replicas: int = 1, ) -> None: """Initialize an ``Ensemble`` of Application instances - + An Ensemble can be tailored to align with one of the following creation strategies: parameter expansion or replicas. - + **Parameter Expansion** - + Parameter expansion allows users to assign different parameter values to multiple Applications. This is done by specifying input to `Ensemble.file_parameters`, `Ensemble.exe_arg_parameters` and `Ensemble.permutation_strategy`. The `permutation_strategy` @@ -87,22 +87,22 @@ def __init__( file_parameters=file_params,permutation_strategy="step") This configuration will yield the following permutations: - + .. highlight:: python .. code-block:: python [ParamSet(params={'SPAM': 'a', 'EGGS': 'c'}, exe_args={'EXE': ['a'], 'ARGS': ['d']}), ParamSet(params={'SPAM': 'b', 'EGGS': 'd'}, exe_args={'EXE': ['b', 'c'], 'ARGS': ['e', 'f']})] - + Each ParamSet contains the parameters assigned from file_params and the corresponding executable arguments from exe_arg_parameters. - - **Replication** + + **Replication** The replication strategy involves creating identical Applications within an Ensemble. This is achieved by specifying the `replicas` argument in the Ensemble. - + For example, by applying the `replicas` argument to the previous parameter expansion example, we can double our Application output: - + .. highlight:: python .. code-block:: python @@ -113,7 +113,7 @@ def __init__( This configuration will result in each ParamSet being replicated, effectively doubling the number of Applications created. - + :param name: name of the ensemble :param exe: executable to run :param exe_args: executable arguments @@ -345,4 +345,4 @@ def build_jobs(self, settings: LaunchSettings) -> tuple[Job, ...]: apps = self._create_applications() if not apps: raise ValueError("There are no members as part of this ensemble") - return tuple(Job(app, settings, app.name) for app in apps) \ No newline at end of file + return tuple(Job(app, settings, app.name) for app in apps) From 8144d51cf8c5a1cd0542e626259c880347f3f72d Mon Sep 17 00:00:00 2001 From: Amanda Richardson Date: Mon, 23 Sep 2024 15:56:36 -0500 Subject: [PATCH 5/5] strategies moved to utils --- smartsim/_core/control/manifest.py | 3 +- smartsim/_core/launcher/step/slurm_step.py | 3 +- smartsim/_core/launcher/step/step.py | 3 +- smartsim/_core/utils/serialize.py | 3 +- smartsim/builders/__init__.py | 28 +++++++++++++++++++ smartsim/builders/ensemble.py | 5 ++-- .../{entity => builders/utils}/strategies.py | 0 smartsim/entity/__init__.py | 1 - tests/_legacy/test_ensemble.py | 3 +- tests/_legacy/test_model.py | 3 +- tests/_legacy/test_smartredis.py | 3 +- tests/test_ensemble.py | 2 +- tests/test_generator.py | 3 +- tests/test_permutation_strategies.py | 4 +-- 14 files changed, 50 insertions(+), 14 deletions(-) create mode 100644 smartsim/builders/__init__.py rename smartsim/{entity => builders/utils}/strategies.py (100%) diff --git a/smartsim/_core/control/manifest.py b/smartsim/_core/control/manifest.py index cb47af14e..89b80c217 100644 --- a/smartsim/_core/control/manifest.py +++ b/smartsim/_core/control/manifest.py @@ -31,8 +31,9 @@ from smartsim.entity._mock import Mock +from ...builders import Ensemble from ...database import FeatureStore -from ...entity import Application, Ensemble, FSNode, SmartSimEntity +from ...entity import Application, FSNode, SmartSimEntity from ...error import SmartSimError from ..config import CONFIG from ..utils import helpers as _helpers diff --git a/smartsim/_core/launcher/step/slurm_step.py b/smartsim/_core/launcher/step/slurm_step.py index 3f178d974..2a9046a3a 100644 --- a/smartsim/_core/launcher/step/slurm_step.py +++ b/smartsim/_core/launcher/step/slurm_step.py @@ -29,7 +29,8 @@ import typing as t from shlex import split as sh_split -from ....entity import Application, Ensemble, FSNode +from ....builders import Ensemble +from ....entity import Application, FSNode from ....error import AllocationError from ....log import get_logger from ....settings import RunSettings, SbatchSettings, Singularity, SrunSettings diff --git a/smartsim/_core/launcher/step/step.py b/smartsim/_core/launcher/step/step.py index 46bcebf7f..b5e79a363 100644 --- a/smartsim/_core/launcher/step/step.py +++ b/smartsim/_core/launcher/step/step.py @@ -38,7 +38,8 @@ from smartsim._core.config import CONFIG from smartsim.error.errors import SmartSimError, UnproxyableStepError -from ....entity import Application, Ensemble, FSNode +from ....builders import Ensemble +from ....entity import Application, FSNode from ....log import get_logger from ....settings import RunSettings, SettingsBase from ...utils.helpers import encode_cmd, get_base_36_repr diff --git a/smartsim/_core/utils/serialize.py b/smartsim/_core/utils/serialize.py index aad38c778..46c0a2c1d 100644 --- a/smartsim/_core/utils/serialize.py +++ b/smartsim/_core/utils/serialize.py @@ -36,8 +36,9 @@ if t.TYPE_CHECKING: from smartsim._core.control.manifest import LaunchedManifest as _Manifest + from smartsim.builders import Ensemble from smartsim.database.orchestrator import FeatureStore - from smartsim.entity import Application, Ensemble, FSNode + from smartsim.entity import Application, FSNode from smartsim.entity.dbobject import FSModel, FSScript from smartsim.settings.base import BatchSettings, RunSettings diff --git a/smartsim/builders/__init__.py b/smartsim/builders/__init__.py new file mode 100644 index 000000000..866269f20 --- /dev/null +++ b/smartsim/builders/__init__.py @@ -0,0 +1,28 @@ +# BSD 2-Clause License +# +# Copyright (c) 2021-2024, Hewlett Packard Enterprise +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from .ensemble import Ensemble +from .utils.strategies import ParamSet diff --git a/smartsim/builders/ensemble.py b/smartsim/builders/ensemble.py index e8c023f6c..c4a57175f 100644 --- a/smartsim/builders/ensemble.py +++ b/smartsim/builders/ensemble.py @@ -32,10 +32,11 @@ import os.path import typing as t -from smartsim.entity import entity, strategies +from smartsim.builders.utils import strategies +from smartsim.builders.utils.strategies import ParamSet +from smartsim.entity import entity from smartsim.entity.application import Application from smartsim.entity.files import EntityFiles -from smartsim.entity.strategies import ParamSet from smartsim.launchable.job import Job if t.TYPE_CHECKING: diff --git a/smartsim/entity/strategies.py b/smartsim/builders/utils/strategies.py similarity index 100% rename from smartsim/entity/strategies.py rename to smartsim/builders/utils/strategies.py diff --git a/smartsim/entity/__init__.py b/smartsim/entity/__init__.py index fcc00201b..a12d737bb 100644 --- a/smartsim/entity/__init__.py +++ b/smartsim/entity/__init__.py @@ -24,7 +24,6 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from ..builders.ensemble import Ensemble from .application import Application from .dbnode import FSNode from .dbobject import * diff --git a/tests/_legacy/test_ensemble.py b/tests/_legacy/test_ensemble.py index 86146c8e4..62c7d8d4f 100644 --- a/tests/_legacy/test_ensemble.py +++ b/tests/_legacy/test_ensemble.py @@ -30,7 +30,8 @@ import pytest from smartsim import Experiment -from smartsim.entity import Application, Ensemble +from smartsim.builders import Ensemble +from smartsim.entity import Application from smartsim.error import EntityExistsError, SSUnsupportedError, UserStrategyError from smartsim.settings import RunSettings diff --git a/tests/_legacy/test_model.py b/tests/_legacy/test_model.py index f32a27a07..5adf8070f 100644 --- a/tests/_legacy/test_model.py +++ b/tests/_legacy/test_model.py @@ -31,7 +31,8 @@ from smartsim import Experiment from smartsim._core.control.manifest import LaunchedManifestBuilder from smartsim._core.launcher.step import SbatchStep, SrunStep -from smartsim.entity import Application, Ensemble +from smartsim.builders import Ensemble +from smartsim.entity import Application from smartsim.error import EntityExistsError, SSUnsupportedError from smartsim.settings import RunSettings, SbatchSettings, SrunSettings from smartsim.settings.mpiSettings import _BaseMPISettings diff --git a/tests/_legacy/test_smartredis.py b/tests/_legacy/test_smartredis.py index ca8d1e0fa..f09cc8ca8 100644 --- a/tests/_legacy/test_smartredis.py +++ b/tests/_legacy/test_smartredis.py @@ -29,8 +29,9 @@ from smartsim import Experiment from smartsim._core.utils import installed_redisai_backends +from smartsim.builders import Ensemble from smartsim.database import FeatureStore -from smartsim.entity import Application, Ensemble +from smartsim.entity import Application from smartsim.status import JobStatus # The tests in this file belong to the group_b group diff --git a/tests/test_ensemble.py b/tests/test_ensemble.py index 2addcc682..9c9015251 100644 --- a/tests/test_ensemble.py +++ b/tests/test_ensemble.py @@ -32,8 +32,8 @@ import pytest from smartsim.builders.ensemble import Ensemble +from smartsim.builders.utils.strategies import ParamSet from smartsim.entity.files import EntityFiles -from smartsim.entity.strategies import ParamSet from smartsim.settings.launch_settings import LaunchSettings pytestmark = pytest.mark.group_a diff --git a/tests/test_generator.py b/tests/test_generator.py index b29c9ca97..8f5a02f0b 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -11,7 +11,8 @@ from smartsim import Experiment from smartsim._core.generation.generator import Generator -from smartsim.entity import Application, Ensemble +from smartsim.builders import Ensemble +from smartsim.entity import Application from smartsim.entity.files import EntityFiles from smartsim.launchable import Job from smartsim.settings import LaunchSettings diff --git a/tests/test_permutation_strategies.py b/tests/test_permutation_strategies.py index b14514c99..314c21063 100644 --- a/tests/test_permutation_strategies.py +++ b/tests/test_permutation_strategies.py @@ -28,8 +28,8 @@ import pytest -from smartsim.entity import strategies -from smartsim.entity.strategies import ParamSet +from smartsim.builders.utils import strategies +from smartsim.builders.utils.strategies import ParamSet from smartsim.error import errors pytestmark = pytest.mark.group_a