diff --git a/coupledmodeldriver/client/check_completion.py b/coupledmodeldriver/client/check_completion.py index c4df124c..d3380992 100644 --- a/coupledmodeldriver/client/check_completion.py +++ b/coupledmodeldriver/client/check_completion.py @@ -12,7 +12,7 @@ from coupledmodeldriver.generate.adcirc.check import ( check_adcirc_completion, CompletionStatus, - is_adcirc_run_directory, + is_adcirc_directory, ) from coupledmodeldriver.utilities import ProcessPoolExecutorStackTraced @@ -59,7 +59,7 @@ def is_model_directory(directory: PathLike, model: ModelJSON = None) -> bool: model = ADCIRCJSON if model == ADCIRCJSON: - is_model_directory = is_adcirc_run_directory(directory) + is_model_directory = is_adcirc_directory(directory) else: raise NotImplementedError(f'model "{model}" not implemented') diff --git a/coupledmodeldriver/client/initialize_adcirc.py b/coupledmodeldriver/client/initialize_adcirc.py index 4bcb7711..03add37e 100644 --- a/coupledmodeldriver/client/initialize_adcirc.py +++ b/coupledmodeldriver/client/initialize_adcirc.py @@ -343,7 +343,7 @@ def initialize_adcirc( absolute_paths: bool = True, overwrite: bool = None, verbose: bool = False, -): +) -> ADCIRCRunConfiguration: """ creates a set of JSON configuration files according to the given parameters @@ -445,6 +445,8 @@ def initialize_adcirc( ) generation_job_script.write(filename=output_directory / 'generate.job', overwrite=True) + return configuration + def get_argument( argument: str, diff --git a/coupledmodeldriver/client/initialize_from_configuration.py b/coupledmodeldriver/client/initialize_from_configuration.py new file mode 100644 index 00000000..db6d99ff --- /dev/null +++ b/coupledmodeldriver/client/initialize_from_configuration.py @@ -0,0 +1,80 @@ +from argparse import ArgumentParser +from os import PathLike +from pathlib import Path + +from typepigeon import convert_value + +from coupledmodeldriver.configure import ModelJSON +from coupledmodeldriver.configure.configure import RunConfiguration +from coupledmodeldriver.generate import ADCIRCRunConfiguration +from coupledmodeldriver.generate.adcirc.base import ADCIRCJSON + +MODELS = {model.name.lower(): model for model in ModelJSON.__subclasses__()} + + +def parse_initialize_from_configuration_arguments(): + argument_parser = ArgumentParser() + argument_parser.add_argument( + 'input-directory', + nargs='*', + default=Path.cwd(), + help='directory containing model configuration', + ) + argument_parser.add_argument( + 'output-directory', + nargs='*', + default=Path.cwd(), + help='directory to write JSON configuration', + ) + argument_parser.add_argument('--model', help='model of configuraiton, one of: `ADCIRC`') + argument_parser.add_argument( + '--skip-existing', action='store_true', help='skip existing files', + ) + + arguments = argument_parser.parse_args() + + input_directory = convert_value(arguments.input_directory, Path) + output_directory = convert_value(arguments.output_directory, Path) + + model = arguments.model + if model is not None: + model = MODELS[model.lower()] + + overwrite = not arguments.skip_existing + + return { + 'input_directory': input_directory, + 'output_directory': output_directory, + 'model': model, + 'overwrite': overwrite, + } + + +def initialize_from_model_configuration_directory( + directory: PathLike, model: ModelJSON = None +) -> RunConfiguration: + if model is None: + model = ADCIRCJSON + + if model == ADCIRCJSON: + run_configuration = ADCIRCRunConfiguration.from_model_configuration_directory( + directory=directory + ) + else: + raise NotImplementedError(f'model "{model}" not implemented') + + return run_configuration + + +def main(): + arguments = parse_initialize_from_configuration_arguments() + run_configuration = initialize_from_model_configuration_directory( + directory=arguments['input_directory'], model=arguments['model'], + ) + run_configuration.write_directory( + directory=arguments['output_directory'], overwrite=arguments['overwrite'], + ) + + +if __name__ == '__main__': + main() diff --git a/coupledmodeldriver/configure/configure.py b/coupledmodeldriver/configure/configure.py index 923c1721..7590eea5 100644 --- a/coupledmodeldriver/configure/configure.py +++ b/coupledmodeldriver/configure/configure.py @@ -1,15 +1,15 @@ -from abc import ABC from copy import copy from os import PathLike from pathlib import Path from typing import Any, Collection, Dict, List, Mapping, Set, Union +from coupledmodeldriver.configure import ConfigurationJSON from coupledmodeldriver.configure.base import ConfigurationJSON, ModelDriverJSON from coupledmodeldriver.configure.forcings.base import ADCIRCPY_FORCING_CLASSES, ForcingJSON from coupledmodeldriver.utilities import LOGGER -class RunConfiguration(ABC): +class RunConfiguration: """ abstraction of a set of model run configurations, encapsulated in JSON files """ @@ -150,6 +150,10 @@ def from_configurations( ) -> 'RunConfiguration': return cls(configurations) + @classmethod + def from_model_configuration_directory(cls, directory: PathLike) -> 'RunConfiguration': + raise NotImplementedError + def from_user_input(value: Any) -> ConfigurationJSON: """ diff --git a/coupledmodeldriver/generate/adcirc/base.py b/coupledmodeldriver/generate/adcirc/base.py index 747ab913..3c41b825 100644 --- a/coupledmodeldriver/generate/adcirc/base.py +++ b/coupledmodeldriver/generate/adcirc/base.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import Any, Dict, List, Union -from adcircpy import AdcircMesh, AdcircRun, Tides +from adcircpy import AdcircMesh, AdcircRun, Fort15, Tides from adcircpy.forcing import BestTrackForcing from adcircpy.forcing.base import Forcing from adcircpy.mesh.fort13 import NodalAttributes @@ -529,3 +529,7 @@ def __copy__(self) -> 'ADCIRCJSON': instance.base_mesh = self.base_mesh instance.forcings = self.forcings return instance + + @classmethod + def from_fort15(cls, filename: PathLike): + Fort15() diff --git a/coupledmodeldriver/generate/adcirc/check.py b/coupledmodeldriver/generate/adcirc/check.py index 27dcaca1..4988701f 100644 --- a/coupledmodeldriver/generate/adcirc/check.py +++ b/coupledmodeldriver/generate/adcirc/check.py @@ -21,7 +21,7 @@ class CompletionStatus(Enum): COMPLETED = 0 -def is_adcirc_run_directory(directory: PathLike = None) -> bool: +def is_adcirc_directory(directory: PathLike = None) -> bool: """ check if the given directory has the baseline ADCIRC configuration files @@ -63,7 +63,7 @@ def check_adcirc_completion( completion = {entry: {} for entry in CompletionStatus} - if not is_adcirc_run_directory(directory): + if not is_adcirc_directory(directory): completion[CompletionStatus.NOT_CONFIGURED][ 'fort.14,fort.15' ] = f'ADCIRC configuration files not found' diff --git a/coupledmodeldriver/generate/adcirc/configure.py b/coupledmodeldriver/generate/adcirc/configure.py index 41cfc441..ad2b6a14 100644 --- a/coupledmodeldriver/generate/adcirc/configure.py +++ b/coupledmodeldriver/generate/adcirc/configure.py @@ -25,6 +25,7 @@ WW3DATAForcingJSON, ) from coupledmodeldriver.generate.adcirc.base import ADCIRCJSON +from coupledmodeldriver.generate.adcirc.check import is_adcirc_directory from coupledmodeldriver.platforms import Platform from coupledmodeldriver.utilities import LOGGER @@ -267,6 +268,36 @@ def read_directory( return super().read_directory(directory, required, supplementary) + @classmethod + def from_model_configuration_directory(cls, directory: PathLike) -> 'RunConfiguration': + if not isinstance(directory, Path): + directory = Path(directory) + + configurations = [] + + spinup_directory = directory / 'spinup' + if spinup_directory.exists(): + if not is_adcirc_directory(spinup_directory): + raise FileNotFoundError(f'not an ADCIRC directory: "{spinup_directory}"') + + configurations.append(ADCIRCJSON.from_fort15(spinup_directory / 'fort.15')) + + runs_directory = directory / 'runs' + if runs_directory.exists(): + perturbations = {} + for run_directory in runs_directory.iterdir(): + pass + + if not is_adcirc_directory(directory): + raise FileNotFoundError('not an ADCIRC directory') + + required = {configuration_class: None for configuration_class in cls.REQUIRED} + supplementary = { + configuration_class: None for configuration_class in cls.SUPPLEMENTARY + } + + return cls() + class NEMSADCIRCRunConfiguration(ADCIRCRunConfiguration): """ @@ -274,10 +305,8 @@ class NEMSADCIRCRunConfiguration(ADCIRCRunConfiguration): """ REQUIRED = { - ModelDriverJSON, + *ADCIRCRunConfiguration.REQUIRED, NEMSJSON, - SlurmJSON, - ADCIRCJSON, } def __init__( diff --git a/tests/test_generation.py b/tests/test_generation.py index 9bf424f7..c2942b01 100644 --- a/tests/test_generation.py +++ b/tests/test_generation.py @@ -9,7 +9,11 @@ from coupledmodeldriver import Platform from coupledmodeldriver.client.initialize_adcirc import initialize_adcirc -from coupledmodeldriver.generate import generate_adcirc_configuration +from coupledmodeldriver.generate import ( + ADCIRCRunConfiguration, + generate_adcirc_configuration, + NEMSADCIRCRunConfiguration, +) from tests import ( check_reference_directory, INPUT_DIRECTORY, @@ -902,3 +906,126 @@ def test_stampede2_adcirc_tidal_nems_atmesh_ww3data(): 'nems.configure': [0], }, ) + + +def test_adcirc_run_configuration(): + output_directory = OUTPUT_DIRECTORY / 'test_adcirc_run_configuration' + + platform = Platform.HERA + mesh = 'shinnecock' + adcirc_processors = 15 * platform.value['processors_per_node'] + modeled_start_time = datetime(2008, 8, 23) + modeled_duration = timedelta(days=14.5) + modeled_timestep = timedelta(seconds=2) + tidal_spinup_duration = timedelta(days=12.5) + job_duration = timedelta(hours=6) + + mesh_directory = INPUT_DIRECTORY / 'meshes' / mesh + + tidal_forcing = Tides(tidal_source=TidalSource.HAMTIDE) + tidal_forcing.use_all() + forcings = [tidal_forcing] + + configuration = initialize_adcirc( + platform=platform, + mesh_directory=mesh_directory, + modeled_start_time=modeled_start_time, + modeled_duration=modeled_duration, + modeled_timestep=modeled_timestep, + tidal_spinup_duration=tidal_spinup_duration, + perturbations=None, + nems_interval=None, + nems_connections=None, + nems_mediations=None, + nems_sequence=None, + modulefile=INPUT_DIRECTORY / 'modulefiles' / 'envmodules_intel.hera', + forcings=forcings, + adcirc_executable=INPUT_DIRECTORY / 'bin' / 'padcirc', + adcprep_executable=INPUT_DIRECTORY / 'bin' / 'adcprep', + aswip_executable=None, + adcirc_processors=adcirc_processors, + job_duration=job_duration, + output_directory=output_directory, + absolute_paths=False, + overwrite=True, + verbose=False, + ) + generate_adcirc_configuration(output_directory, relative_paths=True, overwrite=True) + + parsed_configuration = ADCIRCRunConfiguration.from_model_configuration_directory( + output_directory + ) + + assert parsed_configuration == configuration + + +def test_nems_adcirc_run_configuration(): + output_directory = OUTPUT_DIRECTORY / 'test_nems_adcirc_run_configuration' + + platform = Platform.HERA + mesh = 'shinnecock' + storm = 'ike' + adcirc_processors = 15 * platform.value['processors_per_node'] + modeled_start_time = datetime(2008, 8, 23) + modeled_duration = timedelta(days=14.5) + modeled_timestep = timedelta(seconds=2) + tidal_spinup_duration = None + nems_interval = timedelta(hours=1) + job_duration = timedelta(hours=6) + + mesh_directory = INPUT_DIRECTORY / 'meshes' / mesh + forcings_directory = INPUT_DIRECTORY / 'forcings' / storm + + nems_connections = ['ATM -> OCN', 'WAV -> OCN'] + nems_mediations = None + nems_sequence = [ + 'ATM -> OCN', + 'WAV -> OCN', + 'ATM', + 'WAV', + 'OCN', + ] + + wind_forcing = AtmosphericMeshForcing( + filename=forcings_directory / 'wind_atm_fin_ch_time_vec.nc', + nws=17, + interval_seconds=3600, + ) + wave_forcing = WaveWatch3DataForcing( + filename=forcings_directory / 'ww3.Constant.20151214_sxy_ike_date.nc', + nrs=5, + interval_seconds=3600, + ) + forcings = [wind_forcing, wave_forcing] + + configuration = initialize_adcirc( + platform=platform, + mesh_directory=mesh_directory, + modeled_start_time=modeled_start_time, + modeled_duration=modeled_duration, + modeled_timestep=modeled_timestep, + tidal_spinup_duration=tidal_spinup_duration, + perturbations=None, + nems_interval=nems_interval, + nems_connections=nems_connections, + nems_mediations=nems_mediations, + nems_sequence=nems_sequence, + modulefile=INPUT_DIRECTORY / 'modulefiles' / 'envmodules_intel.hera', + forcings=forcings, + adcirc_executable=INPUT_DIRECTORY / 'bin' / 'NEMS.x', + adcprep_executable=INPUT_DIRECTORY / 'bin' / 'adcprep', + aswip_executable=None, + adcirc_processors=adcirc_processors, + job_duration=job_duration, + output_directory=output_directory, + absolute_paths=False, + overwrite=True, + verbose=False, + ) + generate_adcirc_configuration(output_directory, relative_paths=True, overwrite=True) + + parsed_configuration = NEMSADCIRCRunConfiguration.from_model_configuration_directory( + output_directory + ) + + assert parsed_configuration == configuration