diff --git a/corrai/optimize.py b/corrai/optimize.py index 2714a24..bac865f 100644 --- a/corrai/optimize.py +++ b/corrai/optimize.py @@ -1038,15 +1038,26 @@ def _apply_figure_kwargs(fig: go.Figure, **kwargs) -> None: fig.update_traces(**{key: val}) -def _forest_label(value: float, relabs: str, mode: str) -> str: +def _forest_label( + value: float, relabs: str, mode: str, ref_value: float | None = None +) -> str: """Format a bound or optimal value for forest plot annotation.""" if mode == "normalized": return "" if mode == "relative" and relabs == "Relative": return f"{value * 100:.4g}%" + if mode == "absolute" and relabs == "Relative" and ref_value is not None: + return f"{value * ref_value:.4g}" return f"{value:.4g}" +def _forest_ref_value(p) -> float | None: + """Return the reference value for converting a Relative parameter, or None.""" + if p.relabs == "Relative" and p.init_value is not None: + return float(p.init_value[0]) + return None + + def plot_parameter_forest( parameters: list[Parameter], optimal_values: dict[str, float] | list[float] | pd.Series, @@ -1066,8 +1077,9 @@ def plot_parameter_forest( How bounds are labelled depends on ``mode``: * ``"normalized"`` — no value annotations; Y-axis ticks read 0 % … 100 %. - * ``"absolute"`` — actual lower, upper, and optimal values are shown as - text on each bar. + * ``"absolute"`` — for parameters with ``relabs="Absolute"``, actual bound and + optimal values are shown; for parameters with ``relabs="Relative"`` and a defined + ``init_value``, converted real values are shown (``bound * init_value``). * ``"relative"`` — parameters with ``relabs="Relative"`` are annotated in percent (e.g. ``interval=(0.2, 1.5)`` → ``"20 %"`` / ``"150 %"``); parameters with ``relabs="Absolute"`` fall back to actual values. @@ -1158,15 +1170,17 @@ def plot_parameter_forest( # --- Labels annotate = mode != "normalized" lower_texts = { - p.name: _forest_label(p.interval[0], p.relabs, mode) for p in interval_params + p.name: _forest_label(p.interval[0], p.relabs, mode, _forest_ref_value(p)) + for p in interval_params } upper_texts = { - p.name: _forest_label(p.interval[1], p.relabs, mode) for p in interval_params + p.name: _forest_label(p.interval[1], p.relabs, mode, _forest_ref_value(p)) + for p in interval_params } # Optimal text: mode-aware for interval; empty for choice (tick labels already mark each position) all_opt_texts = { p.name: ( - _forest_label(float(opt_dict[p.name]), p.relabs, mode) + _forest_label(float(opt_dict[p.name]), p.relabs, mode, _forest_ref_value(p)) if p.interval is not None else "" ) diff --git a/tests/test_optimize.py b/tests/test_optimize.py index 67795fd..d18522e 100644 --- a/tests/test_optimize.py +++ b/tests/test_optimize.py @@ -478,6 +478,29 @@ def test_modes(self): ) assert fig_abs_fallback.data[1].text[0] == "0.03" + # absolute mode with Relative param + init_value: bounds converted (bound * init_value) + rel_param_with_init = [ + Parameter( + "mult", + interval=(0.2, 1.5), + relabs="Relative", + init_value=4.0, + model_property="x", + ) + ] + fig_abs_rel = plot_parameter_forest( + rel_param_with_init, {"mult": 0.8}, mode="absolute" + ) + assert fig_abs_rel.data[1].text[0] == "0.8" # 0.2 * 4.0 + assert fig_abs_rel.data[2].text[0] == "6" # 1.5 * 4.0 + assert fig_abs_rel.data[3].text[0] == "3.2" # 0.8 * 4.0 + + # absolute mode with Relative param without init_value: falls back to raw value + fig_abs_no_init = plot_parameter_forest( + rel_params, {"mult": 0.8}, mode="absolute" + ) + assert fig_abs_no_init.data[1].text[0] == "0.2" + def test_layout_and_style(self): fig = plot_parameter_forest(FOREST_PARAMS, _OPT_DICT) assert fig.data[0].line.color == "darkblue"