Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add directory support for generator to_configure parameter #119

Merged
merged 13 commits into from
Jan 7, 2022
133 changes: 126 additions & 7 deletions smartsim/entity/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
# 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.

import os
from os import path

from ..error import SSConfigError
Expand Down Expand Up @@ -63,6 +64,7 @@ def __init__(self, tagged, copy, symlink):
self.tagged = tagged
self.copy = copy
self.link = symlink
self.tagged_hierarchy = None
self._check_files()

def _check_files(self):
Expand All @@ -78,12 +80,9 @@ def _check_files(self):
self.copy = self._type_check_files(self.copy, "Copyable")
self.link = self._type_check_files(self.link, "Symlink")

# check the paths provided by the user and ensure
# that no directories were provided as tagged files
for i in range(len(self.tagged)):
self.tagged[i] = self._check_path(self.tagged[i])
if path.isdir(self.tagged[i]):
raise SSConfigError("Directories cannot be listed in tagged files")
self.tagged_hierarchy = TaggedFilesHierarchy.from_list_paths(
Spartee marked this conversation as resolved.
Show resolved Hide resolved
self.tagged, dir_contents_to_base=True
)

for i in range(len(self.copy)):
self.copy[i] = self._check_path(self.copy[i])
Expand Down Expand Up @@ -114,7 +113,8 @@ def _type_check_files(self, file_list, file_type):
raise TypeError(f"Not all {file_type} files were of type str")
return file_list

def _check_path(self, file_path):
@staticmethod
def _check_path(file_path):
"""Given a user provided path-like str, find the actual path to
the directory or file and create a full path.

Expand All @@ -130,3 +130,122 @@ def _check_path(self, file_path):
if path.isdir(full_path):
return full_path
raise SSConfigError(f"File or Directory {file_path} not found")
MattToast marked this conversation as resolved.
Show resolved Hide resolved


class TaggedFilesHierarchy:
"""The TaggedFilesHierarchy class maintains a list of files and a list of
child TaggedFilesHierarchy instances. Each instance describes how a
directory structure of tagged files.
MattToast marked this conversation as resolved.
Show resolved Hide resolved

TaggedFilesHierarchy represents a directory containing potentially tagged
files and subdirectories.

TaggedFilesHierarchy.base is the directory path from the the root of the
file structure
E.g.
TaggedFilesHierarchy.base = "" => ./
TaggedFilesHierarchy.base = "dir_a" => ./dir_a
TaggedFilesHierarchy.base = "dir_a/dir_b" => ./dir_a/dir_b

TaggedFilesHierarchy.files is a collection of paths to files that need
to be copied to directory that the TaggedFilesHierarchy represents

TaggedFilesHierarchy.dirs is a collection of child TaggedFilesHierarchy,
each with its own files and dirs, representing a directory that needs to
be created within the current directory represented by the current
TaggedFilesHierarchy

By performing a depth first search over the entire hierarchy starting at
the root (TaggedFilesHierarchy.base = ""), one could reconstruct the
tagged file directory structure with relative ease at any location deemed
necessary.
"""

def __init__(self, base=""):
"""Initialize a TaggedFilesHierarchy

:param base: pathlike string specifing the generated directory
MattToast marked this conversation as resolved.
Show resolved Hide resolved
files are located
:type base: str, optional
"""
self.base = base
self.files = set()
self.dirs = set()

@classmethod
def from_list_paths(cls, path_list, dir_contents_to_base=False):
"""Given a list of absolute paths to files and dirs, create and return
a TaggedFilesHierarchy instance representing the file hierarchy of
tagged files. All files in the path list will be placed in the base of
the file hierarchy.

:param path_list: list of absolute paths to tagged files or dirs
containing tagged files
:type path_list: list[str]
:param dir_contents_to_base: When a top level dir is encountered, if
this value is truthy, files in the dir are
Spartee marked this conversation as resolved.
Show resolved Hide resolved
put into the base hierarchy level.
Otherwise, a new sub level is created for
the dir
:type dir_contents_to_base: bool
:return: A built tagged file hierarchy for the given files
:rtype: TaggedFilesHierarchy
"""
tagged_file_hierarchy = cls()
if dir_contents_to_base:
new_paths = []
for path in path_list:
if os.path.isdir(path):
new_paths += [os.path.join(path, file) for file in os.listdir(path)]
else:
new_paths.append(path)
path_list = new_paths
tagged_file_hierarchy._add_paths(path_list)
return tagged_file_hierarchy

def _add_file(self, file):
"""Add a file to the current level in the file hierarchy

:param file: absoute path to a file to add to the hierarchy
:type file: str
"""
self.files.add(file)

def _add_dir(self, dir):
"""Add a dir contianing tagged files by creating a new sub level in the
tagged file hierarchy. All paths within the directroy are added to the
the new level sub level tagged file hierarchy

:param dir: absoute path to a dir to add to the hierarchy
:type dir: str
"""
tagged_file_hierarchy = TaggedFilesHierarchy(
path.join(self.base, path.basename(dir))
)
tagged_file_hierarchy._add_paths(
[path.join(dir, file) for file in os.listdir(dir)]
)
self.dirs.add(tagged_file_hierarchy)

def _add_paths(self, paths):
"""Takes a list of paths and iterates over it, determining if each
path is to a file or a dir and then appropriatly adding it to the
TaggedFilesHierarchy.

:param paths: list of paths to files or dirs to add to the hierarchy
:type paths: list[str]
:raises SSConfigError: if link to dir is found or path does not exist
"""
for path in paths:
path = os.path.abspath(path)
if os.path.isdir(path):
if os.path.islink(path):
raise SSConfigError(
MattToast marked this conversation as resolved.
Show resolved Hide resolved
"Tagged directories and thier subdirectories cannot be links"
+ " to prevent circular directory structures"
)
self._add_dir(path)
elif os.path.isfile(path):
self._add_file(path)
else:
raise SSConfigError(f"File or Directory {path} not found")
MattToast marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion smartsim/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
# 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.

import time
import os.path as osp
import time
from os import getcwd
from pprint import pformat

Expand Down
23 changes: 19 additions & 4 deletions smartsim/generation/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,25 @@ def _write_tagged_entity_files(self, entity):
"""
if entity.files:
to_write = []
for tagged_file in entity.files.tagged:
dst_path = path.join(entity.path, path.basename(tagged_file))
shutil.copyfile(tagged_file, dst_path)
to_write.append(dst_path)

def _build_tagged_files(tagged):
"""Using a TaggedFileHierarchy, reproduce the tagged file
directory structure

:param tagged: a TaggedFileHierarchy to be built as a
directory structure
:type tagged: TaggedFilesHierarchy
"""
for file in tagged.files:
dst_path = path.join(entity.path, tagged.base, path.basename(file))
shutil.copyfile(file, dst_path)
to_write.append(dst_path)

for dir in tagged.dirs:
mkdir(path.join(entity.path, tagged.base, path.basename(dir.base)))
_build_tagged_files(dir)

_build_tagged_files(entity.files.tagged_hierarchy)

# write in changes to configurations
if isinstance(entity, Model):
Expand Down
17 changes: 9 additions & 8 deletions smartsim/settings/cobaltSettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,15 @@ def __init__(
:param batch_args: extra batch arguments, defaults to None
:type batch_args: dict[str, str], optional
"""
super().__init__("qsub",
batch_args=batch_args,
nodes=nodes,
account=account,
queue=queue,
time=time,
**kwargs)

super().__init__(
"qsub",
batch_args=batch_args,
nodes=nodes,
account=account,
queue=queue,
time=time,
**kwargs,
)

def set_walltime(self, walltime):
"""Set the walltime of the job
Expand Down
22 changes: 12 additions & 10 deletions smartsim/settings/lsfSettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def set_tasks_per_rs(self, tasks_per_rs):

def set_tasks_per_node(self, tasks_per_node):
"""Set the number of tasks per resource set.

This function is an alias for `set_tasks_per_rs`.

:param tasks_per_node: number of tasks per resource set
Expand All @@ -138,13 +138,13 @@ def set_hostlist(self, host_list):

This function is only available to unify LSFSettings
to other WLM settings classes.

"""
pass

def set_cpus_per_task(self, cpus_per_task):
"""Set the number of cpus per tasks.

This function is an alias for `set_cpus_per_rs`.

:param cpus_per_task: number of cpus per resource set
Expand Down Expand Up @@ -335,13 +335,15 @@ def __init__(
kwargs.pop("account", None)
else:
project = kwargs.pop("account", None)

super().__init__("bsub",
batch_args=batch_args,
nodes=nodes,
account=project,
time=time,
**kwargs)

super().__init__(
"bsub",
batch_args=batch_args,
nodes=nodes,
account=project,
time=time,
**kwargs,
)

if smts:
self.set_smts(smts)
Expand Down
16 changes: 9 additions & 7 deletions smartsim/settings/pbsSettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,15 @@ def __init__(
:type batch_args: dict[str, str], optional
"""

super().__init__("qsub",
batch_args=batch_args,
nodes=nodes,
account=account,
queue=queue,
time=time,
**kwargs)
super().__init__(
"qsub",
batch_args=batch_args,
nodes=nodes,
account=account,
queue=queue,
time=time,
**kwargs,
)
self.resources = init_default({}, resources, dict)

self._ncpus = ncpus
Expand Down
7 changes: 6 additions & 1 deletion smartsim/settings/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,12 @@ def create_batch_settings(
try:
batch_class = by_launcher[launcher]
batch_settings = batch_class(
nodes=nodes, time=time, batch_args=batch_args, queue=queue, account=account, **kwargs
nodes=nodes,
time=time,
batch_args=batch_args,
queue=queue,
account=account,
**kwargs,
)
return batch_settings

Expand Down
14 changes: 8 additions & 6 deletions smartsim/settings/slurmSettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,12 +224,14 @@ def __init__(self, nodes=None, time="", account=None, batch_args=None, **kwargs)
:param batch_args: extra batch arguments, defaults to None
:type batch_args: dict[str, str], optional
"""
super().__init__("sbatch",
batch_args=batch_args,
nodes=nodes,
account=account,
time=time,
**kwargs)
super().__init__(
"sbatch",
batch_args=batch_args,
nodes=nodes,
account=account,
time=time,
**kwargs,
)

def set_walltime(self, walltime):
"""Set the walltime of the job
Expand Down
2 changes: 1 addition & 1 deletion tests/on_wlm/test_launch_orc_slurm.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def test_incoming_entities(fileutils, wlmutils):
launcher = wlmutils.get_test_launcher()
if launcher != "slurm":
pytest.skip("Test only runs on systems with Slurm as WLM")

exp_name = "test-incoming-entities"
exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher())
test_dir = fileutils.make_test_dir(exp_name)
Expand Down
8 changes: 6 additions & 2 deletions tests/on_wlm/test_simple_base_settings_on_wlm.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
def test_simple_model_on_wlm(fileutils, wlmutils):
launcher = wlmutils.get_test_launcher()
if launcher not in ["pbs", "slurm", "cobalt", "lsf"]:
pytest.skip("Test only runs on systems with LSF, PBSPro, Slurm, or Cobalt as WLM")
pytest.skip(
"Test only runs on systems with LSF, PBSPro, Slurm, or Cobalt as WLM"
)

exp_name = "test-simplebase-settings-model-launch"
exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher())
Expand All @@ -44,7 +46,9 @@ def test_simple_model_on_wlm(fileutils, wlmutils):
def test_simple_model_stop_on_wlm(fileutils, wlmutils):
launcher = wlmutils.get_test_launcher()
if launcher not in ["pbs", "slurm", "cobalt", "lsf"]:
pytest.skip("Test only runs on systems with LSF, PBSPro, Slurm, or Cobalt as WLM")
pytest.skip(
"Test only runs on systems with LSF, PBSPro, Slurm, or Cobalt as WLM"
)

exp_name = "test-simplebase-settings-model-stop"
exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher())
Expand Down
Loading