copied from preCICE tutorials

This commit is contained in:
jakob.schratter 2026-01-26 16:14:46 +01:00
commit 3f1b1a6d0f
68 changed files with 156449 additions and 0 deletions

View file

@ -0,0 +1,443 @@
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Tuple, Optional, Dict
import glob
import yaml
import itertools
from paths import PRECICE_TESTS_DIR, PRECICE_TUTORIAL_DIR
@dataclass
class BuildArgument:
"""Represents a BuildArgument needed to run the docker container"""
description: str
"""The description of the parameter."""
key: str
"""The name of the parameter."""
value_options: Optional[list] = None
"""The optinal list of value options for the parameter. If none is suplied all values are accepted"""
default: Optional[str] = None
"""The default value for the parameter."""
@property
def required(self) -> bool:
"""
Check if the BuildArgument need to be supplied via CommandLineArgs
Returns:
bool: True if the parameter is required, False otherwise.
"""
return False if self.default else True
def __eq__(self, other) -> bool:
if isinstance(other, BuildArgument):
return self.key == other.key
return False
def __hash__(self) -> int:
return hash(self.key)
def __repr__(self) -> str:
return f"{self.key}"
class BuildArguments:
"""Represents a collection of build_arguments used to built the docker images."""
def __init__(self, arguments: List[BuildArgument]):
self.arguments = arguments
@classmethod
def from_components_yaml(cls, data):
"""
Create a list of Paramters from the components YAML data.
Args:
data: The components YAML data.
"""
arguments = []
for argument_name, argument_dict in data['build_arguments'].items():
# TODO maybe **params
description = argument_dict.get(
'description', f"No description provided for {argument_name}")
key = argument_name
default = argument_dict.get('default', None)
value_options = argument_dict.get('value_options', None)
arguments.append(BuildArgument(
description, key, value_options, default))
return cls(arguments)
def __iter__(self):
return iter(self.arguments)
def __getitem__(self, index):
return self.arguments[index]
def __setitem__(self, index, value):
self.arguments[index] = value
def __len__(self):
return len(self.arguments)
def __repr__(self) -> str:
return f"{self.arguments}"
@dataclass
class Component:
"""
Represents a component like e.g the openfoam-adapter
"""
name: str
template: str
repository: str
parameters: BuildArguments
def __eq__(self, other):
if isinstance(other, Component):
return self.name == other.name
return False
def __repr__(self) -> str:
return f"{self.name}"
class Components(list):
"""
Represents the collection of components read in from the components.yaml
"""
def __init__(self, components: List[Component]):
self.components = components
@classmethod
def from_yaml(cls, path):
"""
Creates a Components instance from a YAML file.
Args:
path: The path to the YAML file.
Returns:
An instance of Components.
"""
components = []
with open(path, 'r') as f:
data = yaml.safe_load(f)
for component_name in data:
parameters = BuildArguments.from_components_yaml(
data[component_name])
repository = data[component_name]["repository"]
template = data[component_name]["template"]
components.append(
Component(component_name, template, repository, parameters))
return cls(components)
def __iter__(self):
return iter(self.components)
def __getitem__(self, index):
return self.components[index]
def __setitem__(self, index, value):
self.components[index] = value
def __len__(self):
return len(self.components)
def get_by_name(self, name_to_search):
"""
Retrieves a component by its name.
Args:
name_to_search: The name of the component to search for.
Returns:
The component with the specified name, or None if not found.
"""
for component in self.components:
if component.name == name_to_search:
return component
return None
@dataclass
class Participant:
"""Represents a participant in a coupled simulation"""
name: str
"""The name of the participant."""
def __eq__(self, other) -> bool:
if isinstance(other, Participant):
return self.name == other.name
return False
def __repr__(self) -> str:
return f"{self.name}"
# Forward declaration of tutorial
class Tutorial:
pass
@dataclass
class Case:
"""
Represents a case inside of a tutorial.
"""
name: str
participant: str
path: Path
run_cmd: str
tutorial: Tutorial = field(init=False)
component: Component
def __post_init__(self):
"""
Performs sanity checks after initializing the Case instance.
"""
if not self.component:
raise Exception(
f'Tried to instantiate the case {self.name} but failed. Reason: Could not find the component it uses in the components.yaml file.')
@classmethod
def from_dict(cls, name, dict, available_components):
"""
Creates a Case instance from a the tutorial yaml dict.
Args:
name: The name of the case.
dict: The dictionary containing the case data.
available_components: Components read from the components.yaml file
Returns:
An instance of the Case but without the tutorial set, this needs to be done later
"""
participant = dict["participant"]
path = Path(dict["directory"])
run_cmd = dict["run"]
component = available_components.get_by_name(dict["component"])
return cls(name, participant, path, run_cmd, component)
def __repr__(self) -> str:
return f"{self.name}"
def __hash__(self) -> int:
return hash(f"{self.name,self.participant,self.component,self.tutorial}")
def __eq__(self, other) -> bool:
if isinstance(other, Case):
return (
self.name == other.name) and (
self.participant == other.participant) and (
self.component == other.component) and (
self.tutorial == other.tutorial)
return False
@dataclass
class CaseCombination:
"""Represents a case combination able to run the tutorial"""
cases: Tuple[Case]
tutorial: Tutorial
def __eq__(self, other) -> bool:
if isinstance(other, CaseCombination):
return set(self.cases) == set(other.cases)
return False
def __repr__(self) -> str:
return f"{self.cases}"
@classmethod
def from_string_list(cls, case_names: List[str], tutorial: Tutorial):
cases = []
for case_name in case_names:
cases.append(tutorial.get_case_by_string(case_name))
return cls(tuple(cases), tutorial)
@classmethod
def from_cases_tuple(cls, cases: Tuple[Case], tutorial: Tutorial):
return cls(cases, tutorial)
@dataclass
class ReferenceResult:
path: Path
case_combination: CaseCombination
def __repr__(self) -> str:
return f"{self.path.as_posix()}"
def __post_init__(self):
# built full path
self.path = PRECICE_TUTORIAL_DIR / self.path
@dataclass
class Tutorial:
"""
Represents a tutorial with various attributes and methods.
"""
name: str
path: Path
url: str
participants: List[str]
cases: List[Case]
case_combinations: List[CaseCombination] = field(init=False)
def __post_init__(self):
for case in self.cases:
case.tutorial = self
# get all case combinations
def get_all_possible_case_combinations(tutorial: Tutorial):
case_combinations = []
cases_dict = {}
for participant in tutorial.participants:
cases_dict[participant] = []
for case in tutorial.cases:
cases_dict[case.participant].append(case)
for combination in itertools.product(*[cases_dict[participant] for participant in tutorial.participants]):
case_combinations.append(CaseCombination.from_cases_tuple(combination, self))
return case_combinations
self.case_combinations = get_all_possible_case_combinations(self)
def __eq__(self, other) -> bool:
if isinstance(other, Tutorial):
return (self.name == other.name) and (self.path == other.path)
return False
def __hash__(self) -> int:
return hash(self.path)
def __repr__(self) -> str:
"""
Returns a string representation of the Tutorial.
"""
return f"""\n{self.name}:
Path: {self.path}
URL: {self.url}
Participants: {self.participants}
Cases: {self.cases}
"""
def get_case_by_string(self, case_name: str) -> Optional[Case]:
"""
Retrieves Optional case based on the case_name
Args:
case_name: the name of the case in search
Returns:
Either None or a Case mathing the casename
"""
for case in self.cases:
if case.name == case_name:
return case
return None
@classmethod
def from_yaml(cls, path, available_components):
"""
Creates a Tutorial instance from a YAML file.
Args:
path: The path to the YAML file.
available_components: The Components instance containing available components.
Returns:
An instance of Tutorial.
"""
with open(path, 'r') as f:
data = yaml.safe_load(f)
name = data['name']
path = PRECICE_TUTORIAL_DIR / data['path']
url = data['url']
participants = data.get('participants', [])
cases_raw = data.get('cases', {})
cases = []
for case_name in cases_raw.keys():
cases.append(Case.from_dict(
case_name, cases_raw[case_name], available_components))
return cls(name, path, url, participants, cases)
class Tutorials(list):
"""
Represents a collection of tutorials.
"""
def __iter__(self):
return iter(self.tutorials)
def __getitem__(self, index):
return self.tutorials[index]
def __setitem__(self, index, value):
self.tutorials[index] = value
def __len__(self):
return len(self.tutorials)
def __init__(self, tutorials: List[Tutorial]):
"""
Initializes the Tutorials instance with a base path and a list of tutorials.
Args:
path: The path to the folder containing the tutorial folders.
tutorials: The list of tutorials.
"""
self.tutorials = tutorials
def get_by_path(self, relative_path: str) -> Optional[Tutorial]:
"""
Retrieves a Tutorial by its relative path.
Args:
path_to_search: The path of the Tutorial to search for.
Returns:
The Tutorial with the specified path, or None if not found.
"""
for tutorial in self.tutorials:
if tutorial.path.name == relative_path:
return tutorial
return None
@classmethod
def from_path(cls, path):
"""
Read ins all the metadata.yaml files available in path/*/metadata.yaml
Args:
path: The path containing the tutorial folders
"""
yaml_files = glob.glob(f'{path}/*/metadata.yaml')
tutorials = []
available_components = Components.from_yaml(
PRECICE_TESTS_DIR / "components.yaml")
for yaml_path in yaml_files:
tut = Tutorial.from_yaml(yaml_path, available_components)
tutorials.append(tut)
return cls(tutorials)