Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visualize: allow for log scale of hierarchical parameters for visualization #1435

Merged
merged 9 commits into from
Aug 13, 2024
17 changes: 17 additions & 0 deletions pypesto/hierarchical/base_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,23 @@ def __init__(

if scale not in {LIN, LOG, LOG10}:
raise ValueError(f"Scale not recognized: {scale}.")

if (
scale in [LOG, LOG10]
and inner_parameter_type == InnerParameterType.SIGMA
):
raise ValueError(
f"Inner parameter type `{inner_parameter_type}` "
f"cannot be log-scaled."
)

if scale in [LOG, LOG10] and lb <= 0:
raise ValueError(
f"Lower bound of inner parameter `{inner_parameter_id}` "
f"cannot be non-positive for log-scaled parameters. "
f"Provide a positive lower bound."
)

self.scale = scale

if inner_parameter_type not in (
Expand Down
37 changes: 33 additions & 4 deletions pypesto/hierarchical/base_problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import numpy as np
import pandas as pd

from ..C import LIN, LOG, LOG10
from .base_parameter import InnerParameter

try:
Expand Down Expand Up @@ -82,6 +83,10 @@ def get_interpretable_x_ids(self) -> list[str]:
"""
return list(self.xs.keys())

def get_interpretable_x_scales(self) -> list[str]:
"""Get scales of interpretable inner parameters."""
return [x.scale for x in self.xs.values()]

def get_xs_for_type(
self, inner_parameter_type: str
) -> list[InnerParameter]:
Expand Down Expand Up @@ -119,7 +124,9 @@ def get_for_id(self, inner_parameter_id: str) -> InnerParameter:
try:
return self.xs[inner_parameter_id]
except KeyError:
raise KeyError(f"Cannot find parameter with id {id}.") from None
raise KeyError(
f"Cannot find parameter with id {inner_parameter_id}."
) from None

def is_empty(self) -> bool:
"""Check for emptiness.
Expand Down Expand Up @@ -222,15 +229,37 @@ def scale_value_dict(

def scale_value(val: float | np.array, scale: str) -> float | np.array:
"""Scale a single value."""
if scale == "lin":
if scale == LIN:
return val
if scale == "log":
if scale == LOG:
return np.log(val)
if scale == "log10":
if scale == LOG10:
return np.log10(val)
raise ValueError(f"Scale {scale} not recognized.")


def scale_back_value_dict(
dct: dict[str, float], problem: InnerProblem
) -> dict[str, float]:
"""Scale back a value dictionary."""
scaled_dct = {}
for key, val in dct.items():
x = problem.get_for_id(key)
scaled_dct[key] = scale_back_value(val, x.scale)
return scaled_dct


def scale_back_value(val: float | np.array, scale: str) -> float | np.array:
"""Scale back a single value."""
if scale == LIN:
return val
if scale == LOG:
return np.exp(val)
if scale == LOG10:
return 10**val
raise ValueError(f"Scale {scale} not recognized.")


def ix_matrices_from_arrays(
ixs: dict[str, list[tuple[int, int, int]]], edatas: list[np.array]
) -> dict[str, list[np.array]]:
Expand Down
8 changes: 8 additions & 0 deletions pypesto/hierarchical/inner_calculator_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,14 @@ def get_interpretable_inner_par_bounds(
ub.extend(ub_i)
return np.asarray(lb), np.asarray(ub)

def get_interpretable_inner_par_scales(self) -> list[str]:
"""Return the scales of interpretable inner parameters of all inner problems."""
return [
scale
for inner_calculator in self.inner_calculators
for scale in inner_calculator.inner_problem.get_interpretable_x_scales()
]

def __call__(
self,
x_dct: dict,
Expand Down
7 changes: 7 additions & 0 deletions pypesto/hierarchical/ordinal/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,13 @@ def get_interpretable_x_ids(self) -> list[str]:
"""
return []

def get_interpretable_x_scales(self) -> list[str]:
"""Get scales of interpretable inner parameters.

There are no interpretable inner parameters for the ordinal problem.
"""
return []

def get_groups_for_xs(self, inner_parameter_type: str) -> list[int]:
"""Get unique list of ``OptimalScalingParameter.group`` values."""
groups = [x.group for x in self.get_xs_for_type(inner_parameter_type)]
Expand Down
26 changes: 24 additions & 2 deletions pypesto/hierarchical/petab.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Helper methods for hierarchical optimization with PEtab."""

import warnings
from typing import Literal

import pandas as pd
Expand Down Expand Up @@ -94,14 +95,35 @@ def validate_hierarchical_petab_problem(petab_problem: petab.Problem) -> None:
and not (
inner_parameter_table[petab.PARAMETER_SCALE].isna()
| (inner_parameter_table[petab.PARAMETER_SCALE] == petab.LIN)
| (
inner_parameter_table[PARAMETER_TYPE]
!= InnerParameterType.SIGMA
)
).all()
):
sub_df = inner_parameter_table.loc[
:, [PARAMETER_TYPE, petab.PARAMETER_SCALE]
]
raise NotImplementedError(
"Only parameterScale=lin supported for parameters of the inner "
f"subproblem.\n{sub_df}"
"LOG and LOG10 parameter scale of inner parameters is not supported "
"for sigma parameters. Inner parameter table:\n"
f"{sub_df}"
)
elif (
petab.PARAMETER_SCALE in inner_parameter_table
and not (
inner_parameter_table[petab.PARAMETER_SCALE].isna()
| (inner_parameter_table[petab.PARAMETER_SCALE] == petab.LIN)
).all()
):
sub_df = inner_parameter_table.loc[
:, [PARAMETER_TYPE, petab.PARAMETER_SCALE]
]
warnings.warn(
f"LOG and LOG10 parameter scale of inner parameters is used only "
f"for their visualization, and does not affect their optimization. "
f"Inner parameter table:\n{sub_df}",
stacklevel=1,
)

inner_parameter_df = validate_measurement_formulae(
Expand Down
10 changes: 9 additions & 1 deletion pypesto/hierarchical/relative/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
from ...optimize import minimize
from ...problem import Problem
from ..base_parameter import InnerParameter
from ..base_problem import InnerProblem, scale_value_dict
from ..base_problem import (
InnerProblem,
scale_back_value_dict,
scale_value_dict,
)
from ..base_solver import InnerSolver
from .util import (
apply_offset,
Expand Down Expand Up @@ -62,6 +66,8 @@ def calculate_obj_function(
relevant_data = copy.deepcopy(problem.data)
sim = copy.deepcopy(sim)
sigma = copy.deepcopy(sigma)
inner_parameters = copy.deepcopy(inner_parameters)
inner_parameters = scale_back_value_dict(inner_parameters, problem)

for x in problem.get_xs_for_type(InnerParameterType.OFFSET):
apply_offset(
Expand Down Expand Up @@ -140,6 +146,8 @@ def calculate_gradients(
relevant_data = copy.deepcopy(problem.data)
sim = copy.deepcopy(sim)
sigma = copy.deepcopy(sigma)
inner_parameters = copy.deepcopy(inner_parameters)
inner_parameters = scale_back_value_dict(inner_parameters, problem)

# restructure sensitivities to have parameter index as second index
ssim = [
Expand Down
15 changes: 14 additions & 1 deletion pypesto/hierarchical/semiquantitative/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
NOISE_PARAMETERS,
OBSERVABLE_ID,
PARAMETER_ID,
PARAMETER_SCALE,
UPPER_BOUND,
)
except ImportError:
Expand Down Expand Up @@ -149,6 +150,18 @@ def get_interpretable_x_ids(self) -> list[str]:
if x.inner_parameter_type == InnerParameterType.SIGMA
]

def get_interpretable_x_scales(self) -> list[str]:
"""Get scales of interpretable inner parameters.

The interpretable inner parameters of the semiquantitative
problem are the noise parameters.
"""
return [
x.scale
for x in self.xs.values()
if x.inner_parameter_type == InnerParameterType.SIGMA
]

def get_semiquant_observable_ids(self) -> list[str]:
"""Get the IDs of semiquantitative observables."""
return list(
Expand Down Expand Up @@ -420,7 +433,7 @@ def noise_inner_parameters_from_parameter_df(
SplineInnerParameter(
inner_parameter_id=row[PARAMETER_ID],
inner_parameter_type=InnerParameterType.SIGMA,
scale=LIN,
scale=row[PARAMETER_SCALE],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If sigma parameters cannot handle the log values, why would we change that here from the LIN?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True... This is left from a version in which I tried doing it for sigma as well.
But even tho it doesn't work for sigma, it might be good to keep row[PARAMETER_SCALE] here. There are petab checks that will fail before you come to this spot if you have any LOG scale for the sigma parameters. So it feels cleaner to have it taken from petab parameter df instead of hard-coding LIN scale here.

lb=row[LOWER_BOUND],
ub=row[UPPER_BOUND],
observable_id=observable_id,
Expand Down
9 changes: 9 additions & 0 deletions pypesto/problem/hierarchical.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ class HierarchicalProblem(Problem):
Only relevant if hierarchical is True. Contains the bounds of easily
interpretable inner parameters only, e.g. noise parameters, scaling
factors, offsets.
inner_scales:
The scales for the inner optimization parameters. Only relevant if
hierarchical is True. Contains the scales of easily interpretable inner
parameters only, e.g. noise parameters, scaling factors, offsets. Can
be pypesto.C.{LIN,LOG,LOG10}. Used only for visualization purposes.
semiquant_observable_ids:
The ids of semiquantitative observables. Only relevant if hierarchical
is True. If not None, the optimization result's `spline_knots` will be
Expand Down Expand Up @@ -77,6 +82,10 @@ def __init__(
self.inner_lb = np.array(inner_lb)
self.inner_ub = np.array(inner_ub)

self.inner_scales = (
self.objective.calculator.get_interpretable_inner_par_scales()
)

self.semiquant_observable_ids = (
self.objective.calculator.semiquant_observable_ids
)
Expand Down
13 changes: 13 additions & 0 deletions pypesto/visualize/observable_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from amici.petab.conditions import fill_in_parameters

from ..hierarchical import InnerCalculatorCollector
from ..hierarchical.base_problem import scale_back_value_dict
from ..hierarchical.relative.calculator import RelativeAmiciCalculator
from ..hierarchical.relative.problem import RelativeInnerProblem
from ..hierarchical.semiquantitative.calculator import SemiquantCalculator
Expand Down Expand Up @@ -301,6 +302,18 @@ def plot_linear_observable_mappings_from_pypesto_result(
)
)

# Remove inner parameters not belonging to the relative inner problem.
inner_parameter_values = {
key: value
for key, value in inner_parameter_values.items()
if key in inner_problem.get_x_ids()
}

# Scale the inner parameters back to linear scale.
inner_parameter_values = scale_back_value_dict(
inner_parameter_values, inner_problem
)

######################################
# Plot the linear observable mappings.
######################################
Expand Down
Loading