Skip to content

openscm_calibration.scipy_plotting#

Support for plotting during scipy optimisation

Modules:

Name Description
base

Base implementation of support for plotting during scipy optimisation

scmdata

Support for plotting during scipy optimisation with [scmdata][scmdata]

Classes:

Name Description
CallbackProxy

Callback helper

NoSuccessfulRunsError

Raised when no runs completed successfully i.e. there is nothing to plot

OptPlotter

Optimisation progress plotting helper

Functions:

Name Description
get_optimisation_mosaic

Get optimisation mosaic

get_runs_to_plot

Get runs to plot

get_ymax_default

Get y-max based on costs

plot_costs

Plot cost function

plot_parameters

Plot parameters

CallbackProxy #

Bases: Generic[DataContainer]

Callback helper

This class acts as a proxy and decides whether the real callback should actually be called. If provided, it also keeps track of the number of model calls via a progress bar.

Methods:

Name Description
callback_differential_evolution

Update the plots

callback_minimize

Update the plots

time_to_call_real_callback

Check whether it is time to call the real callback

update_progress_bar

Update the progress bar

Attributes:

Name Type Description
last_callback_val int

Number of model calls at last callback

progress_bar tqdm[Any] | None

Progress bar to track iterations

real_callback SupportsScipyOptCallback

Callback to be called if a sufficient number of runs have been done

store OptResStore[DataContainer]

Optimisation result store

update_every int

Update the plots every X calls to the model

Source code in src/openscm_calibration/scipy_plotting/base.py
@define
class CallbackProxy(Generic[DataContainer]):
    """
    Callback helper

    This class acts as a proxy and decides whether the real callback should
    actually be called. If provided, it also keeps track of the number of
    model calls via a progress bar.
    """

    real_callback: SupportsScipyOptCallback
    """Callback to be called if a sufficient number of runs have been done"""

    store: OptResStore[DataContainer]
    """Optimisation result store"""

    last_callback_val: int = 0
    """Number of model calls at last callback"""

    update_every: int = 50
    """Update the plots every X calls to the model"""

    progress_bar: tqdm.std.tqdm[Any] | None = None
    """Progress bar to track iterations"""

    def callback_minimize(
        self,
        xk: np.typing.NDArray[np.number[Any]],
    ) -> None:
        """
        Update the plots

        Intended to be used as the `callback` argument to
        `scipy.optimize.minimize`

        Parameters
        ----------
        xk
            Last used parameter vector
        """
        if self.time_to_call_real_callback():
            self.real_callback.callback_minimize(xk)

    def callback_differential_evolution(
        self,
        xk: np.typing.NDArray[np.number[Any]],
        convergence: float | None = None,
    ) -> None:
        """
        Update the plots

        Intended to be used as the `callback` argument to
        `scipy.optimize.differential_evolution`

        Parameters
        ----------
        xk
            Parameter vector with best solution found so far

        convergence
            Received from :func:`scipy.optimize.differential_evolution`
            on callback. Not sure what this does is or is used for.
        """
        if self.time_to_call_real_callback():
            self.real_callback.callback_differential_evolution(xk, convergence)

    def time_to_call_real_callback(self) -> bool:
        """
        Check whether it is time to call the real callback

        Returns
        -------
            ``True`` if the real callback should be called
        """
        n_model_calls = sum(x is not None for x in self.store.x_samples)
        if self.progress_bar:
            self.update_progress_bar(n_model_calls)

        if n_model_calls < self.last_callback_val + self.update_every:
            return False

        # Note that we ran the full callback
        self.last_callback_val = n_model_calls

        return True

    def update_progress_bar(self, n_model_calls: int) -> None:
        """
        Update the progress bar

        Parameters
        ----------
        n_model_calls
            Number of model calls in total

        Raises
        ------
        TypeError
            ``self.progress_bar`` is ``None``. This typically happens if it
            was not set at the time the :obj:`CallbackProxy` was initialised.
        """
        if self.progress_bar is None:
            raise TypeError(self.progress_bar)

        self.progress_bar.update(n_model_calls - self.progress_bar.last_print_n)

last_callback_val class-attribute instance-attribute #

last_callback_val: int = 0

Number of model calls at last callback

progress_bar class-attribute instance-attribute #

progress_bar: tqdm[Any] | None = None

Progress bar to track iterations

real_callback instance-attribute #

real_callback: SupportsScipyOptCallback

Callback to be called if a sufficient number of runs have been done

store instance-attribute #

store: OptResStore[DataContainer]

Optimisation result store

update_every class-attribute instance-attribute #

update_every: int = 50

Update the plots every X calls to the model

callback_differential_evolution #

callback_differential_evolution(
    xk: NDArray[number[Any]],
    convergence: float | None = None,
) -> None

Update the plots

Intended to be used as the callback argument to scipy.optimize.differential_evolution

Parameters:

Name Type Description Default
xk NDArray[number[Any]]

Parameter vector with best solution found so far

required
convergence float | None

Received from :func:scipy.optimize.differential_evolution on callback. Not sure what this does is or is used for.

None
Source code in src/openscm_calibration/scipy_plotting/base.py
def callback_differential_evolution(
    self,
    xk: np.typing.NDArray[np.number[Any]],
    convergence: float | None = None,
) -> None:
    """
    Update the plots

    Intended to be used as the `callback` argument to
    `scipy.optimize.differential_evolution`

    Parameters
    ----------
    xk
        Parameter vector with best solution found so far

    convergence
        Received from :func:`scipy.optimize.differential_evolution`
        on callback. Not sure what this does is or is used for.
    """
    if self.time_to_call_real_callback():
        self.real_callback.callback_differential_evolution(xk, convergence)

callback_minimize #

callback_minimize(xk: NDArray[number[Any]]) -> None

Update the plots

Intended to be used as the callback argument to scipy.optimize.minimize

Parameters:

Name Type Description Default
xk NDArray[number[Any]]

Last used parameter vector

required
Source code in src/openscm_calibration/scipy_plotting/base.py
def callback_minimize(
    self,
    xk: np.typing.NDArray[np.number[Any]],
) -> None:
    """
    Update the plots

    Intended to be used as the `callback` argument to
    `scipy.optimize.minimize`

    Parameters
    ----------
    xk
        Last used parameter vector
    """
    if self.time_to_call_real_callback():
        self.real_callback.callback_minimize(xk)

time_to_call_real_callback #

time_to_call_real_callback() -> bool

Check whether it is time to call the real callback

Returns:

Type Description
``True`` if the real callback should be called
Source code in src/openscm_calibration/scipy_plotting/base.py
def time_to_call_real_callback(self) -> bool:
    """
    Check whether it is time to call the real callback

    Returns
    -------
        ``True`` if the real callback should be called
    """
    n_model_calls = sum(x is not None for x in self.store.x_samples)
    if self.progress_bar:
        self.update_progress_bar(n_model_calls)

    if n_model_calls < self.last_callback_val + self.update_every:
        return False

    # Note that we ran the full callback
    self.last_callback_val = n_model_calls

    return True

update_progress_bar #

update_progress_bar(n_model_calls: int) -> None

Update the progress bar

Parameters:

Name Type Description Default
n_model_calls int

Number of model calls in total

required

Raises:

Type Description
TypeError

self.progress_bar is None. This typically happens if it was not set at the time the :obj:CallbackProxy was initialised.

Source code in src/openscm_calibration/scipy_plotting/base.py
def update_progress_bar(self, n_model_calls: int) -> None:
    """
    Update the progress bar

    Parameters
    ----------
    n_model_calls
        Number of model calls in total

    Raises
    ------
    TypeError
        ``self.progress_bar`` is ``None``. This typically happens if it
        was not set at the time the :obj:`CallbackProxy` was initialised.
    """
    if self.progress_bar is None:
        raise TypeError(self.progress_bar)

    self.progress_bar.update(n_model_calls - self.progress_bar.last_print_n)

NoSuccessfulRunsError #

Bases: ValueError

Raised when no runs completed successfully i.e. there is nothing to plot

Source code in src/openscm_calibration/scipy_plotting/base.py
class NoSuccessfulRunsError(ValueError):
    """
    Raised when no runs completed successfully i.e. there is nothing to plot
    """

OptPlotter #

Bases: Generic[DataContainer]

Optimisation progress plotting helper

This class is an adapter between interfaces required by Scipy's callback arguments and updating the plots. The class holds all the information required to make useful plots. It is intended to be used in interactive Python i.e. to make updating plots.

Methods:

Name Description
callback_differential_evolution

Update the plots

callback_minimize

Update the plots

from_autogenerated_figure

Create plotter with automatic figure generation

update_plots

Update all the plots

Attributes:

Name Type Description
axes dict[str, Axes]

Dictionary storing axes on which to plot

convert_results_to_plot_dict ResultsToDictConverter[DataContainer]

Callable which converts results into a dictionary

cost_key str

Key for the axes on which the cost function should be plotted

fig Figure

Figure on which to plot

get_timeseries Callable[[DataContainer], DataFrame]

Function which converts data into timeseries.

holder SupportsFigUpdate

Figure updater, typically [IPython.core.display_functions.DisplayHandle][IPython.core.display_functions.DisplayHandle]

parameters tuple[str, ...]

Parameters to be optimised

plot_costs PlotCostsLike | None

Function that plots our costs

plot_parameters PlotParametersLike | None

Function that plots our parameters

plot_timeseries PlotTimeseriesLike[DataContainer]

Function that plots our timeseries

store OptResStore[DataContainer]

Optimisation result store

target DataContainer

Target used for optimisation

thin_ts_to_plot int

Thinning to apply to the timeseries to plot

timeseries_axes tuple[str, ...]

Axes on which to plot timeseries

Source code in src/openscm_calibration/scipy_plotting/base.py
@define
class OptPlotter(Generic[DataContainer]):
    """
    Optimisation progress plotting helper

    This class is an adapter between interfaces required by Scipy's callback arguments
    and updating the plots.
    The class holds all the information required to make useful plots.
    It is intended to be used in interactive Python i.e. to make updating plots.
    """

    holder: SupportsFigUpdate
    """Figure updater, typically [`IPython.core.display_functions.DisplayHandle`][]"""

    fig: matplotlib.figure.Figure
    """Figure on which to plot"""

    axes: dict[str, matplotlib.axes.Axes]
    """
    Dictionary storing axes on which to plot

    The plot of the cost function over time will be plotted on the axes
    with key given by `cost_key`.

    Each parameter will be plotted on the axes with the same key as the parameter
    (as defined in `parameters`).

    The timeseries will be plotted on the axes specified by `timeseries_axes`.
    See docstring of `timeseries_axes` for rules about its values.
    """

    cost_key: str
    """Key for the axes on which the cost function should be plotted"""

    parameters: tuple[str, ...] = field(validator=[_all_in_axes])
    """
    Parameters to be optimised

    This must match the order in which the parameters are handled by the optimiser,
    i.e. it is used to translate the unlabeled array of parameter values
    onto the desired axes.
    """

    timeseries_axes: tuple[str, ...] = field(
        validator=[_all_in_axes, _compatible_with_convert_and_target]
    )
    """
    Axes on which to plot timeseries

    The timeseries in `target` and `store.res`
    are converted into dictionaries using `convert_results_to_plot_dict`.
    The keys of the result of `convert_results_to_plot_dict`
    must match the values in `timeseries_axes`.
    """

    target: DataContainer
    """Target used for optimisation"""

    store: OptResStore[DataContainer]
    """Optimisation result store"""

    convert_results_to_plot_dict: ResultsToDictConverter[DataContainer]
    """
    Callable which converts results into a dictionary
    in which the keys are a subset of the values in `timeseries_axes`
    """

    get_timeseries: Callable[[DataContainer], pd.DataFrame]
    """
    Function which converts data into timeseries.
    """

    plot_timeseries: PlotTimeseriesLike[DataContainer]
    """
    Function that plots our timeseries
    """

    thin_ts_to_plot: int = 20
    """
    Thinning to apply to the timeseries to plot

    In other words, only plot every `thin_ts_to_plot` runs on the timeseries plots.
    Plotting all runs can be very expensive.
    """

    plot_costs: PlotCostsLike | None = None
    """
    Function that plots our costs

    If not supplied, we use
    [`plot_costs`][openscm_calibration.scipy_plotting.plot_costs].
    """

    plot_parameters: PlotParametersLike | None = None
    """
    Function that plots our parameters

    If not supplied, we use
    [`plot_parameters`][openscm_calibration.scipy_plotting.plot_parameters].
    """

    def callback_minimize(
        self,
        xk: np.typing.NDArray[np.number[Any]],
    ) -> None:
        """
        Update the plots

        Intended to be used as the `callback` argument to
        [`scipy.optimize.minimize`][].

        Parameters
        ----------
        xk
            Last used parameter vector
        """
        self.update_plots()

    def callback_differential_evolution(
        self,
        xk: np.typing.NDArray[np.number[Any]],
        convergence: float | None = None,
    ) -> None:
        """
        Update the plots

        Intended to be used as the `callback` argument to
        `scipy.optimize.differential_evolution`

        Parameters
        ----------
        xk
            Parameter vector with best solution found so far

        convergence
            Received from [`scipy.optimize.differential_evolution`][] on callback.
            We are not sure what this does is or what it is used for.
        """
        self.update_plots()

    @classmethod
    def from_autogenerated_figure(  # noqa: PLR0913
        cls,
        cost_key: str,
        params: tuple[str],
        convert_results_to_plot_dict: ResultsToDictConverter[DataContainer],
        target: DataContainer,
        store: OptResStore[DataContainer],
        kwargs_create_mosaic: dict[str, Any] | None = None,
        kwargs_get_fig_axes_holder: dict[str, Any] | None = None,
        **kwargs: Any,
    ) -> OptPlotter[DataContainer]:
        """
        Create plotter with automatic figure generation

        Parameters
        ----------
        cost_key
            Name to use for the cost axis

        params
            Parameters that are being optimised

            This is used to generate the plotting axes.
            It must also match the order in which the parameters are handled by the optimiser,
            i.e. it is used to translate the unlabeled array of parameter values
            onto the desired axes.

        convert_results_to_plot_dict
            Callable which converts results into a dictionary

        target
            Target to which we are optimising

        store
            Optimisation result store

        kwargs_create_mosaic
            Passed to [`get_optimisation_mosaic`][openscm_calibration.scipy_plotting.get_optimisation_mosaic]

        kwargs_get_fig_axes_holder
            Passed to [`get_fig_axes_holder_from_mosaic`][openscm_calibration.matplotlib_utils.get_fig_axes_holder_from_mosaic]

        **kwargs
            Passed to the initialiser of [`OptPlotter`][openscm_calibration.scipy_plotting.OptPlotter]

        Returns
        -------
        :
            Initialised instance with generated figure, axes and holder
        """  # noqa: E501
        if kwargs_create_mosaic is None:
            kwargs_create_mosaic = {}

        if kwargs_get_fig_axes_holder is None:
            kwargs_get_fig_axes_holder = {}

        timeseries_axes = tuple(convert_results_to_plot_dict(target).keys())
        mosaic = get_optimisation_mosaic(
            cost_key=cost_key,
            params=params,
            timeseries=timeseries_axes,
            **kwargs_create_mosaic,
        )

        fig, axes, holder = get_fig_axes_holder_from_mosaic(
            mosaic, **kwargs_get_fig_axes_holder
        )

        return cls(
            holder=holder,
            fig=fig,
            axes=axes,
            cost_key=cost_key,
            parameters=params,
            timeseries_axes=timeseries_axes,
            convert_results_to_plot_dict=convert_results_to_plot_dict,
            target=target,
            store=store,
            **kwargs,
        )

    def update_plots(self) -> None:
        """
        Update all the plots
        """
        costs, para_vals, res = self.store.get_costs_labelled_xsamples_res()

        # check if anything to plot
        if np.all(~np.isfinite(costs)):
            logger.info("No runs succeeded, nothing to plot")
            return

        # plot cost function
        ax_cost = self.axes[self.cost_key]
        ax_cost.clear()

        if self.plot_costs is None:
            plot_costs_h = plot_costs
        else:
            plot_costs_h = self.plot_costs

        plot_costs_h(ax=ax_cost, ylabel=self.cost_key, costs=costs)

        # plot parameters
        for parameter in self.parameters:
            self.axes[parameter].clear()

        if self.plot_parameters is None:
            plot_parameters_h = plot_parameters
        else:
            plot_parameters_h = self.plot_parameters

        plot_parameters_h(axes=self.axes, para_vals=para_vals)

        # plot timeseries
        best_run, others_to_plot = get_runs_to_plot(costs, res, self.thin_ts_to_plot)

        for k in self.timeseries_axes:
            self.axes[k].clear()

        self.plot_timeseries(
            best_run=best_run,
            others_to_plot=others_to_plot,
            target=self.target,
            convert_results_to_plot_dict=self.convert_results_to_plot_dict,
            timeseries_keys=self.timeseries_axes,
            axes=self.axes,
            get_timeseries=self.get_timeseries,
        )

        # update and return
        self.fig.tight_layout()
        self.holder.update(self.fig)

axes instance-attribute #

axes: dict[str, Axes]

Dictionary storing axes on which to plot

The plot of the cost function over time will be plotted on the axes with key given by cost_key.

Each parameter will be plotted on the axes with the same key as the parameter (as defined in parameters).

The timeseries will be plotted on the axes specified by timeseries_axes. See docstring of timeseries_axes for rules about its values.

convert_results_to_plot_dict instance-attribute #

convert_results_to_plot_dict: ResultsToDictConverter[
    DataContainer
]

Callable which converts results into a dictionary in which the keys are a subset of the values in timeseries_axes

cost_key instance-attribute #

cost_key: str

Key for the axes on which the cost function should be plotted

fig instance-attribute #

fig: Figure

Figure on which to plot

get_timeseries instance-attribute #

get_timeseries: Callable[[DataContainer], DataFrame]

Function which converts data into timeseries.

holder instance-attribute #

Figure updater, typically [IPython.core.display_functions.DisplayHandle][IPython.core.display_functions.DisplayHandle]

parameters class-attribute instance-attribute #

parameters: tuple[str, ...] = field(
    validator=[_all_in_axes]
)

Parameters to be optimised

This must match the order in which the parameters are handled by the optimiser, i.e. it is used to translate the unlabeled array of parameter values onto the desired axes.

plot_costs class-attribute instance-attribute #

plot_costs: PlotCostsLike | None = None

Function that plots our costs

If not supplied, we use plot_costs.

plot_parameters class-attribute instance-attribute #

plot_parameters: PlotParametersLike | None = None

Function that plots our parameters

If not supplied, we use plot_parameters.

plot_timeseries instance-attribute #

plot_timeseries: PlotTimeseriesLike[DataContainer]

Function that plots our timeseries

store instance-attribute #

store: OptResStore[DataContainer]

Optimisation result store

target instance-attribute #

target: DataContainer

Target used for optimisation

thin_ts_to_plot class-attribute instance-attribute #

thin_ts_to_plot: int = 20

Thinning to apply to the timeseries to plot

In other words, only plot every thin_ts_to_plot runs on the timeseries plots. Plotting all runs can be very expensive.

timeseries_axes class-attribute instance-attribute #

timeseries_axes: tuple[str, ...] = field(
    validator=[
        _all_in_axes,
        _compatible_with_convert_and_target,
    ]
)

Axes on which to plot timeseries

The timeseries in target and store.res are converted into dictionaries using convert_results_to_plot_dict. The keys of the result of convert_results_to_plot_dict must match the values in timeseries_axes.

callback_differential_evolution #

callback_differential_evolution(
    xk: NDArray[number[Any]],
    convergence: float | None = None,
) -> None

Update the plots

Intended to be used as the callback argument to scipy.optimize.differential_evolution

Parameters:

Name Type Description Default
xk NDArray[number[Any]]

Parameter vector with best solution found so far

required
convergence float | None

Received from [scipy.optimize.differential_evolution][scipy.optimize.differential_evolution] on callback. We are not sure what this does is or what it is used for.

None
Source code in src/openscm_calibration/scipy_plotting/base.py
def callback_differential_evolution(
    self,
    xk: np.typing.NDArray[np.number[Any]],
    convergence: float | None = None,
) -> None:
    """
    Update the plots

    Intended to be used as the `callback` argument to
    `scipy.optimize.differential_evolution`

    Parameters
    ----------
    xk
        Parameter vector with best solution found so far

    convergence
        Received from [`scipy.optimize.differential_evolution`][] on callback.
        We are not sure what this does is or what it is used for.
    """
    self.update_plots()

callback_minimize #

callback_minimize(xk: NDArray[number[Any]]) -> None

Update the plots

Intended to be used as the callback argument to [scipy.optimize.minimize][scipy.optimize.minimize].

Parameters:

Name Type Description Default
xk NDArray[number[Any]]

Last used parameter vector

required
Source code in src/openscm_calibration/scipy_plotting/base.py
def callback_minimize(
    self,
    xk: np.typing.NDArray[np.number[Any]],
) -> None:
    """
    Update the plots

    Intended to be used as the `callback` argument to
    [`scipy.optimize.minimize`][].

    Parameters
    ----------
    xk
        Last used parameter vector
    """
    self.update_plots()

from_autogenerated_figure classmethod #

from_autogenerated_figure(
    cost_key: str,
    params: tuple[str],
    convert_results_to_plot_dict: ResultsToDictConverter[
        DataContainer
    ],
    target: DataContainer,
    store: OptResStore[DataContainer],
    kwargs_create_mosaic: dict[str, Any] | None = None,
    kwargs_get_fig_axes_holder: dict[str, Any]
    | None = None,
    **kwargs: Any,
) -> OptPlotter[DataContainer]

Create plotter with automatic figure generation

Parameters:

Name Type Description Default
cost_key str

Name to use for the cost axis

required
params tuple[str]

Parameters that are being optimised

This is used to generate the plotting axes. It must also match the order in which the parameters are handled by the optimiser, i.e. it is used to translate the unlabeled array of parameter values onto the desired axes.

required
convert_results_to_plot_dict ResultsToDictConverter[DataContainer]

Callable which converts results into a dictionary

required
target DataContainer

Target to which we are optimising

required
store OptResStore[DataContainer]

Optimisation result store

required
kwargs_create_mosaic dict[str, Any] | None None
kwargs_get_fig_axes_holder dict[str, Any] | None None
**kwargs Any

Passed to the initialiser of OptPlotter

{}

Returns:

Type Description
OptPlotter[DataContainer]

Initialised instance with generated figure, axes and holder

Source code in src/openscm_calibration/scipy_plotting/base.py
@classmethod
def from_autogenerated_figure(  # noqa: PLR0913
    cls,
    cost_key: str,
    params: tuple[str],
    convert_results_to_plot_dict: ResultsToDictConverter[DataContainer],
    target: DataContainer,
    store: OptResStore[DataContainer],
    kwargs_create_mosaic: dict[str, Any] | None = None,
    kwargs_get_fig_axes_holder: dict[str, Any] | None = None,
    **kwargs: Any,
) -> OptPlotter[DataContainer]:
    """
    Create plotter with automatic figure generation

    Parameters
    ----------
    cost_key
        Name to use for the cost axis

    params
        Parameters that are being optimised

        This is used to generate the plotting axes.
        It must also match the order in which the parameters are handled by the optimiser,
        i.e. it is used to translate the unlabeled array of parameter values
        onto the desired axes.

    convert_results_to_plot_dict
        Callable which converts results into a dictionary

    target
        Target to which we are optimising

    store
        Optimisation result store

    kwargs_create_mosaic
        Passed to [`get_optimisation_mosaic`][openscm_calibration.scipy_plotting.get_optimisation_mosaic]

    kwargs_get_fig_axes_holder
        Passed to [`get_fig_axes_holder_from_mosaic`][openscm_calibration.matplotlib_utils.get_fig_axes_holder_from_mosaic]

    **kwargs
        Passed to the initialiser of [`OptPlotter`][openscm_calibration.scipy_plotting.OptPlotter]

    Returns
    -------
    :
        Initialised instance with generated figure, axes and holder
    """  # noqa: E501
    if kwargs_create_mosaic is None:
        kwargs_create_mosaic = {}

    if kwargs_get_fig_axes_holder is None:
        kwargs_get_fig_axes_holder = {}

    timeseries_axes = tuple(convert_results_to_plot_dict(target).keys())
    mosaic = get_optimisation_mosaic(
        cost_key=cost_key,
        params=params,
        timeseries=timeseries_axes,
        **kwargs_create_mosaic,
    )

    fig, axes, holder = get_fig_axes_holder_from_mosaic(
        mosaic, **kwargs_get_fig_axes_holder
    )

    return cls(
        holder=holder,
        fig=fig,
        axes=axes,
        cost_key=cost_key,
        parameters=params,
        timeseries_axes=timeseries_axes,
        convert_results_to_plot_dict=convert_results_to_plot_dict,
        target=target,
        store=store,
        **kwargs,
    )

update_plots #

update_plots() -> None

Update all the plots

Source code in src/openscm_calibration/scipy_plotting/base.py
def update_plots(self) -> None:
    """
    Update all the plots
    """
    costs, para_vals, res = self.store.get_costs_labelled_xsamples_res()

    # check if anything to plot
    if np.all(~np.isfinite(costs)):
        logger.info("No runs succeeded, nothing to plot")
        return

    # plot cost function
    ax_cost = self.axes[self.cost_key]
    ax_cost.clear()

    if self.plot_costs is None:
        plot_costs_h = plot_costs
    else:
        plot_costs_h = self.plot_costs

    plot_costs_h(ax=ax_cost, ylabel=self.cost_key, costs=costs)

    # plot parameters
    for parameter in self.parameters:
        self.axes[parameter].clear()

    if self.plot_parameters is None:
        plot_parameters_h = plot_parameters
    else:
        plot_parameters_h = self.plot_parameters

    plot_parameters_h(axes=self.axes, para_vals=para_vals)

    # plot timeseries
    best_run, others_to_plot = get_runs_to_plot(costs, res, self.thin_ts_to_plot)

    for k in self.timeseries_axes:
        self.axes[k].clear()

    self.plot_timeseries(
        best_run=best_run,
        others_to_plot=others_to_plot,
        target=self.target,
        convert_results_to_plot_dict=self.convert_results_to_plot_dict,
        timeseries_keys=self.timeseries_axes,
        axes=self.axes,
        get_timeseries=self.get_timeseries,
    )

    # update and return
    self.fig.tight_layout()
    self.holder.update(self.fig)

get_optimisation_mosaic #

get_optimisation_mosaic(
    cost_key: str,
    params: tuple[str, ...],
    timeseries: tuple[str, ...],
    cost_col_relwidth: int = 1,
    n_parameters_per_row: int = 1,
    n_timeseries_per_row: int = 1,
) -> list[list[str]]

Get optimisation mosaic

This gives back the grid of axes to use for plotting. It can be understood by matplotlib but in theory could be used with any tool that understands such mosaics/grids.

Parameters:

Name Type Description Default
cost_key str

Name to use for the cost axis

required
params tuple[str, ...]

Parameters axes to generate

required
timeseries tuple[str, ...]

Timeseries axes to generate

required
cost_col_relwidth int

Width of the cost axis, relative to the width of each parameter axis

1
n_parameters_per_row int

Number of parameters to plot per row (as many rows as are needed to plot all the parameters are created)

1
n_timeseries_per_row int

Number of timeseries to plot per row (as many rows as are needed to plot all the timeseries are created)

1

Returns:

Type Description
Mosaic
Source code in src/openscm_calibration/scipy_plotting/base.py
def get_optimisation_mosaic(  # noqa: PLR0913
    cost_key: str,
    params: tuple[str, ...],
    timeseries: tuple[str, ...],
    cost_col_relwidth: int = 1,
    n_parameters_per_row: int = 1,
    n_timeseries_per_row: int = 1,
) -> list[list[str]]:
    """
    Get optimisation mosaic

    This gives back the grid of axes to use for plotting. It can be understood
    by matplotlib but in theory could be used with any tool that understands
    such mosaics/grids.

    Parameters
    ----------
    cost_key
        Name to use for the cost axis

    params
        Parameters axes to generate

    timeseries
        Timeseries axes to generate

    cost_col_relwidth
        Width of the cost axis, relative to the width of each parameter axis

    n_parameters_per_row
        Number of parameters to plot per row (as many rows as are needed to
        plot all the parameters are created)

    n_timeseries_per_row
        Number of timeseries to plot per row (as many rows as are needed to
        plot all the timeseries are created)

    Returns
    -------
        Mosaic
    """
    parameters_wrapped = more_itertools.grouper(
        params, n_parameters_per_row, fillvalue="."
    )
    timeseries_axes_wrapped = more_itertools.grouper(
        timeseries, n_timeseries_per_row, fillvalue="."
    )

    n_top_half_cols = cost_col_relwidth + n_parameters_per_row
    n_bottom_half_cols = n_timeseries_per_row

    top_half_row_repeats = [
        ([cost_key] * cost_col_relwidth + list(parameter_row), n_bottom_half_cols)
        for parameter_row in parameters_wrapped
    ]
    bottom_half_row_repeats = [
        (row, n_top_half_cols) for row in timeseries_axes_wrapped
    ]

    mosaic = [
        list(more_itertools.repeat_each(row, n_repeats))
        for row, n_repeats in top_half_row_repeats + bottom_half_row_repeats
    ]

    return mosaic

get_runs_to_plot #

get_runs_to_plot(
    costs: tuple[float, ...],
    res: tuple[DataContainer, ...],
    thin_ts_to_plot: int,
) -> tuple[DataContainer, tuple[DataContainer, ...]]

Get runs to plot

This retrieves the run which best matches the target (has lowest cost) and then a series of others to plot.

Parameters:

Name Type Description Default
costs tuple[float, ...]

Cost function value for each run (used to determine the best result)

required
res tuple[DataContainer, ...]

Results of each run.

It is assumed that the elements in res and costs line up i.e. the nth element of costs is the cost function for the nth element in res.

required
thin_ts_to_plot int

Thinning to apply to the timeseries to plot

In other words, only plot every thin_ts_to_plot runs on the timeseries plots. Plotting all runs can be very expensive.

required

Returns:

Type Description
tuple[DataContainer, tuple[DataContainer, ...]]

Best iteration and other runs to plot

Raises:

Type Description
ValueError

No successful runs are included in res

Source code in src/openscm_calibration/scipy_plotting/base.py
def get_runs_to_plot(
    costs: tuple[float, ...],
    res: tuple[DataContainer, ...],
    thin_ts_to_plot: int,
) -> tuple[DataContainer, tuple[DataContainer, ...]]:
    """
    Get runs to plot

    This retrieves the run which best matches the target (has lowest cost)
    and then a series of others to plot.

    Parameters
    ----------
    costs
        Cost function value for each run (used to determine the best result)

    res
        Results of each run.

        It is assumed that the elements in ``res``
        and ``costs`` line up i.e. the nth element of ``costs``
        is the cost function for the nth element in ``res``.

    thin_ts_to_plot
        Thinning to apply to the timeseries to plot

        In other words, only plot every `thin_ts_to_plot` runs
        on the timeseries plots.
        Plotting all runs can be very expensive.

    Returns
    -------
    :
        Best iteration and other runs to plot

    Raises
    ------
    ValueError
        No successful runs are included in ``res``
    """
    # Convert to dict for quicker lookup later
    res_d_success = {i: v for i, v in enumerate(res) if v is not None}
    if not res_d_success:
        raise NoSuccessfulRunsError()

    best_it = int(np.argmin(costs))
    out_best = res_d_success[best_it]

    success_keys = list(res_d_success.keys())
    to_plot_not_best = success_keys[
        len(success_keys) - 1 :: -thin_ts_to_plot  # (prefer black)
    ]
    if best_it in to_plot_not_best:
        to_plot_not_best.remove(best_it)

    out_not_best = tuple([res_d_success[i] for i in to_plot_not_best])

    return out_best, out_not_best

get_ymax_default #

get_ymax_default(
    costs: tuple[float, ...],
    min_scale_factor: float = 10.0,
    min_v_median_scale_factor: float = 2.0,
) -> float

Get y-max based on costs

This is the default function used by plot_costs. The algorithm is

.. math::

\text{ymax} = min(
    \text{min_scale_factor} \times min(costs),
    max(
        median(costs),
        \text{min_v_median_scale_factor} \times min(costs)
    )
)

Parameters:

Name Type Description Default
costs tuple[float, ...]

Cost function values

required
min_scale_factor float

Value by which the minimum value is scaled when determining the plot limits

10.0
min_v_median_scale_factor float

Value by which the minimum value is scaled when comparing to the median as part of determining the plot limits

2.0

Returns:

Type Description
Maximum value to use on the y-axis
Source code in src/openscm_calibration/scipy_plotting/base.py
def get_ymax_default(
    costs: tuple[float, ...],
    min_scale_factor: float = 10.0,
    min_v_median_scale_factor: float = 2.0,
) -> float:
    r"""
    Get y-max based on costs

    This is the default function used by
    [`plot_costs`][openscm_calibration.scipy_plotting.base.plot_costs].
    The algorithm is

    .. math::

        \text{ymax} = min(
            \text{min_scale_factor} \times min(costs),
            max(
                median(costs),
                \text{min_v_median_scale_factor} \times min(costs)
            )
        )

    Parameters
    ----------
    costs
        Cost function values

    min_scale_factor
        Value by which the minimum value is scaled when determining the plot
        limits

    min_v_median_scale_factor
        Value by which the minimum value is scaled when comparing to the
        median as part of determining the plot limits

    Returns
    -------
        Maximum value to use on the y-axis
    """
    min_cost = np.min(costs)
    ymax = np.min(
        [
            min_scale_factor * min_cost,
            np.max([np.median(costs), min_v_median_scale_factor * min_cost]),
        ]
    )

    return float(ymax)

plot_costs #

plot_costs(
    ax: Axes,
    ylabel: str,
    costs: tuple[float, ...],
    ymin: float = 0.0,
    get_ymax: Callable[[tuple[float, ...]], float]
    | None = None,
    alpha: float = 0.7,
    **kwargs: Any,
) -> None

Plot cost function

Parameters:

Name Type Description Default
ax Axes

Axes on which to plot

required
ylabel str

y-axis label

required
costs tuple[float, ...]

Costs to plot

required
ymin float

Minimum y-axis value

0.0
get_ymax Callable[[tuple[float, ...]], float] | None

Function which gets the y-max based on the costs. If not provided, :func:get_ymax_default is used

None
alpha float

Alpha to apply to plotted points

0.7
**kwargs Any

Passed to :meth:ax.scatter

{}
Source code in src/openscm_calibration/scipy_plotting/base.py
def plot_costs(  # noqa: PLR0913
    ax: matplotlib.axes.Axes,
    ylabel: str,
    costs: tuple[float, ...],
    ymin: float = 0.0,
    get_ymax: Callable[[tuple[float, ...]], float] | None = None,
    alpha: float = 0.7,
    **kwargs: Any,
) -> None:
    r"""
    Plot cost function

    Parameters
    ----------
    ax
        Axes on which to plot

    ylabel
        y-axis label

    costs
        Costs to plot

    ymin
        Minimum y-axis value

    get_ymax
        Function which gets the y-max based on the costs. If not provided,
        :func:`get_ymax_default` is used

    alpha
        Alpha to apply to plotted points

    **kwargs
        Passed to :meth:`ax.scatter`
    """
    if get_ymax is None:
        get_ymax = get_ymax_default

    ax.scatter(range(len(costs)), costs, alpha=alpha, **kwargs)
    ax.set_ylabel(ylabel)

    ymax = get_ymax(costs)
    if not np.isfinite(ymax):
        ymax = 10**3

    ax.set_ylim(
        ymin=ymin,
        ymax=ymax,
    )

plot_parameters #

plot_parameters(
    axes: dict[str, Axes],
    para_vals: dict[str, NDArray[number[Any]]],
    alpha: float = 0.7,
    **kwargs: Any,
) -> None

Plot parameters

Parameters:

Name Type Description Default
axes dict[str, Axes]

Axes on which to plot. The keys should match the keys in para_vals

required
para_vals dict[str, NDArray[number[Any]]]

Parameter values. Each key should be the name of a parameter

required
alpha float

Alpha to use when calling :meth:matplotlib.axes.Axes.scatter

0.7
**kwargs Any

Passed to each call to :meth:matplotlib.axes.Axes.scatter

{}
Source code in src/openscm_calibration/scipy_plotting/base.py
def plot_parameters(
    axes: dict[str, matplotlib.axes.Axes],
    para_vals: dict[str, np.typing.NDArray[np.number[Any]]],
    alpha: float = 0.7,
    **kwargs: Any,
) -> None:
    """
    Plot parameters

    Parameters
    ----------
    axes
        Axes on which to plot. The keys should match the keys in ``para_vals``

    para_vals
        Parameter values. Each key should be the name of a parameter

    alpha
        Alpha to use when calling :meth:`matplotlib.axes.Axes.scatter`

    **kwargs
        Passed to each call to :meth:`matplotlib.axes.Axes.scatter`
    """
    for parameter, values in para_vals.items():
        axes[parameter].scatter(range(len(values)), values, alpha=alpha, **kwargs)
        axes[parameter].set_ylabel(parameter)