Source code for pharmpy.model.parameters

from __future__ import annotations

from collections.abc import Mapping
from collections.abc import Sequence as CollectionsSequence
from typing import Any, Optional, Sequence, Union, overload

from pharmpy.basic import Expr
from pharmpy.deps import numpy as np
from pharmpy.deps import pandas as pd
from pharmpy.internals.immutable import Immutable, cache_method


[docs] class Parameter(Immutable): """A single parameter Example ------- >>> from pharmpy.model import Parameter >>> param = Parameter("TVCL", 0.005, lower=0.0) >>> param.init 0.005 Parameters ---------- name : str Name of the parameter init : number Initial estimate or simply the value of parameter. fix : bool A boolean to indicate whether the parameter is fixed or not. Note that fixing a parameter will keep its bounds even if a fixed parameter is actually constrained to one single value. This is so that unfixing will take back the previous bounds. lower : float The lower bound of the parameter. Default no bound. Must be less than the init. upper : float The upper bound of the parameter. Default no bound. Must be greater than the init. """ def __init__( self, name: str, init: float, lower: float = -float("inf"), upper: float = float("inf"), fix: bool = False, ): self._name = name self._init = init self._lower = lower self._upper = upper self._fix = fix
[docs] @classmethod def create( cls, name: str, init: Union[float, Expr], lower: Optional[Union[float, Expr]] = None, upper: Optional[Union[float, Expr]] = None, fix: bool = False, ): """Alternative constructor for Parameter with error checking""" if not isinstance(name, str): raise ValueError("Name of parameter must be of type string") init = float(init) if np.isnan(init): raise ValueError('Initial estimate cannot be NaN') if lower is None: lower = -float('inf') else: lower = float(lower) if upper is None: upper = float('inf') else: upper = float(upper) if init < lower: raise ValueError(f'Lower bound {lower} cannot be greater than init {init}') if init > upper: raise ValueError(f'Upper bound {upper} cannot be less than init {init}') return cls(name, init, lower, upper, bool(fix))
[docs] def replace(self, **kwargs) -> Parameter: """Replace properties and create a new Parameter""" name = kwargs.get('name', self._name) init = kwargs.get('init', self._init) lower = kwargs.get('lower', self._lower) upper = kwargs.get('upper', self._upper) fix = kwargs.get('fix', self._fix) new = Parameter.create(name, init, lower=lower, upper=upper, fix=fix) return new
@property def name(self) -> str: """Parameter name""" return self._name @property def fix(self) -> bool: """Should parameter be fixed or not""" return self._fix @property def symbol(self) -> Expr: """Symbol representing the parameter""" return Expr.symbol(self._name) @property def lower(self) -> float: """Lower bound of the parameter""" return self._lower @property def upper(self) -> float: """Upper bound of the parameter""" return self._upper @property def init(self) -> float: """Initial parameter estimate or value""" return self._init @cache_method def __hash__(self): return hash((self.name, self.init, self.lower, self.upper, self.fix))
[docs] def to_dict(self) -> dict[str, Any]: return { 'name': self.name, 'init': self.init, 'lower': self.lower, 'upper': self.upper, 'fix': self.fix, }
[docs] @classmethod def from_dict(cls, d: dict[str, Any]): return cls(**d)
def __eq__(self, other: Any): """Two parameters are equal if they have the same name, init and constraints""" if not isinstance(other, Parameter): return NotImplemented if hash(self) != hash(other): return False return ( self._init == other._init # pragma: no cover and self._lower == other._lower # pragma: no cover and self._upper == other._upper # pragma: no cover and self._name == other._name # pragma: no cover and self._fix == other._fix # pragma: no cover ) def __repr__(self): if self._lower == -float("inf"): lower = "-∞" else: lower = self._lower if self._upper == float("inf"): upper = "∞" else: upper = self._upper return ( f'Parameter("{self._name}", {self._init}, lower={lower}, upper={upper}, ' f'fix={self._fix})' )
[docs] class Parameters(CollectionsSequence, Immutable): """An immutable collection of parameters Class representing a group of parameters. Usually all parameters in a model. This class give a ways of displaying, summarizing and manipulating more than one parameter at a time. Specific parameters can be found using indexing on the parameter name Example ------- >>> from pharmpy.model import Parameters, Parameter >>> par1 = Parameter("x", 0) >>> par2 = Parameter("y", 1) >>> pset = Parameters((par1, par2)) >>> pset["x"] Parameter("x", 0, lower=-∞, upper=∞, fix=False) >>> "x" in pset True """ def __init__(self, parameters: tuple[Parameter, ...] = ()): self._params = parameters
[docs] @classmethod def create(cls, parameters: Optional[Union[Parameters, Sequence[Parameter]]] = None): if isinstance(parameters, Parameters): return parameters elif parameters is None: parameters = () else: parameters = tuple(parameters) names = set() for p in parameters: if not isinstance(p, Parameter): raise ValueError(f'Can not add variable of type {type(p)} to Parameters') if p.name in names: raise ValueError( f'Parameter names must be unique. Parameter "{p.name}" ' 'was added more than once to Parameters' ) names.add(p.name) return cls(parameters)
[docs] def replace(self, **kwargs) -> Parameters: """Replace properties and create a new Parameters object""" parameters = kwargs.get('parameters', self._params) new = Parameters.create(parameters) return new
def __len__(self): return len(self._params) def _lookup_param(self, ind: Union[int, str, Expr, Parameter]): if isinstance(ind, Expr): if ind.is_symbol(): ind = ind.name else: raise KeyError("Cannot index Parameters with Expr other than Symbol") if isinstance(ind, str): for i, param in enumerate(self._params): if ind == param.name: return i, param raise KeyError(f'Could not find {ind} in Parameters') elif isinstance(ind, Parameter): try: i = self._params.index(ind) except ValueError: raise KeyError(f'Could not find {ind.name} in Parameters') return i, ind return ind, self._params[ind] @overload def __getitem__(self, ind: Union[int, str, Expr, Parameter]) -> Parameter: ... @overload def __getitem__( self, ind: Union[slice, Sequence[Union[int, str, Expr, Parameter]]] ) -> Parameters: ... def __getitem__(self, ind): if isinstance(ind, slice): return Parameters(self._params[ind]) elif not isinstance(ind, str) and isinstance(ind, CollectionsSequence): return Parameters(tuple(self._lookup_param(i)[1] for i in ind)) else: _, param = self._lookup_param(ind) return param def __contains__(self, ind): try: self._lookup_param(ind) return True except KeyError: return False
[docs] def to_dataframe(self) -> pd.DataFrame: """Create a dataframe with a summary of all Parameters Returns ------- DataFrame A dataframe with one row per parameter. The columns are value, lower, upper and fix Row Index is the names Example ------- >>> from pharmpy.model import Parameters, Parameter >>> par1 = Parameter("CL", 1, lower=0, upper=10) >>> par2 = Parameter("V", 10, lower=0, upper=100) >>> pset = Parameters((par1, par2)) >>> pset.to_dataframe() value lower upper fix CL 1 0 10 False V 10 0 100 False """ symbols = [param.name for param in self._params] values = [param.init for param in self._params] lower = [param.lower for param in self._params] upper = [param.upper for param in self._params] fix = [param.fix for param in self._params] index = pd.Index(symbols) return pd.DataFrame( {'value': values, 'lower': lower, 'upper': upper, 'fix': fix}, index=index )
@property def names(self) -> list[str]: """List of all parameter names""" return [p.name for p in self._params] @property def symbols(self) -> list[Expr]: """List of all parameter symbols""" return [p.symbol for p in self._params] @property def lower(self) -> dict[str, float]: """Lower bounds of all parameters as a dictionary""" return {p.name: p.lower for p in self._params} @property def upper(self) -> dict[str, float]: """Upper bounds of all parameters as a dictionary""" return {p.name: p.upper for p in self._params} @property def inits(self) -> dict[str, float]: """Initial estimates of parameters as dict""" return {p.name: p.init for p in self._params}
[docs] def set_initial_estimates(self, inits: Mapping[str, float]) -> Parameters: """Create a new Parameters with changed initial estimates Parameters ---------- inits : dict A dictionary of parameter names to initial estimates Return ------ Parameters An update Parameters object """ new = [] for p in self: if p.name in inits: newparam = p.replace(init=inits[p.name]) else: newparam = p new.append(newparam) return Parameters(tuple(new))
@property def fix(self) -> dict[str, bool]: """Fixedness of parameters as dict""" return {p.name: p.fix for p in self._params}
[docs] def set_fix(self, fix: Mapping[str, bool]) -> Parameters: """Create a new Parameters with changed fix state Parameters ---------- fix : dict A dictionary of parameter names to boolean fix state Return ------ Parameters An update Parameters object """ new = [] for p in self: if p.name in fix: newparam = p.replace(fix=fix[p.name]) else: newparam = p new.append(newparam) return Parameters(tuple(new))
@property def fixed(self) -> Parameters: """All fixed parameters""" fixed = [p for p in self._params if p.fix] return Parameters(tuple(fixed)) @property def nonfixed(self) -> Parameters: """All non-fixed parameters""" nonfixed = [p for p in self._params if not p.fix] return Parameters(tuple(nonfixed)) def __add__(self, other: Union[Parameter, Parameters, Sequence[Parameter]]) -> Parameters: if isinstance(other, Parameter): return Parameters.create(self._params + (other,)) elif isinstance(other, Parameters): return Parameters.create(self._params + other._params) elif isinstance(other, Sequence): return Parameters.create(self._params + tuple(other)) else: raise ValueError(f"Cannot add {other} to Parameters") def __radd__(self, other: Union[Parameter, Sequence[Parameter]]) -> Parameters: if isinstance(other, Parameter): return Parameters.create((other,) + self._params) elif isinstance(other, Sequence): return Parameters.create(tuple(other) + self._params) else: raise ValueError(f"Cannot add {other} to Parameters") def __sub__(self, other: Union[Parameter, Parameters, Sequence[Parameter]]) -> Parameters: if isinstance(other, Parameter): return Parameters(tuple(p for p in self._params if p.name != other.name)) elif isinstance(other, Parameters) or isinstance(other, Sequence): names = [p.name for p in other] return Parameters(tuple(p for p in self._params if p.name not in names)) else: raise ValueError(f"Cannot remove {other} from Parameters") def __rsub__(self, other: Union[Parameter, Sequence[Parameter]]) -> Parameters: if isinstance(other, Parameter): return Parameters(()) if other.name in self.names else Parameters((other,)) elif isinstance(other, Sequence): return Parameters(tuple(p for p in other if p.name not in self.names)) else: raise ValueError(f"Cannot remove Parameters from {other}") def __eq__(self, other: Any): if not isinstance(other, Parameters): return NotImplemented if hash(self) != hash(other): return False if len(self) != len(other): return False # pragma: no cover for p1, p2 in zip(self, other): if p1 != p2: return False # pragma: no cover return True @cache_method def __hash__(self): return hash(self._params)
[docs] def to_dict(self) -> dict[str, Any]: params = tuple(p.to_dict() for p in self._params) return {'parameters': params}
[docs] @classmethod def from_dict(cls, d) -> Parameters: params = tuple(Parameter.from_dict(p) for p in d['parameters']) return cls(parameters=params)
def __repr__(self): if len(self) == 0: return "Parameters()" return ( self.to_dataframe().replace(float("inf"), "∞").replace(-float("inf"), "-∞").to_string() ) def _repr_html_(self) -> str: if len(self) == 0: return "Parameters()" else: return ( self.to_dataframe() .replace(float("inf"), "∞") .replace(-float("inf"), "-∞") .to_html() )