Skip to content

Commit

Permalink
addressing all of matts comments besides 1
Browse files Browse the repository at this point in the history
  • Loading branch information
amandarichardsonn committed Aug 6, 2024
1 parent f9c9d56 commit f90a285
Show file tree
Hide file tree
Showing 12 changed files with 262 additions and 198 deletions.
4 changes: 4 additions & 0 deletions smartsim/_core/entrypoints/file_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,15 @@ def copy(parsed_args: argparse.Namespace) -> None:
FileExistsError will be raised
"""
if os.path.isdir(parsed_args.source):
print("here")
print(parsed_args.source)
print(parsed_args.dest)
shutil.copytree(
parsed_args.source,
parsed_args.dest,
dirs_exist_ok=parsed_args.dirs_exist_ok,
)
print(os.listdir(parsed_args.dest))
else:
shutil.copy(parsed_args.source, parsed_args.dest)

Expand Down
77 changes: 19 additions & 58 deletions smartsim/_core/generation/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,16 @@ class Generator:
files into the Job directory.
"""

def __init__(self, exp_path: str, run_id: str) -> None:
def __init__(self, root: str | os.PathLike[str]) -> None:
"""Initialize a generator object
The Generator class is responsible for creating Job directories.
TODO The Generator class is responsible for creating Job directories.
It ensures that paths adhere to SmartSim path standards. Additionally,
it creates a run directory to handle symlinking,
configuration, and file copying to the job directory.
:param gen_path: Path in which files need to be generated
:param run_ID: The id of the Experiment
"""
self.exp_path = pathlib.Path(exp_path)
"""The path under which the experiment operate"""
self.run_id = run_id
"""The runID for Experiment.start"""
self.root = root
"""The root path under which to generate files"""

def log_file(self, log_path: pathlib.Path) -> str:
"""Returns the location of the file
Expand All @@ -77,67 +72,33 @@ def log_file(self, log_path: pathlib.Path) -> str:
"""
return join(log_path, "smartsim_params.txt")

def generate_job(self, job: Job, job_index: int) -> pathlib.Path:
"""Generate the Job directory

Generate the file structure for a SmartSim Job. This
includes writing and configuring input files for the entity.
def generate_job(self, job: Job, job_path: str, log_path: str):
"""Write and configure input files for a Job.
To have files or directories present in the created Job
directories, such as datasets or input files, call
directory, such as datasets or input files, call
``entity.attach_generator_files`` prior to generation.
Tagged application files are read, checked for input variables to
configure, and written. Input variables to configure are
specified with a tag within the input file itself.
The default tag is surronding an input value with semicolons.
e.g. ``THERMO=;90;``
:param job: The job instance to write and configure files for.
:param job_path: The path to the \"run\" directory for the job instance.
:param log_path: The path to the \"log\" directory for the job instance.
"""
# Generate ../job_name/run directory
job_path = self._generate_job_path(job, job_index)
# Generate ../job_name/log directory
log_path = self._generate_log_path(job, job_index)

# Create and write to the parameter settings file
with open(self.log_file(log_path), mode="w", encoding="utf-8") as log_file:
dt_string = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
log_file.write(f"Generation start date and time: {dt_string}\n")

# Perform file system ops
# Perform file system operations on attached files
self._build_operations(job, job_path)

# Return Job path
return job_path

def _generate_job_path(self, job: Job, job_index: int) -> pathlib.Path:
"""
Generate the run directory for a Job.
:param job: The Job to generate a directory
:returns pathlib.Path:: The generated run path for the Job
"""
job_type = f"{job.__class__.__name__.lower()}s"
job_path = (
self.exp_path / self.run_id / job_type / f"{job.name}-{job_index}" / "run"
)
# Create Job directory
job_path.mkdir(exist_ok=True, parents=True)
return job_path

def _generate_log_path(self, job: Job, job_index: int) -> pathlib.Path:
"""
Generate the log directory for a Job.
:param job: The Job to generate a directory
:returns pathlib.Path:: The generated log path for the Job
"""
job_type = f"{job.__class__.__name__.lower()}s"
log_path = (
self.exp_path / self.run_id / job_type / f"{job.name}-{job_index}" / "log"
)
log_path.mkdir(exist_ok=True, parents=True)
return log_path

def _build_operations(self, job: Job, job_path: pathlib.Path) -> None:
"""This method orchestrates file system ops for the attached SmartSim entity.
Expand All @@ -151,7 +112,7 @@ def _build_operations(self, job: Job, job_path: pathlib.Path) -> None:
app = t.cast(Application, job.entity)
self._copy_files(app.files, job_path)
self._symlink_files(app.files, job_path)
self._write_tagged_files(app, job_path)
self._write_tagged_files(app.files, app.params, job_path)

@staticmethod
def _copy_files(files: t.Union[EntityFiles, None], dest: pathlib.Path) -> None:
Expand Down Expand Up @@ -217,7 +178,7 @@ def _symlink_files(files: t.Union[EntityFiles, None], dest: pathlib.Path) -> Non
)

@staticmethod
def _write_tagged_files(app: Application, dest: pathlib.Path) -> None:
def _write_tagged_files(files: t.Union[EntityFiles, None], params: t.Mapping[str, str], dest: pathlib.Path) -> None:
"""Read, configure and write the tagged input files for
a Job instance. This function specifically deals with the tagged
files attached to an entity.
Expand All @@ -226,9 +187,9 @@ def _write_tagged_files(app: Application, dest: pathlib.Path) -> None:
:param dest: Path to the Jobs run directory
"""
# Return if no files are attached
if app.files is None:
if files is None:
return
if app.files.tagged:
if files.tagged:
to_write = []

def _build_tagged_files(tagged: TaggedFilesHierarchy) -> None:
Expand All @@ -247,11 +208,11 @@ def _build_tagged_files(tagged: TaggedFilesHierarchy) -> None:
mkdir(path.join(dest, tagged.base, path.basename(tagged_dir.base)))
_build_tagged_files(tagged_dir)

if app.files.tagged_hierarchy:
_build_tagged_files(app.files.tagged_hierarchy)
if files.tagged_hierarchy:
_build_tagged_files(files.tagged_hierarchy)

# Pickle the dictionary
pickled_dict = pickle.dumps(app.params)
pickled_dict = pickle.dumps(params)
# Default tag delimiter
tag = ";"
# Encode the pickled dictionary with Base64
Expand Down
13 changes: 13 additions & 0 deletions smartsim/_core/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@
_T = t.TypeVar("_T")
_TSignalHandlerFn = t.Callable[[int, t.Optional["FrameType"]], object]

def check_name(name: str) -> None:
"""
Checks if the input name is valid.
:param name: The name to be checked.
:raises ValueError: If the name contains the path separator (os.path.sep).
:raises ValueError: If the name is an empty string.
"""
if os.path.sep in name:
raise ValueError("Invalid input: String contains the path separator.")
if name == "":
raise ValueError("Invalid input: Name cannot be an empty string.")

def unpack_fs_identifier(fs_id: str, token: str) -> t.Tuple[str, str]:
"""Unpack the unformatted feature store identifier
Expand Down
1 change: 0 additions & 1 deletion smartsim/entity/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,6 @@ def attach_generator_files(
"`smartsim_params.txt` is a file automatically "
+ "generated by SmartSim and cannot be ovewritten."
)
# files is not a list of entity files
self.files = EntityFiles(to_configure, to_copy, to_symlink)

@property
Expand Down
61 changes: 51 additions & 10 deletions smartsim/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,11 @@ def start(self, *jobs: Job) -> tuple[LaunchedJobID, ...]:
jobs that can be used to query or alter the status of that
particular execution of the job.
"""
run_id = datetime.datetime.now().strftime("run-%H:%M:%ST%Y-%m-%d")
run_id = datetime.datetime.now().replace(microsecond=0).isoformat()
root = pathlib.Path(self.exp_path, run_id)
"""Create the run id for Experiment.start"""
return self._dispatch(
Generator(self.exp_path, run_id), dispatch.DEFAULT_DISPATCHER, *jobs
Generator(root), dispatch.DEFAULT_DISPATCHER, *jobs
)

def _dispatch(
Expand All @@ -196,8 +197,8 @@ def _dispatch(
) -> tuple[LaunchedJobID, ...]:
"""Dispatch a series of jobs with a particular dispatcher
:param generator: The Generator holds the run_id and experiment
path for use when producing job directories.
:param generator: The generator is responsible for creating the
job run and log directory.
:param dispatcher: The dispatcher that should be used to determine how
to start a job based on its launch settings.
:param job: The first job instance to dispatch
Expand All @@ -208,7 +209,6 @@ def _dispatch(
"""

def execute_dispatch(generator: Generator, job: Job, idx: int) -> LaunchedJobID:
print(job)
args = job.launch_settings.launch_args
env = job.launch_settings.env_vars
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Expand Down Expand Up @@ -259,18 +259,59 @@ def _generate(self, generator: Generator, job: Job, job_index: int) -> pathlib.P
An instance of ``Generator`` and ``Job`` can be passed as an argument to
the protected _generate member.
:param generator: Generator that holds the run_id and experiment
path for use when producing the job directory.
:param job: Job to generate file structure.
:returns: The generated Job path.
:param generator: The generator is responsible for creating the job run and log directory.
:param job: The job instance for which the output is generated.
:param job_index: The index of the job instance (used for naming).
:returns: The path to the generated output for the job instance.
:raises: A SmartSimError if an error occurs during the generation process.
"""
# Generate ../job_name/run directory
job_path = self._generate_job_path(job, job_index, generator.root)
# Generate ../job_name/log directory
log_path = self._generate_log_path(job, job_index, generator.root)
try:
job_path = generator.generate_job(job, job_index)
generator.generate_job(job, job_path, log_path)
return job_path
except SmartSimError as e:
logger.error(e)
raise

def _generate_job_root(self, job: Job, job_index: int, root: str) -> pathlib.Path:
"""Generates the root directory for a specific job instance.
:param job: The Job instance for which the root directory is generated.
:param job_index: The index of the Job instance (used for naming).
:returns: The path to the root directory for the Job instance.
"""
job_type = f"{job.__class__.__name__.lower()}s"
job_path = root / f"{job_type}/{job.name}-{job_index}"
job_path.mkdir(exist_ok=True, parents=True)
return job_path

def _generate_job_path(self, job: Job, job_index: int, root: str) -> pathlib.Path:
"""Generates the path for the \"run\" directory within the root directory
of a specific job instance.
:param job (Job): The job instance for which the path is generated.
:param job_index (int): The index of the job instance (used for naming).
:returns: The path to the \"run\" directory for the job instance.
"""
path = self._generate_job_root(job, job_index, root) / "run"
path.mkdir(exist_ok=False, parents=True)
return path

def _generate_log_path(self, job: Job, job_index: int, root: str) -> pathlib.Path:
"""
Generates the path for the \"log\" directory within the root directory of a specific job instance.
:param job: The job instance for which the path is generated.
:param job_index: The index of the job instance (used for naming).
:returns: The path to the \"log\" directory for the job instance.
"""
path = self._generate_job_root(job, job_index, root) / "log"
path.mkdir(exist_ok=False, parents=True)
return path

def preview(
self,
*args: t.Any,
Expand Down
15 changes: 14 additions & 1 deletion smartsim/launchable/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@
from __future__ import annotations

import typing as t
import os
from copy import deepcopy

from smartsim._core.commands.launchCommands import LaunchCommands
from smartsim.launchable.basejob import BaseJob
from smartsim.settings import LaunchSettings
from smartsim._core.utils.helpers import check_name

if t.TYPE_CHECKING:
from smartsim.entity.entity import SmartSimEntity
Expand All @@ -50,32 +52,43 @@ def __init__(
self,
entity: SmartSimEntity,
launch_settings: LaunchSettings,
name: str | None = None,
name: str | None = "job",
):
super().__init__()
self._entity = deepcopy(entity)
self._launch_settings = deepcopy(launch_settings)
check_name(name)
self._name = name if name else entity.name

@property
def name(self) -> str:
"""Retrieves the name of the Job."""
return self._name

@name.setter
def name(self, name: str) -> None:
"""Sets the name of the Job."""
check_name(name)
self._entity = name

@property
def entity(self) -> SmartSimEntity:
"""Retrieves the Job entity."""
return deepcopy(self._entity)

@entity.setter
def entity(self, value: SmartSimEntity) -> None:
"""Sets the Job entity."""
self._entity = deepcopy(value)

@property
def launch_settings(self) -> LaunchSettings:
"""Retrieves the Job LaunchSettings."""
return deepcopy(self._launch_settings)

@launch_settings.setter
def launch_settings(self, value: LaunchSettings) -> None:
"""Sets the Job LaunchSettings."""
self._launch_settings = deepcopy(value)

def get_launch_steps(self) -> LaunchCommands:
Expand Down
9 changes: 9 additions & 0 deletions smartsim/launchable/jobGroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from .basejob import BaseJob
from .baseJobGroup import BaseJobGroup

from .._core.utils.helpers import check_name

if t.TYPE_CHECKING:
from typing_extensions import Self

Expand All @@ -23,12 +25,19 @@ def __init__(
) -> None:
super().__init__()
self._jobs = deepcopy(jobs)
check_name(name)
self._name = name

@property
def name(self) -> str:
"""Retrieves the name of the JobGroup."""
return self._name

@name.setter
def name(self, name: str) -> None:
"""Sets the name of the JobGroup."""
check_name(name)
self._entity = name

@property
def jobs(self) -> t.List[BaseJob]:
Expand Down
5 changes: 1 addition & 4 deletions smartsim/settings/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@
_T_contra = t.TypeVar("_T_contra", contravariant=True)

_WorkingDirectory: TypeAlias = t.Union[str, os.PathLike[str]]
"""A type alias for a Jobs working directory. Paths may be strings or
PathLike objects.
"""
"""A working directory represented as a string or PathLike object"""

_DispatchableT = t.TypeVar("_DispatchableT", bound="LaunchArguments")
"""Any type of luanch arguments, typically used when the type bound by the type
Expand Down Expand Up @@ -453,7 +451,6 @@ class ShellLauncher:
def __init__(self) -> None:
self._launched: dict[LaunchedJobID, sp.Popen[bytes]] = {}

# TODO inject path here
def start(
self, command: tuple[str | os.PathLike[str], t.Sequence[str]]
) -> LaunchedJobID:
Expand Down
Loading

0 comments on commit f90a285

Please sign in to comment.