"""Objective utilities."""
from typing import Union
import numpy as np
def _check_none(fun):
"""Return None if any input argument is None; Wrapper function."""
def checked_fun(*args, **kwargs):
if any(x is None for x in [*args, *(kwargs.values())]):
return None
return fun(*args, **kwargs)
return checked_fun
[docs]@_check_none
def res_to_chi2(res: np.ndarray) -> Union[float, None]:
"""Translate residuals to chi2 values, `chi2 = sum(res**2)`."""
return float(np.dot(res, res))
@_check_none
def chi2_to_fval(chi2: float) -> Union[float, None]:
"""Translate chi2 to function value, `fval = 0.5*chi2 = 0.5*sum(res**2)`.
Note that for the function value we thus employ a probabilistic
interpretation, as the log-likelihood of a standard normal noise model.
This is in line with e.g. AMICI's and SciPy's objective definition.
"""
return 0.5 * chi2
@_check_none
def res_to_fval(res: np.ndarray) -> Union[float, None]:
"""Translate residuals to function value, `fval = 0.5*sum(res**2)`."""
return chi2_to_fval(res_to_chi2(res))
[docs]@_check_none
def sres_to_schi2(res: np.ndarray, sres: np.ndarray):
"""Translate residual sensitivities to chi2 gradient."""
return 2 * res.dot(sres)
@_check_none
def schi2_to_grad(schi2: np.ndarray) -> np.ndarray:
"""Translate chi2 gradient to function value gradient.
See also :func:`chi2_to_fval`.
"""
return 0.5 * schi2
@_check_none
def sres_to_grad(res: np.ndarray, sres: np.ndarray):
"""Translate residual sensitivities to function value gradient.
Assumes `fval = 0.5*sum(res**2)`.
See also :func:`chi2_to_fval`.
"""
return schi2_to_grad(sres_to_schi2(res, sres))
@_check_none
def sres_to_fim(sres: np.ndarray):
"""Translate residual sensitivities to FIM.
The FIM is based on the function values, not chi2, i.e. has a normalization
of 0.5 as in :func:`res_to_fval`.
"""
return sres.transpose().dot(sres)