Пример #1
0
    def linear(cls):
        """Linear continuous template (for continuous covariates)."""
        symbol = S('symbol')
        expression = 1 + S('theta') * (S('cov') - S('median'))
        template = Assignment(symbol, expression)

        return cls(template)
Пример #2
0
 def parameter_translation(self,
                           reverse=False,
                           remove_idempotent=False,
                           as_symbols=False):
     """Get a dict of NONMEM name to Pharmpy parameter name
     i.e. {'THETA(1)': 'TVCL', 'OMEGA(1,1)': 'IVCL'}
     """
     self.parameters
     d = dict()
     for theta_record in self.control_stream.get_records('THETA'):
         for key, value in theta_record.name_map.items():
             nonmem_name = f'THETA({value})'
             d[nonmem_name] = key
     for record in self.control_stream.get_records('OMEGA'):
         for key, value in record.name_map.items():
             nonmem_name = f'OMEGA({value[0]},{value[1]})'
             d[nonmem_name] = key
     for record in self.control_stream.get_records('SIGMA'):
         for key, value in record.name_map.items():
             nonmem_name = f'SIGMA({value[0]},{value[1]})'
             d[nonmem_name] = key
     if remove_idempotent:
         d = {key: val for key, val in d.items() if key != val}
     if reverse:
         d = {val: key for key, val in d.items()}
     if as_symbols:
         d = {S(key): S(val) for key, val in d.items()}
     return d
Пример #3
0
    def exponential(cls):
        """Exponential template (for continuous covariates)."""
        symbol = S('symbol')
        expression = exp(S('theta') * (S('cov') - S('median')))
        template = Assignment(symbol, expression)

        return cls(template)
Пример #4
0
    def power(cls):
        """Power template (for continuous covariates)."""
        symbol = S('symbol')
        expression = (S('cov') / S('median'))**S('theta')
        template = Assignment(symbol, expression)

        return cls(template)
Пример #5
0
def test_apply(eta_trans, symbol, expression):
    etas = {'eta1': S('ETA(1)'), 'eta2': S('ETA(2)'), 'etab1': S('ETAB(1)'), 'etab2': S('ETAB(2)')}

    thetas = {'theta1': 'BOXCOX1', 'theta2': 'BOXCOX2'}

    eta_trans.apply(etas, thetas)
    assert eta_trans.assignments[1].symbol == symbol
    assert eta_trans.assignments[1].expression == expression
Пример #6
0
    def boxcox(cls, no_of_etas):
        assignments = ModelStatements()
        for i in range(1, no_of_etas + 1):
            symbol = S(f'etab{i}')
            expression = (exp(S(f'eta{i}')) ** S(f'theta{i}') - 1) / (S(f'theta{i}'))

            assignment = Assignment(symbol, expression)
            assignments.append(assignment)

        return cls('boxcox', assignments, 'lambda')
Пример #7
0
 def _abbr_translation(self, rv_trans):
     abbr_pharmpy = self.control_stream.abbreviated.translate_to_pharmpy_names(
     )
     abbr_replace = self.control_stream.abbreviated.replace
     abbr_trans = update_abbr_record(self, rv_trans)
     abbr_recs = {
         S(abbr_pharmpy[value]): S(key)
         for key, value in abbr_replace.items()
         if value in abbr_pharmpy.keys()
     }
     abbr_trans.update(abbr_recs)
     return abbr_trans
Пример #8
0
    def categorical(cls, counts):
        """Linear categorical template (for categorical covariates)."""
        symbol = S('symbol')
        most_common = counts.mode().pop(0)
        categories = counts.unique()
        values = [1]

        if np.isnan(most_common):
            most_common = S('NaN')

        conditions = [Eq(S('cov'), most_common)]

        for i, cat in enumerate(categories):
            if cat != most_common:
                if len(categories) == 2:
                    values += [1 + S('theta')]
                else:
                    values += [1 + S(f'theta{i}')]
                if np.isnan(cat):
                    conditions += [Eq(S('cov'), S('NaN'))]
                else:
                    conditions += [Eq(S('cov'), cat)]

        expression = Piecewise(*zip(values, conditions))

        template = Assignment(symbol, expression)

        return cls(template)
Пример #9
0
def iiv_on_ruv(model, list_of_eps=None, same_eta=True, eta_names=None):
    """
    Multiplies epsilons with exponential (new) etas. Initial estimates for new etas are 0.09.

    Parameters
    ----------
    model : Model
        Pharmpy model to apply IIV on epsilons.
    list_of_eps : str, list
        Name/names of epsilons to multiply with exponential etas. If None, all epsilons will
        be chosen. None is default.
    same_eta : bool
        Boolean of whether all RUVs from input should use the same new ETA or if one ETA
        should be created for each RUV. True is default.
    eta_names: str, list
        Custom names of new etas. Must be equal to the number epsilons or 1 if same eta.
    """
    list_of_eps = _format_input_list(list_of_eps)
    eps = _get_epsilons(model, list_of_eps)

    if eta_names and len(eta_names) != len(eps):
        raise ValueError(
            'The number of provided eta names must be equal to the number of epsilons.'
        )

    rvs, pset, sset = model.random_variables, model.parameters, model.statements

    if same_eta:
        eta = _create_eta(pset, 1, eta_names)
        rvs.append(eta)
        eta_dict = {e: eta for e in eps}
    else:
        etas = [_create_eta(pset, i + 1, eta_names) for i in range(len(eps))]
        for eta in etas:
            rvs.append(eta)
        eta_dict = {e: eta for e, eta in zip(eps, etas)}

    for e in eps:
        statement = sset.find_assignment(e.name, is_symbol=False)
        statement.expression = statement.expression.subs(
            S(e.name),
            S(e.name) * sympy.exp(S(eta_dict[e].name)))

    model.random_variables = rvs
    model.parameters = pset
    model.statements = sset

    model.modelfit_results = None

    return model
Пример #10
0
    def john_draper(cls, no_of_etas):
        assignments = ModelStatements()
        for i in range(1, no_of_etas + 1):
            symbol = S(f'etad{i}')

            eta = S(f'eta{i}')
            theta = S(f'theta{i}')

            expression = sign(eta) * (((abs(eta) + 1) ** theta - 1) / theta)

            assignment = Assignment(symbol, expression)
            assignments.append(assignment)

        return cls('johndraper', assignments, 'lambda')
Пример #11
0
    def statements(self):
        try:
            return self._statements
        except AttributeError:
            pass

        rec = self.get_pred_pk_record()
        statements = rec.statements

        des = self._get_des_record()
        error = self._get_error_record()
        if error:
            sub = self.control_stream.get_records('SUBROUTINES')[0]
            advan = sub.get_option_startswith('ADVAN')
            trans = sub.get_option_startswith('TRANS')
            if not trans:
                trans = 'TRANS1'
            comp = compartmental_model(self, advan, trans, des)
            if comp is not None:
                cm, link = comp
                statements += [cm, link]
            else:
                statements.append(
                    ODESystem())  # FIXME: Placeholder for ODE-system
                # FIXME: Dummy link statement
                statements.append(Assignment('F', S('F')))
            statements += error.statements

        if not hasattr(self, '_parameters'):
            self._read_parameters()

        trans_statements, trans_params = self._create_name_trans(statements)
        for key, value in trans_params.items():
            try:
                self.parameters[key].name = value
                for theta in self.control_stream.get_records('THETA'):
                    theta.update_name_map(trans_params)
                for omega in self.control_stream.get_records('OMEGA'):
                    omega.update_name_map(trans_params)
                for sigma in self.control_stream.get_records('SIGMA'):
                    sigma.update_name_map(trans_params)
            except KeyError:
                self.random_variables.subs({S(key): value})

        statements.subs(trans_statements)

        self._statements = statements
        self._old_statements = statements.copy()
        return statements
Пример #12
0
def power_on_ruv(model, list_of_eps=None):
    """
    Applies a power effect to provided epsilons. Initial estimates for new thetas are 1 if the error
    model is proportional, otherwise they are 0.1.

    Parameters
    ----------
    model : Model
        Pharmpy model to create block effect on.
    list_of_eps : list
        List of epsilons to apply power effect. If None, all epsilons will be used.
        None is default.
    """
    eps = _get_epsilons(model, list_of_eps)
    pset, sset = model.parameters, model.statements

    if model.error_model == 'PROP':
        theta_init = 1
    else:
        theta_init = 0.1

    for i, e in enumerate(eps):
        theta_name = str(
            model.create_symbol(stem='power', force_numbering=True))
        theta = Parameter(theta_name, theta_init)
        pset.add(theta)

        sset.subs(
            {e.name: model.individual_prediction_symbol**S(theta.name) * e})

    model.parameters = pset
    model.statements = sset

    return model
Пример #13
0
def power_on_ruv(model, list_of_eps=None):
    """
    Applies a power effect to provided epsilons. Initial estimates for new thetas are 1 if the error
    model is proportional, otherwise they are 0.1.

    Parameters
    ----------
    model : Model
        Pharmpy model to create block effect on.
    list_of_eps : str, list
        Name/names of epsilons to apply power effect. If None, all epsilons will be used.
        None is default.
    """
    list_of_eps = _format_input_list(list_of_eps)
    eps = model.random_variables.epsilons
    if list_of_eps is not None:
        eps = eps[list_of_eps]
    pset, sset = model.parameters, model.statements

    if has_proportional_error(model):
        theta_init = 1
    else:
        theta_init = 0.1

    for i, e in enumerate(eps):
        theta_name = str(model.create_symbol(stem='power', force_numbering=True))
        theta = Parameter(theta_name, theta_init)
        pset.append(theta)
        sset.subs({e.name: model.individual_prediction_symbol ** S(theta.name) * e.symbol})

    model.parameters = pset
    model.statements = sset

    return model
Пример #14
0
 def __init__(self, src, **kwargs):
     super().__init__()
     parser = NMTranParser()
     self.source = src
     if not self.source.filename_extension:
         self.source.filename_extension = '.ctl'
     self._name = self.source.path.stem
     self.control_stream = parser.parse(src.code)
     self._initial_individual_estimates_updated = False
     self._updated_etas_file = None
     self._dataset_updated = False
     self._modelfit_results = None
     self.dependent_variable = S('Y')
     self.individual_prediction_symbol = S('CIPREDI')
     self.observation_transformation = self.dependent_variable
     self._old_observation_transformation = self.dependent_variable
Пример #15
0
def remove_iov(model):
    """
    Removes all IOV omegas.

    Parameters
    ----------
    model : Model
        Pharmpy model to remove IOV from.
    """
    rvs, sset = model.random_variables, model.statements
    etas = _get_etas(rvs)

    if not etas:
        warnings.warn('No IOVs present')
        return model

    for eta in etas:
        statement = sset.find_assignment(eta.name, is_symbol=False)
        statement.expression = statement.expression.subs(S(eta.name), 0)
        del rvs[eta]

    model.random_variables = rvs
    model.statements = sset

    model.modelfit_results = None

    return model
Пример #16
0
def test_choose_param_init(pheno_path, testdata):
    model = Model(pheno_path)
    params = (model.parameters['OMEGA(1,1)'], model.parameters['OMEGA(2,2)'])
    rvs = RandomVariables(model.random_variables.etas)
    init = _choose_param_init(model, rvs, params)

    assert init == 0.0118179

    model = Model(pheno_path)
    model.source.path = testdata  # Path where there is no .ext-file
    init = _choose_param_init(model, rvs, params)

    assert init == 0.0031045

    model = Model(pheno_path)

    omega1 = S('OMEGA(3,3)')
    x = stats.Normal('ETA(3)', 0, sympy.sqrt(omega1))
    x.variability_level = VariabilityLevel.IIV
    rvs.add(x)

    ie = model.modelfit_results.individual_estimates
    ie['ETA(3)'] = ie['ETA(1)']
    model.modelfit_results = ModelfitResults(individual_estimates=ie)

    init = _choose_param_init(model, rvs, params)

    assert init == 0.0118179
Пример #17
0
def remove_iiv(model, list_to_remove=None):
    """
    Removes all IIV omegas given a list with eta names and/or parameter names.

    Parameters
    ----------
    model : Model
        Pharmpy model to create block effect on.
    list_to_remove : list
        List of etas and/or parameter name to remove. If None, all etas that are IIVs will
        be removed. None is default.
    """
    rvs, sset = model.random_variables, model.statements
    etas = _get_etas(model, list_to_remove)

    for eta in etas:
        statement = sset.find_assignment(eta.name, is_symbol=False)
        statement.expression = statement.expression.subs(S(eta.name), 0)
        rvs.discard(eta)

    model.random_variables = rvs
    model.statements = sset

    model.modelfit_results = None

    return model
Пример #18
0
 def _name_as_comments(self, statements):
     params_current = self.parameter_translation(remove_idempotent=True)
     for name_abbr, name_nonmem in self.control_stream.abbreviated.replace.items(
     ):
         if name_nonmem in params_current.keys():
             params_current[name_abbr] = params_current.pop(name_nonmem)
     trans_params = {
         name_comment: name_comment
         for name_current, name_comment in params_current.items()
         if S(name_current) not in statements.free_symbols
     }
     trans_statements = {
         name_current: name_comment
         for name_current, name_comment in params_current.items()
         if S(name_current) in statements.free_symbols
     }
     return trans_statements, trans_params
Пример #19
0
def _create_eta(pset, number):
    omega = S(f'IIV_RUV{number}')
    pset.add(Parameter(str(omega), 0.09))

    eta = stats.Normal(f'RV{number}', 0, sympy.sqrt(omega))
    eta.variability_level = VariabilityLevel.IIV

    return eta
Пример #20
0
def iiv_on_ruv(model, list_of_eps=None, same_eta=True):
    """
    Multiplies epsilons with exponential (new) etas. Initial estimates for new etas are 0.09.

    Parameters
    ----------
    model : Model
        Pharmpy model to apply IIV on epsilons.
    list_of_eps : list
        List of epsilons to multiply with exponential etas. If None, all epsilons will
        be chosen. None is default.
    same_eta : bool
        Boolean of whether all RUVs from input should use the same new ETA or if one ETA
        should be created for each RUV. True is default.
    """
    eps = _get_epsilons(model, list_of_eps)

    rvs, pset, sset = model.random_variables, model.parameters, model.statements

    if same_eta:
        eta = _create_eta(pset, 1)
        rvs.add(eta)
        eta_dict = {e: eta for e in eps}
    else:
        etas = [_create_eta(pset, i + 1) for i in range(len(eps))]
        for eta in etas:
            rvs.add(eta)
        eta_dict = {e: eta for e, eta in zip(eps, etas)}

    for e in eps:
        statement = sset.find_assignment(e.name, is_symbol=False)
        statement.expression = statement.expression.subs(
            S(e.name), S(e.name) * sympy.exp(S(eta_dict[e].name))
        )

    model.random_variables = rvs
    model.parameters = pset
    model.statements = sset

    model.modelfit_results = None

    return model
Пример #21
0
def _create_eta(pset, number, eta_names):
    omega = S(f'IIV_RUV{number}')
    pset.append(Parameter(str(omega), 0.09))

    if eta_names:
        eta_name = eta_names[number - 1]
    else:
        eta_name = f'ETA_RV{number}'

    eta = RandomVariable.normal(eta_name, 'iiv', 0, omega)
    return eta
Пример #22
0
 def rv_translation(self,
                    reverse=False,
                    remove_idempotent=False,
                    as_symbols=False):
     self.random_variables
     d = dict()
     for record in self.control_stream.get_records('OMEGA'):
         for key, value in record.eta_map.items():
             nonmem_name = f'ETA({value})'
             d[nonmem_name] = key
     for record in self.control_stream.get_records('SIGMA'):
         for key, value in record.eta_map.items():
             nonmem_name = f'EPS({value})'
             d[nonmem_name] = key
     if remove_idempotent:
         d = {key: val for key, val in d.items() if key != val}
     if reverse:
         d = {val: key for key, val in d.items()}
     if as_symbols:
         d = {S(key): S(val) for key, val in d.items()}
     return d
Пример #23
0
    def tdist(cls, no_of_etas):
        assignments = ModelStatements()
        for i in range(1, no_of_etas + 1):
            symbol = S(f'etat{i}')

            eta = S(f'eta{i}')
            theta = S(f'theta{i}')

            num_1 = eta ** 2 + 1
            denom_1 = 4 * theta

            num_2 = (5 * eta ** 4) + (16 * eta ** 2 + 3)
            denom_2 = 96 * theta ** 2

            num_3 = (3 * eta ** 6) + (19 * eta ** 4) + (17 * eta ** 2) - 15
            denom_3 = 384 * theta ** 3

            expression = eta * (1 + (num_1 / denom_1) + (num_2 / denom_2) + (num_3 / denom_3))

            assignment = Assignment(symbol, expression)
            assignments.append(assignment)

        return cls('tdist', assignments, 'df')
Пример #24
0
    def apply(self, parameter, covariate, thetas, statistics):
        effect_name = f'{parameter}{covariate}'
        self.template.symbol = S(effect_name)

        self.template.subs(thetas)
        self.template.subs({'cov': covariate})

        template_str = [str(symbol) for symbol in self.template.free_symbols]

        if 'mean' in template_str:
            self.template.subs({'mean': f'{covariate}_MEAN'})
            s = Assignment(S(f'{covariate}_MEAN'),
                           Float(statistics['mean'], 6))
            self.statistic_statements.append(s)
        if 'median' in template_str:
            self.template.subs({'median': f'{covariate}_MEDIAN'})
            s = Assignment(S(f'{covariate}_MEDIAN'),
                           Float(statistics['median'], 6))
            self.statistic_statements.append(s)
        if 'std' in template_str:
            self.template.subs({'std': f'{covariate}_STD'})
            s = Assignment(S(f'{covariate}_STD'), Float(statistics['std'], 6))
            self.statistic_statements.append(s)
Пример #25
0
def _create_template(effect, model, covariate):
    """Creates Covariate class objects with effect template."""
    if effect == 'lin':
        return CovariateEffect.linear()
    elif effect == 'cat':
        counts = _count_categorical(model, covariate)
        return CovariateEffect.categorical(counts)
    elif effect == 'piece_lin':
        return CovariateEffect.piecewise_linear()
    elif effect == 'exp':
        return CovariateEffect.exponential()
    elif effect == 'pow':
        return CovariateEffect.power()
    else:
        symbol = S('symbol')
        expression = sympy.sympify(effect)
        return CovariateEffect(Assignment(symbol, expression))
Пример #26
0
def add_etas(model, parameter, expression, operation='*'):
    """
    Adds etas to :class:`pharmpy.model`. Effects that currently have templates are:

    - Additive (*add*)
    - Proportional (*prop*)
    - Exponential (*exp*)
    - Logit (*logit*)

    For all except exponential the operation input is not needed. Otherwise user specified
    input is supported. Initial estimates for new etas are 0.09.

    Parameters
    ----------
    model : Model
        Pharmpy model to add new etas to.
    parameter : str
        Name of parameter to add new etas to.
    expression : str
        Effect on eta. Either abbreviated (see above) or custom.
    operation : str, optional
        Whether the new eta should be added or multiplied (default).
    """
    rvs, pset, sset = model.random_variables, model.parameters, model.statements

    omega = S(f'IIV_{parameter}')
    eta = stats.Normal(f'ETA_{parameter}', 0, sympy.sqrt(omega))
    eta.variability_level = VariabilityLevel.IIV

    rvs.add(eta)
    pset.add(Parameter(str(omega), init=0.09))

    statement = sset.find_assignment(parameter)
    eta_addition = _create_template(expression, operation)
    eta_addition.apply(statement.expression, eta.name)

    statement.expression = eta_addition.template

    model.random_variables = rvs
    model.parameters = pset
    model.statements = sset

    return model
Пример #27
0
    def _create_name_trans(self, statements):
        rvs = self.random_variables

        conf_functions = {
            'comment': self._name_as_comments(statements),
            'abbr': self._name_as_abbr(rvs),
            'basic': self._name_as_basic(),
        }

        abbr = self.control_stream.abbreviated.replace
        pset_current = {
            **self.parameter_translation(reverse=True),
            **{rv: rv
               for rv in [rv.name for rv in rvs]},
        }
        sset_current = {
            **abbr,
            **{
                rv.name: rv.name
                for rv in rvs if rv.name not in abbr.keys() and rv.symbol in statements.free_symbols
            },
            **{
                p: p
                for p in pset_current.values() if p not in abbr.keys() and S(p) in statements.free_symbols
            },
        }

        trans_sset, trans_pset = dict(), dict()
        names_sset_translated, names_pset_translated, names_basic = [], [], []
        clashing_symbols = set()

        for setting in pharmpy.plugins.nonmem.conf.parameter_names:
            trans_sset_setting, trans_pset_setting = conf_functions[setting]
            if setting != 'basic':
                clashing_symbols.update(
                    self._clashing_symbols(statements, {
                        **trans_sset_setting,
                        **trans_pset_setting
                    }))
            for name_current, name_new in trans_sset_setting.items():
                name_nonmem = sset_current[name_current]

                if S(
                        name_new
                ) in clashing_symbols or name_nonmem in names_sset_translated:
                    continue

                name_in_sset_current = {v: k
                                        for k, v in sset_current.items()
                                        }[name_nonmem]
                trans_sset[name_in_sset_current] = name_new
                names_sset_translated.append(name_nonmem)

                if name_nonmem in pset_current.values(
                ) and name_new in pset_current.keys():
                    names_pset_translated.append(name_nonmem)

            for name_current, name_new in trans_pset_setting.items():
                name_nonmem = pset_current[name_current]

                if S(
                        name_new
                ) in clashing_symbols or name_nonmem in names_pset_translated:
                    continue

                trans_pset[name_current] = name_new
                names_pset_translated.append(name_nonmem)

            if setting == 'basic':
                params_left = [
                    k for k in pset_current.keys()
                    if k not in names_pset_translated
                ]
                params_left += [
                    rv.name for rv in rvs
                    if rv.name not in names_sset_translated
                ]
                names_basic = [
                    name for name in params_left
                    if name not in names_sset_translated
                ]
                break

        if clashing_symbols:
            warnings.warn(
                f'The parameter names {clashing_symbols} are also names of variables '
                f'in the model code. Falling back to the in naming scheme config '
                f'names for these.')

        names_nonmem_all = [rv.name for rv in rvs] + [
            key for key in self.parameter_translation().keys()
        ]

        if set(names_nonmem_all) - set(names_sset_translated +
                                       names_pset_translated + names_basic):
            raise ValueError(
                'Mismatch in number of parameter names, all have not been accounted for. If basic '
                'NONMEM-names are desired as fallback, double-check that "basic" is included in '
                'config-settings for parameter_names.')
        return trans_sset, trans_pset
Пример #28
0
    def piecewise_linear(cls):
        """Piecewise linear ("hockey-stick") template (for continuous
        covariates)."""
        symbol = S('symbol')
        values = [
            1 + S('theta1') * (S('cov') - S('median')),
            1 + S('theta2') * (S('cov') - S('median')),
        ]
        conditions = [Le(S('cov'), S('median')), Gt(S('cov'), S('median'))]
        expression = Piecewise((values[0], conditions[0]),
                               (values[1], conditions[1]))

        template = Assignment(symbol, expression)

        return cls(template)
Пример #29
0
 def _clashing_symbols(statements, trans_statements):
     parameter_symbols = {S(symb) for _, symb in trans_statements.items()}
     clashing_symbols = parameter_symbols & statements.free_symbols
     return clashing_symbols
Пример #30
0
def add_covariate_effect(model, parameter, covariate, effect, operation='*'):
    """
    Adds covariate effect to :class:`pharmpy.model`. The following effects have templates:

    - Linear function for continuous covariates (*lin*)
        - Initial estimate: 0.001
        - Upper bound: 100,000 if the median of the covariate is equal to the minimum, otherwise
          :math:`1/(median - min)`
        - Lower bound: -100,000 if the median of the covariate is equal to the maximum, otherwise
          :math:`1/(median - max)`
    - Linear function for categorical covariates (*cat*)
        - Initial estimate: 0.001
        - Upper bound: 100,000
        - Lower bound: -100,000
    - Piecewise linear function/"hockey-stick", continuous covariates only (*piece_lin*)
        - Initial estimate: 0.001
        - Upper bound: for first state 1/(median - minimum), otherwise 100,000
        - Lower bound: for first state -100,000, otherwise 1/(median - maximum)
    - Exponential function, continuous covariates only (*exp*)
        - Initial estimate: 0.001 unless lower bound > 0.001 or upper bound < 0.001. In that case
          :math:`init = (upper - lower)/2`, if init = 0: :math:`init = upper/2`
        - Upper bound: if :math:`min - median = 0` or :math:`max - median = 0`, upper bound is 100.
          Otherwise the upper bound is
          :math:`min(log(0.01)/(min - median), log(100)/(max - median))`
        - Lower bound: if :math:`min - median = 0` or :math:`max - median = 0`, lower bound is 0.01.
          Otherwise the lower bound is
          :math:`max(log(0.01)/(max - median), log(100)/(min - median))`
    - Power function, continuous covariates only (*pow*)
        - Initial estimate: 0.001
        - Upper bound: 100,000
        - Lower bound: -100

    Parameters
    ----------
    model : Model
        Pharmpy model to add covariate effect to.
    parameter : str
        Name of parameter to add covariate effect to.
    covariate : str
        Name of covariate.
    effect : str
        Type of covariate effect. May be abbreviated covariate effect (see above) or custom.
    operation : str, optional
        Whether the covariate effect should be added or multiplied (default).
    """
    sset = model.statements

    if S(f'{parameter}{covariate}') in sset.free_symbols:
        warnings.warn('Covariate effect already exists')
        return model

    statistics = dict()
    statistics['mean'] = _calculate_mean(model.dataset, covariate)
    statistics['median'] = _calculate_median(model.dataset, covariate)
    statistics['std'] = _calculate_std(model.dataset, covariate)

    covariate_effect = _create_template(effect, model, covariate)
    thetas = _create_thetas(model, effect, covariate,
                            covariate_effect.template)

    param_statement = sset.find_assignment(parameter)

    index = sset.index(param_statement)

    covariate_effect.apply(parameter, covariate, thetas, statistics)
    effect_statement = covariate_effect.create_effect_statement(
        operation, param_statement)

    statements = covariate_effect.statistic_statements
    statements.append(covariate_effect.template)
    statements.append(effect_statement)

    for i, statement in enumerate(statements, 1):
        sset.insert(index + i, statement)

    model.statements = sset
    return model