Source code for pharmpy.model

"""
===================
Generic Model class
===================

**Base class of all implementations.**

Inherit to *implement*, i.e. to define support for a specific model type.

Definitions
-----------
"""

import copy
import io
import pathlib
import warnings
from pathlib import Path

import sympy

from pharmpy.datainfo import ColumnInfo, DataInfo
from pharmpy.estimation import EstimationSteps
from pharmpy.parameters import Parameters
from pharmpy.plugins.utils import detect_model
from pharmpy.random_variables import RandomVariables
from pharmpy.statements import Statements


[docs]class ModelError(Exception): """Exception for errors in model object""" pass
[docs]class ModelSyntaxError(ModelError): """Exception for Syntax errors in model code""" def __init__(self, msg='model syntax error'): super().__init__(msg)
[docs]class ModelfitResultsError(ModelError): """Exception for issues with ModelfitResults""" pass
[docs]class Model: """The Pharmpy model class""" def __init__(self): self.parameters = Parameters([]) self.random_variables = RandomVariables([]) self.statements = Statements() self.dependent_variable = sympy.Symbol('y') self.observation_transformation = self.dependent_variable self.modelfit_results = None self.parent_model = None self.initial_individual_estimates = None self.value_type = 'PREDICTION' self.description = '' def __eq__(self, other): """Compare two models for equality Tests whether a model is equal to another model. This ignores implementation-specific details such as NONMEM $DATA and FILE pointers, or certain $TABLE printing options. Parameters ---------- other : Model Other model to compare this one with Examples -------- >>> from pharmpy import Model >>> from pharmpy.modeling import load_example_model >>> a = load_example_model("pheno") >>> a == a True >>> a == 0 Traceback (most recent call last): ... NotImplementedError: Cannot compare Model with <class 'int'> >>> a == None Traceback (most recent call last): ... NotImplementedError: Cannot compare Model with <class 'NoneType'> >>> b = load_example_model("pheno") >>> b == a True >>> a.name = 'a' >>> b.name = 'b' >>> a == b True """ if self is other: return True if not isinstance(other, Model): raise NotImplementedError(f'Cannot compare Model with {type(other)}') if self.parameters != other.parameters: return False if self.random_variables != other.random_variables: return False if self.statements != other.statements: return False if self.dependent_variable != other.dependent_variable: return False if self.observation_transformation != other.observation_transformation: return False if self.estimation_steps != other.estimation_steps: return False if self.initial_individual_estimates != other.initial_individual_estimates: return False if self.datainfo != other.datainfo: return False if self.value_type != other.value_type: return False return True def __repr__(self): return f'<Pharmpy model object {self.name}>' def _repr_html_(self): stat = self.statements._repr_html_() rvs = self.random_variables._repr_latex_() return f'<hr>{stat}<hr>${rvs}$<hr>{self.parameters._repr_html_()}<hr>' @property def name(self): """Name of the model""" return self._name @name.setter def name(self, value): if not isinstance(value, str): raise TypeError("Name of a model has to be of string type") self._name = value @property def filename_extension(self): """Filename extension of model file""" return self._filename_extension @filename_extension.setter def filename_extension(self, value): if not isinstance(value, str): raise TypeError("Filename extension has to be of string type") self._filename_extension = value @property def dependent_variable(self): """The model dependent variable, i.e. y""" return self._dependent_variable @dependent_variable.setter def dependent_variable(self, value): self._dependent_variable = value @property def value_type(self): """The type of the model value (dependent variable) By default this is set to 'PREDICTION' to mean that the model outputs a prediction. It could optionally be set to 'LIKELIHOOD' or '-2LL' to let the model output the likelihood or -2*log(likelihood) of the prediction. If set to a symbol this variable can be used to change the type for different records. The model would then set this symbol to 0 for a prediction value, 1 for likelihood and 2 for -2ll. """ return self._value_type @value_type.setter def value_type(self, value): allowed_strings = ['PREDICTION', 'LIKELIHOOD', '-2LL'] if isinstance(value, str): if value.upper() not in allowed_strings: raise ValueError( f"Cannot set value_type to {value}. Must be one of {allowed_strings} " f"or a symbol" ) value = value.upper() elif not isinstance(value, sympy.Symbol): raise ValueError("Can only set value_type to one of {allowed_strings} or a symbol") self._value_type = value @property def observation_transformation(self): """Transformation to be applied to the observation data""" return self._observation_transformation @observation_transformation.setter def observation_transformation(self, value): self._observation_transformation = value @property def parameters(self): """Definitions of population parameters See :class:`pharmpy.Parameters` """ return self._parameters @parameters.setter def parameters(self, value): inits = value.inits if inits and not self.random_variables.validate_parameters(inits): nearest = self.random_variables.nearest_valid_parameters(inits) if nearest != inits: before, after = self._compare_before_after_params(inits, nearest) warnings.warn( f"Adjusting initial estimates to create positive semidefinite " f"omega/sigma matrices.\nBefore adjusting: {before}.\n" f"After adjusting: {after}" ) value = value.set_initial_estimates(nearest) else: raise ValueError("New parameter inits are not valid") self._parameters = value @staticmethod def _compare_before_after_params(old, new): before = dict() after = dict() for key, value in old.items(): if new[key] != value: before[key] = value after[key] = new[key] return before, after @property def random_variables(self): """Definitions of random variables See :class:`pharmpy.RandomVariables` """ return self._random_variables @random_variables.setter def random_variables(self, value): if not isinstance(value, RandomVariables): raise TypeError("model.random_variables must be of RandomVariables type") self._random_variables = value @property def statements(self): """Definitions of model statements See :class:`pharmpy.Statements` """ return self._statements @statements.setter def statements(self, value): if not isinstance(value, Statements): raise TypeError("model.statements must be of Statements type") self._statements = value @property def estimation_steps(self): """Definitions of estimation steps See :class:`pharmpy.EstimationSteps` """ return self._estimation_steps @estimation_steps.setter def estimation_steps(self, value): if not isinstance(value, EstimationSteps): raise TypeError("model.estimation_steps must be of EstimationSteps type") self._estimation_steps = value @property def datainfo(self): """Definitions of model statements See :class:`pharmpy.Statements` """ return self._datainfo @datainfo.setter def datainfo(self, value): if not isinstance(value, DataInfo): raise TypeError("model.datainfo must be of DataInfo type") self._datainfo = value @property def dataset(self): """Dataset connected to model""" return self._dataset @dataset.setter def dataset(self, value): self._dataset = value self.update_datainfo() @property def initial_individual_estimates(self): """Initial estimates for individual parameters""" return self._initial_individual_estimates @initial_individual_estimates.setter def initial_individual_estimates(self, value): self._initial_individual_estimates = value @property def modelfit_results(self): """Modelfit results for this model""" return self._modelfit_results @modelfit_results.setter def modelfit_results(self, value): self._modelfit_results = value @property def model_code(self): """Model type specific code""" raise NotImplementedError("Generic model does not implement the model_code property") @property def parent_model(self): """Name of parent model""" return self._parent_model @parent_model.setter def parent_model(self, value): self._parent_model = value
[docs] def has_same_dataset_as(self, other): """Check if this model has the same dataset as another model Parameters ---------- other : Model Another model Returns ------- bool True if both models have the same dataset """ if self.dataset is None: if other.dataset is None: return True else: return False if other.dataset is None: return False # NOTE rely on duck-typing here (?) return self.dataset.equals(other.dataset)
@property def description(self): """A free text discription of the model""" return self._description @description.setter def description(self, value): self._description = value
[docs] def read_modelfit_results(self, path: Path): """Read in modelfit results""" raise NotImplementedError("Read modelfit results not implemented for generic models")
[docs] def update_datainfo(self): """Update model.datainfo for a new dataset""" colnames = self.dataset.columns try: curdi = self.datainfo except AttributeError: curdi = DataInfo() newdi = [] for colname in colnames: try: col = curdi[colname] except IndexError: datatype = ColumnInfo.convert_pd_dtype_to_datatype( self.dataset.dtypes[colname].name ) col = ColumnInfo(colname, datatype=datatype) newdi.append(col) newdi = curdi.derive(columns=newdi) if curdi != newdi: # Remove path if dataset has been updated newdi = newdi.derive(path=None) self.datainfo = newdi
[docs] def copy(self): """Create a deepcopy of the model object""" model_copy = copy.deepcopy(self) try: model_copy.parent_model = self.name except AttributeError: # NOTE Name could be absent. pass return model_copy
[docs] @staticmethod def create_model(obj=None, **kwargs): """Factory for creating a :class:`pharmpy.model` object from an object representing the model .. _path-like object: https://docs.python.org/3/glossary.html#term-path-like-object Parameters ---------- obj `path-like object`_ pointing to the model file or an IO object. Returns ------- Model Generic :class:`~pharmpy.generic.Model` if obj is None, otherwise appropriate implementation is invoked (e.g. NONMEM7 :class:`~pharmpy.plugins.nonmem.Model`). """ if isinstance(obj, str): path = Path(obj) elif isinstance(obj, pathlib.Path): path = obj elif isinstance(obj, io.IOBase): path = None elif obj is None: return Model() else: raise ValueError("Unknown input type to Model constructor") if path is not None: with open(path, 'r', encoding='latin-1') as fp: code = fp.read() else: code = obj.read() model_class = detect_model(code) model = model_class(code, path, **kwargs) # Setup model database here # Read in model results here? # Set filename extension? return model