From 5c4ab56e44e04e0d6e6646c42efc219f9f0447ce Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Fri, 8 May 2026 09:16:13 +0200 Subject: [PATCH] Allow passing selected model to Configuration.check_consistent --- ymmsl/v0_2/configuration.py | 34 +++++++++++------ ymmsl/v0_2/tests/conftest.py | 51 ++++++++++++++++++++++++-- ymmsl/v0_2/tests/test_configuration.py | 10 +++++ 3 files changed, 80 insertions(+), 15 deletions(-) diff --git a/ymmsl/v0_2/configuration.py b/ymmsl/v0_2/configuration.py index d6a0d96..accc247 100644 --- a/ymmsl/v0_2/configuration.py +++ b/ymmsl/v0_2/configuration.py @@ -184,7 +184,9 @@ def update(self, overlay: 'Configuration') -> None: self.checkpoints.update(overlay.checkpoints) self.resume.update(overlay.resume) - def check_consistent(self, check_runnable: bool = True) -> None: + def check_consistent( + self, check_runnable: bool = True, selected_model: Optional[str] = None + ) -> None: """Checks that the configuration is internally consistent. This checks: @@ -204,6 +206,9 @@ def check_consistent(self, check_runnable: bool = True) -> None: Args: check_runnable: if False, skip the checks for whether component implementations exist and whether resources have been requested. + selected_model: if set, gives the name of the root model that we intend to + run, and which must therefore have resources defined. Only used if + check_runnable is True. """ errors = list() @@ -221,14 +226,14 @@ def check_consistent(self, check_runnable: bool = True) -> None: errors.extend(self._check_custom_implementations(component_paths)) errors.extend(self._check_consistent_settings(component_paths)) if check_runnable: - errors.extend(self._check_resources(component_paths)) + errors.extend(self._check_resources(component_paths, selected_model)) if errors: raise RuntimeError( 'The configuration is internally inconsistent. The following' ' problems were found:\n- ' + '\n- '.join(errors)) - + def get_resources(self, name: Reference) -> ResourceRequirements: """Get the resource requirements for a component. @@ -325,7 +330,7 @@ def _component_paths(self) -> Dict[Reference, Component]: """ result = dict() queue: List[Tuple[Model, Reference, List[Tuple[Reference, Reference]]]] = \ - [(m, Reference([]), []) for m in self._root_models()] + [(m, m.name, []) for m in self._root_models()] _logger.debug(f'cmp_paths: initial queue: {[t[0].name for t in queue]}') while queue: @@ -512,7 +517,7 @@ def _check_supported_setting( dims = component.multiplicity for index in itertools.product(*map(range, dims)): - instance_path = component_path + index + instance_path = component_path[1:] + index for j in range(len(instance_path), -1, -1): found_setting = instance_path[:j] + name if found_setting in self.settings: @@ -523,7 +528,8 @@ def _check_supported_setting( val_str = f'"{val}"' errors.append( f'Instance "{instance_path}" of component' - f' "{component_path}" with implementation "{impl.name}"' + f' "{component_path[1:]}" with implementation' + f' "{impl.name}"' f' has a supported setting "{name}" with type' f' {typ.value}, but setting "{found_setting}" has value' f' {val_str}, which does not match that type') @@ -555,7 +561,8 @@ def _setting_type_matches(self, value: SettingValue, typ: SettingType) -> bool: return False def _check_resources( - self, component_paths: Dict[Reference, Component]) -> List[str]: + self, component_paths: Dict[Reference, Component], + selected_model: Optional[str]) -> List[str]: """Check that each component path has a corresponding resource request. For non-MPI components, resources are optional: if not specified, @@ -566,6 +573,9 @@ def _check_resources( errors = list() for path, component in component_paths.items(): _logger.debug(f'Checking resources for {path} {component.name}') + if selected_model is not None and path[0] != selected_model: + continue + impl_ref = self.custom_implementations.get(path, component.implementation) _logger.debug(f'Implementation: {impl_ref}') if impl_ref is None or impl_ref not in self.programs: @@ -578,21 +588,23 @@ def _check_resources( em_nompi = impl.execution_model is ExecutionModel.DIRECT - if path not in self.resources: + # can become just path again when we prefix resources with the model name + if path[1:] not in self.resources: if em_mpi: errors.append(f'Component "{path}" is missing a resource request') else: + # also here res_mpi = isinstance( - self.resources[path], (MPICoresResReq, MPINodesResReq)) + self.resources[path[1:]], (MPICoresResReq, MPINodesResReq)) if em_mpi and not res_mpi: errors.append( - f'Component "{path}" has implementation "{impl_ref}",' + f'Component "{path[1:]}" has implementation "{impl_ref}",' ' which has an MPI execution model, but the resources' ' requested for it do not ask for MPI processes.') elif res_mpi and em_nompi: errors.append( - f'Component "{path}" has implementation "{impl_ref}",' + f'Component "{path[1:]}" has implementation "{impl_ref}",' ' which has a non-MPI execution model, but the resources' ' requested for it ask for MPI processes.') diff --git a/ymmsl/v0_2/tests/conftest.py b/ymmsl/v0_2/tests/conftest.py index dd7eefb..9d49444 100644 --- a/ymmsl/v0_2/tests/conftest.py +++ b/ymmsl/v0_2/tests/conftest.py @@ -464,8 +464,8 @@ def config_consistent_custom_impls() -> Configuration: return Configuration( 'testing consistency of custom implementations', None, [model1, model2], { - Reference('c1'): Reference('program1'), - Reference('c2.init_model'): Reference('initer2')}, + Reference('test_model.c1'): Reference('program1'), + Reference('test_model.c2.init_model'): Reference('initer2')}, None, programs, resources) @@ -488,8 +488,8 @@ def config_inconsistent_custom_impls() -> Configuration: return Configuration( 'testing consistency of custom implementations', None, [model1, model2], { - Reference('cl'): Reference('program1'), - Reference('c2.init_model'): Reference('initer2')}, + Reference('test_model.cl'): Reference('program1'), + Reference('test_model.c2.init_model'): Reference('initer2')}, None, None, resources) @@ -619,6 +619,49 @@ def config_inconsistent_resources() -> Configuration: 'test_config', None, [model1, model2], None, None, programs, resources) +@pytest.fixture +def config_consistent_partial_resources() -> Configuration: + model1 = Model( + 'resources_test', + None, + 'description', + None, + [ + Component('singlethreaded', Ports(), 'description', 'a'), + Component('multithreaded', Ports(), 'description', 'b'), + ]) + + model2 = Model( + 'resources_test2', + None, + 'description', + None, + [ + Component('mpi_cores1', Ports(), 'description', 'c'), + Component('mpi_cores2', Ports(), 'description', 'd'), + Component('mpi_nodes1', Ports(), 'description', 'c'), + Component('mpi_nodes2', Ports(), 'description', 'd')], + []) + + em = { + 'a': ExecutionModel.DIRECT, + 'b': ExecutionModel.DIRECT, + 'c': ExecutionModel.OPENMPI, + 'd': ExecutionModel.OPENMPI} + programs = [Program(x, script='script', execution_model=em[x]) for x in 'abcd'] + + resources = [ + ThreadedResReq(Reference('singlethreaded'), 1), + ThreadedResReq(Reference('multithreaded'), 4), + MPICoresResReq(Reference('mpi_cores1'), 16), + MPINodesResReq(Reference('mpi_nodes1'), 10, 16), + MPINodesResReq(Reference('mpi_nodes2'), 10, 4, 4)] + + return Configuration( + 'consistent_resources', None, [model1, model2], None, None, programs, + resources) + + @pytest.fixture def config_component_loop() -> Configuration: main = Model( diff --git a/ymmsl/v0_2/tests/test_configuration.py b/ymmsl/v0_2/tests/test_configuration.py index 9a0d002..c8cff83 100644 --- a/ymmsl/v0_2/tests/test_configuration.py +++ b/ymmsl/v0_2/tests/test_configuration.py @@ -461,6 +461,16 @@ def test_check_inconsistent_resources( config_inconsistent_resources.check_consistent(False) +def test_check_partial_resources( + config_consistent_partial_resources: Configuration) -> None: + config_consistent_partial_resources.check_consistent(True, 'resources_test') + + with pytest.raises(RuntimeError) as e: + config_consistent_partial_resources.check_consistent(True, 'resources_test2') + + assert len(str(e.value).split('\n')) == 2 + + def test_get_resources_defined() -> None: """Test that get_resources returns the defined resource for a component.""" resources = [