from __future__ import annotations
from collections.abc import Mapping
from collections.abc import Sequence as CollectionsSequence
from typing import TYPE_CHECKING, Any, Optional, Sequence, Union, overload
from pharmpy.basic import Expr
from pharmpy.internals.immutable import Immutable, cache_method
if TYPE_CHECKING:
import numpy as np
import pandas as pd
else:
from pharmpy.deps import numpy as np
from pharmpy.deps import pandas as pd
[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 hash(self) != hash(other):
return False
return (
isinstance(other, Parameter)
and self._init == other._init
and self._lower == other._lower
and self._upper == other._upper
and self._name == other._name
and self._fix == other._fix
)
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 __eq__(self, other: Any):
if hash(self) != hash(other):
return False
if not isinstance(other, Parameters):
return False
if len(self) != len(other):
return False
for p1, p2 in zip(self, other):
if p1 != p2:
return False
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()
)