Source code for petab_select.problem

"""The model selection problem class."""
import abc
from functools import partial
from pathlib import Path
from typing import Any, Callable, Dict, Iterable, Optional, Union

import yaml

from .candidate_space import CandidateSpace, method_to_candidate_space_class
from .constants import (
    CANDIDATE_SPACE_ARGUMENTS,
    CRITERION,
    METHOD,
    MODEL_SPACE_FILES,
    PREDECESSOR_MODEL,
    VERSION,
    Criterion,
    Method,
)
from .model import Model, default_compare
from .model_space import ModelSpace

__all__ = [
    'Problem',
]


[docs]class Problem(abc.ABC): """Handle everything related to the model selection problem. Attributes: model_space: The model space. calibrated_models: Calibrated models. Will be used to augment the model selection problem (e.g. by excluding them from the model space). FIXME(dilpath) refactor out candidate_space_arguments: Custom options that are used to construct the candidate space. compare: A method that compares models by selection criterion. See :func:`petab_select.model.default_compare` for an example. criterion: The criterion used to compare models. method: The method used to search the model space. version: The version of the PEtab Select format. yaml_path: The location of the selection problem YAML file. Used for relative paths that exist in e.g. the model space files. TODO should the relative paths be relative to the YAML or the file that contains them? """ """ FIXME(dilpath) Unsaved attributes: candidate_space: The candidate space that will be used. Reason for not saving: Essentially reproducible from :attr:`Problem.method` and :attr:`Problem.calibrated_models`. """
[docs] def __init__( self, model_space: ModelSpace, candidate_space_arguments: Dict[str, Any] = None, compare: Callable[[Model, Model], bool] = None, criterion: Criterion = None, method: str = None, version: str = None, yaml_path: Union[Path, str] = None, ): self.model_space = model_space self.criterion = criterion self.method = method self.version = version self.yaml_path = Path(yaml_path) self.candidate_space_arguments = candidate_space_arguments if self.candidate_space_arguments is None: self.candidate_space_arguments = {} self.compare = compare if self.compare is None: self.compare = partial(default_compare, criterion=self.criterion)
[docs] def get_path(self, relative_path: Union[str, Path]) -> Path: """Get the path to a resource, from a relative path. Args: relative_path: The path to the resource, that is relative to the PEtab Select problem YAML file location. Returns: The path to the resource. """ """ TODO: Unused? """ if self.yaml_path is None: return Path(relative_path) return self.yaml_path.parent / relative_path
[docs] def exclude_models( self, models: Iterable[Model], ) -> None: """Exclude models from the model space. Args: models: The models. """ self.model_space.exclude_models(models)
[docs] def exclude_model_hashes( self, model_hashes: Iterable[str], ) -> None: """Exclude models from the model space, by model hashes. Args: model_hashes: The model hashes. """ self.model_space.exclude_model_hashes(model_hashes)
[docs] @staticmethod def from_yaml( yaml_path: Union[str, Path], ) -> 'Problem': """Generate a problem from a PEtab Select problem YAML file. Args: yaml_path: The location of the PEtab Select problem YAML file. Returns: A `Problem` instance. """ yaml_path = Path(yaml_path) with open(yaml_path, 'r') as f: problem_specification = yaml.safe_load(f) if not problem_specification.get(MODEL_SPACE_FILES, []): raise KeyError( 'The model selection problem specification file is missing ' 'model space files.' ) model_space = ModelSpace.from_files( # problem_specification[MODEL_SPACE_FILES], [ # `pathlib.Path` appears to handle absolute `model_space_file` paths # correctly, even if used as a relative path. # TODO test # This is similar to the `Problem.get_path` method. yaml_path.parent / model_space_file for model_space_file in problem_specification[ MODEL_SPACE_FILES ] ], # source_path=yaml_path.parent, ) criterion = problem_specification.get(CRITERION, None) if criterion is not None: criterion = Criterion(criterion) candidate_space_arguments = problem_specification.get( CANDIDATE_SPACE_ARGUMENTS, None, ) if candidate_space_arguments is not None: if PREDECESSOR_MODEL in candidate_space_arguments: candidate_space_arguments[PREDECESSOR_MODEL] = ( yaml_path.parent / candidate_space_arguments[PREDECESSOR_MODEL] ) return Problem( model_space=model_space, candidate_space_arguments=candidate_space_arguments, criterion=criterion, # TODO refactor method to use enum method=problem_specification.get(METHOD, None), version=problem_specification.get(VERSION, None), yaml_path=yaml_path, )
[docs] def get_best( self, models: Optional[Iterable[Model]], criterion: Optional[Union[str, None]] = None, compute_criterion: bool = False, ) -> Model: """Get the best model from a collection of models. The best model is selected based on the selection problem's criterion. Args: models: The best model will be taken from these models. criterion: The criterion by which models will be compared. Defaults to ``self.criterion`` (e.g. as defined in the PEtab Select problem YAML file). compute_criterion: Whether to try computing criterion values, if sufficient information is available (e.g., likelihood and number of parameters, to compute AIC). Returns: The best model. """ if criterion is None: criterion = self.criterion best_model = None for model in models: if compute_criterion and not model.has_criterion(criterion): model.get_criterion(criterion) if best_model is None: if model.has_criterion(criterion): best_model = model # TODO warn if criterion is not available? continue if self.compare(best_model, model, criterion=criterion): best_model = model if best_model is None: raise KeyError( f'None of the supplied models have a value set for the criterion {criterion}.' ) return best_model
[docs] def new_candidate_space( self, *args, method: Method = None, **kwargs, ) -> CandidateSpace: """Construct a new candidate space. Args: args, kwargs: Arguments are passed to the candidate space constructor. method: The model selection method. """ if method is None: method = self.method candidate_space_class = method_to_candidate_space_class(method) candidate_space_arguments = ( candidate_space_class.read_arguments_from_yaml_dict( self.candidate_space_arguments ) ) candidate_space_kwargs = { **candidate_space_arguments, **kwargs, } candidate_space = candidate_space_class( *args, **candidate_space_kwargs, ) return candidate_space