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

Implemented option to plot pulls #219

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions examples/006_advanced_errors/03_relative_uncertainties.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
- still not completely correct, but much better.
"""

from kafe2 import XYContainer, Fit, Plot, ContoursProfiler
from kafe2 import XYContainer, Fit, Plot

x = [19.8, 3.0, 5.1, 16.1, 8.2, 11.7, 6.2, 10.1]
y = [23.2, 3.2, 4.5, 19.9, 7.1, 12.5, 4.5, 7.2]
Expand Down Expand Up @@ -42,12 +42,13 @@
plot.customize('model_line', 'color', ('orange', 'mistyrose'))
plot.customize('model_error_band', 'label', (r'$\pm 1 \sigma$', r'$\pm 1 \sigma$'))
plot.customize('model_error_band', 'color', ('orange', 'mistyrose'))
plot.plot(ratio=True)

cpf1 = ContoursProfiler(linear_fit1)
cpf1.plot_profiles_contours_matrix(show_grid_for='contours')

cpf2 = ContoursProfiler(linear_fit2)
cpf2.plot_profiles_contours_matrix(show_grid_for='contours')
# Test for pull plot
plot.plot(pull=True)

# cpf1 = ContoursProfiler(linear_fit1)
# cpf1.plot_profiles_contours_matrix(show_grid_for='contours')
#
# cpf2 = ContoursProfiler(linear_fit2)
# cpf2.plot_profiles_contours_matrix(show_grid_for='contours')

plot.show()
152 changes: 107 additions & 45 deletions kafe2/fit/_base/plot.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import abc
import numpy # help IDEs with type-hinting inside docstrings
import numpy as np
import six
import textwrap
import warnings
import itertools
import matplotlib as mpl
import os
import textwrap
import warnings
from collections import OrderedDict
from collections.abc import Iterable

import matplotlib as mpl
import numpy # help IDEs with type-hinting inside docstrings
import numpy as np
import six
from matplotlib import gridspec as gs
from matplotlib import pyplot as plt
from matplotlib import rcParams, rc_context
from matplotlib.legend_handler import HandlerBase

from .container import DataContainerBase
from .format import ParameterFormatter
from ..multi.fit import MultiFit
from ..util.wrapper import _fit_history
from ...config import kc, ConfigError, kafe2_rc

from collections import OrderedDict
from matplotlib import pyplot as plt
from matplotlib import gridspec as gs
from matplotlib.legend_handler import HandlerBase
from matplotlib import rcParams, rc_context

try:
import typing # help IDEs with type-hinting inside docstrings
except ImportError:
Expand Down Expand Up @@ -71,7 +71,7 @@ def __init__(self, *args):
self._prop_val_sizes = np.array(_pv_sizes, dtype=int)
self._counter_divisors = np.ones_like(self._prop_val_sizes, dtype=int)
for i in range(1, self._dim):
self._counter_divisors[i] = self._counter_divisors[i-1] * self._prop_val_sizes[i-1]
self._counter_divisors[i] = self._counter_divisors[i - 1] * self._prop_val_sizes[i - 1]
self._cycle_counter = 0

# public properties
Expand All @@ -83,7 +83,7 @@ def modulo(self):
# public methods

def get(self, cycle_position):
_prop_positions = [(cycle_position//self._counter_divisors[i]) % self._prop_val_sizes[i]
_prop_positions = [(cycle_position // self._counter_divisors[i]) % self._prop_val_sizes[i]
for i in six.moves.range(self._dim)]
_ps = {}
for _i, _content in enumerate(self._props):
Expand Down Expand Up @@ -120,6 +120,7 @@ def subset_cycler(self, properties):

class DummyLegendHandler(HandlerBase):
"""Dummy legend handler (nothing is drawn)"""

def legend_artist(self, *args, **kwargs):
return None

Expand Down Expand Up @@ -165,6 +166,11 @@ class PlotAdapterBase:
plot_adapter_method='plot_residual',
target_axes='residual',
),
pull=dict(
plot_style_as='data',
plot_adapter_method='plot_pull',
target_axes='pull',
),
)

AVAILABLE_X_SCALES = ('linear',)
Expand Down Expand Up @@ -261,7 +267,8 @@ def _set_plot_labels(self):
self.update_plot_kwargs(plot_model_name, dict(label=self._fit.model_label))
except ValueError:
pass # no model plot function available
_model_error_name = kc('fit', 'plot', 'error_label') % dict(model_label=self._fit.model_label) \
_model_error_name = kc('fit', 'plot', 'error_label') % dict(
model_label=self._fit.model_label) \
if self._fit.model_label != '__del__' else '__del__'
try:
self.update_plot_kwargs('model_error_band', dict(label=_model_error_name))
Expand All @@ -283,10 +290,12 @@ def _get_subplot_kwargs(self, plot_index, plot_type):
_plot_style_as = _subplots[plot_type].get('plot_style_as', plot_type)

# retrieve default plot keywords from style config
_kwargs = dict(kc_plot_style(self.PLOT_STYLE_CONFIG_DATA_TYPE, _plot_style_as, 'plot_kwargs'))
_kwargs = dict(
kc_plot_style(self.PLOT_STYLE_CONFIG_DATA_TYPE, _plot_style_as, 'plot_kwargs'))

# initialize property cycler from style config and commit keywords
_prop_cycler_args = kc_plot_style(self.PLOT_STYLE_CONFIG_DATA_TYPE, _plot_style_as, 'property_cycler')
_prop_cycler_args = kc_plot_style(self.PLOT_STYLE_CONFIG_DATA_TYPE, _plot_style_as,
'property_cycler')
_prop_cycler = Cycler(*_prop_cycler_args)
_kwargs.update(**_prop_cycler.get(plot_index))

Expand All @@ -295,7 +304,7 @@ def _get_subplot_kwargs(self, plot_index, plot_type):

# remove keywords set to the special value '__del__'
_kwargs = {
_k : _v
_k: _v
for _k, _v in six.iteritems(_kwargs)
if _v != '__del__'
}
Expand All @@ -309,7 +318,8 @@ def _get_subplot_kwargs(self, plot_index, plot_type):
# calculate zorder if not explicitly given
_n_defined_plot_types = len(_subplots)
if 'zorder' not in _kwargs:
_kwargs['zorder'] = plot_index * _n_defined_plot_types + list(_subplots).index(plot_type)
_kwargs['zorder'] = plot_index * _n_defined_plot_types + list(_subplots).index(
plot_type)

_container_valid = _subplots[plot_type].get("container_valid", None)
if _container_valid is not None:
Expand Down Expand Up @@ -659,6 +669,25 @@ def plot_residual(self, target_axes, error_contributions=('data',), **kwargs):
**kwargs
)

def plot_pull(self, target_axes, error_contributions=('data',), **kwargs):
Copy link
Member

Choose a reason for hiding this comment

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

I think the default for the shift (y location) should be the model error contribution, and the default for the y_err of the errorbar should be the data error.

"""Plot the pull to a :py:obj:`matplotlib.axes.Axes` object.

:param matplotlib.axes.Axes target_axes: The :py:obj:`matplotlib` axes used for plotting.
:param error_contributions: Which error contributions to include when plotting the data.
Can either be ``data``, ``'model'`` or both.
Copy link
Collaborator

Choose a reason for hiding this comment

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

The use of quotation marks is inconsistent here. Since it seems to be the result of copy-pasting, please fix it for the other methods as well.

:type error_contributions: str or Tuple[str]
:param dict kwargs: Keyword arguments accepted by :py:obj:`matplotlib.pyplot.errorbar`.
:return: plot handle(s)
"""
# TODO: how to handle case when x and y error/model differ?
return target_axes.errorbar(
self.data_x,
(self.data_y - self.model_y) / self._get_total_error(error_contributions),
xerr=self.data_xerr,
yerr=self._get_total_error(error_contributions),
**kwargs
)

# Overridden by multi plot adapters
def get_formatted_model_function(self, **kwargs):
"""return model function string"""
Expand All @@ -670,6 +699,7 @@ def model_function_parameter_formatters(self):
"""The model function parameter formatters, excluding the independent variable."""
return self._fit.model_function.formatter.par_formatters


# -- must come last!


Expand Down Expand Up @@ -769,11 +799,12 @@ def _create_figure_axes(self, axes_keys, height_ratios=None, width_ratios=None,

# create named axes
self._current_axes = self._figure_dicts[-1]['axes'] = {
_k : self._current_figure.add_subplot(_plot_axes_gs[_i, 0])
_k: self._current_figure.add_subplot(_plot_axes_gs[_i, 0])
for _i, _k in enumerate(axes_keys)
}
# create a fake axes for the legend
self._current_axes['__legendfakeaxes__'] = self._current_figure.add_subplot(_plot_axes_gs[:, 1])
self._current_axes['__legendfakeaxes__'] = self._current_figure.add_subplot(
_plot_axes_gs[:, 1])
self._current_axes['__legendfakeaxes__'].set_visible(False)

# make all axes share the 'x' axis of the first axes
Expand Down Expand Up @@ -810,11 +841,11 @@ def _plot_and_get_results(self, plot_indices=None):
if not _plot_adapter._fit.did_fit and not _plot_adapter.from_container:
warnings.warn(
"No fit has been performed for {}. Did you forget to run fit.do_fit()?"
.format(_plot_adapter._fit))
.format(_plot_adapter._fit))
elif not self._multifit.did_fit:
warnings.warn(
"No fit has been performed for {}. Did you forget to run fit.do_fit()?"
.format(self._multifit))
.format(self._multifit))

_plots = {}
for _i_pdc, _pdc in zip(plot_indices, _plot_adapters):
Expand All @@ -836,18 +867,18 @@ def _plot_and_get_results(self, plot_indices=None):
_plot_kwargs = _pdc._get_subplot_kwargs(_i_pdc, _pt)
if 'zorder' not in _plot_kwargs:
_plot_kwargs['zorder'] = 0
_plot_kwargs['zorder'] = _plot_kwargs['zorder'] - 10*_i_pdc
_plot_kwargs['zorder'] = _plot_kwargs['zorder'] - 10 * _i_pdc

_artist = _pdc.call_plot_method(_pt,
target_axes=self._get_axes(_axes_key),
**_plot_kwargs
)

_axes_plots.append({
'type' : _pt,
'fit_index' : _i_pdc,
'adapter' : _pdc,
'artist' : _artist,
'type': _pt,
'fit_index': _i_pdc,
'adapter': _pdc,
'artist': _artist,
})

if _pdc.x_range is not None:
Expand Down Expand Up @@ -975,7 +1006,8 @@ def _get_fit_info(self, plot_adapter, format_as_latex, asymmetric_parameter_erro
_info_text += _template.format(**_multi_info_dict)
return _info_text

def _render_legend(self, plot_results, axes_keys, fit_info=True, asymmetric_parameter_errors=False, **kwargs):
def _render_legend(self, plot_results, axes_keys, fit_info=True,
asymmetric_parameter_errors=False, **kwargs):
"""render the legend for axes `axes_keys`"""
for _axes_key in axes_keys:
_axes = self._get_axes(_axes_key)
Expand Down Expand Up @@ -1053,12 +1085,12 @@ def _render_legend(self, plot_results, axes_keys, fit_info=True, asymmetric_para
# and produce undesirable layouts

_leg = _axes.get_figure().legend(_hs_sorted, _ls_sorted,
mode=_mode,
borderaxespad=_borderaxespad,
ncol=_ncol,
fontsize=rcParams["font.size"],
handler_map={'_nokey_': DummyLegendHandler()},
**kwargs)
mode=_mode,
borderaxespad=_borderaxespad,
ncol=_ncol,
fontsize=rcParams["font.size"],
handler_map={'_nokey_': DummyLegendHandler()},
**kwargs)
_leg.set_zorder(_zorder)

# manually change bbox from figure to axes
Expand Down Expand Up @@ -1283,11 +1315,12 @@ def show(*args, **kwargs):
"""Convenience wrapper for matplotlib.pyplot.show()"""
plt.show(*args, **kwargs)


def plot(self, legend=True, fit_info=True, asymmetric_parameter_errors=False,
ratio=False, ratio_range=None, ratio_height_share=0.25,
residual=False, residual_range=None, residual_height_share=0.25,
plot_width_share=0.5, font_scale=1.0, figsize=None):
pull=False, pull_range=None, pull_height_share=0.25,
plot_width_share=0.5, figsize=None,
font_scale=1.0):
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is your reason for changing the order of figsize and font_scale?

"""
Plot data, model (and other subplots) for all child :py:obj:`Fit` objects.

Expand All @@ -1312,8 +1345,9 @@ def plot(self, legend=True, fit_info=True, asymmetric_parameter_errors=False,
:return: dictionary containing information about the plotted objects
:rtype: dict
"""
if ratio and residual:
raise NotImplementedError("Cannot plot ratio and residual at the same time.")
if sum([ratio, residual, pull]) > 1:
raise NotImplementedError(
"Cannot plot more than one of ratio, residual and pull at the same time.")

with rc_context(kafe2_rc):
rcParams["font.size"] *= font_scale
Expand All @@ -1329,6 +1363,10 @@ def plot(self, legend=True, fit_info=True, asymmetric_parameter_errors=False,
_axes_keys += ('residual',)
_height_ratios[0] -= residual_height_share
_height_ratios.append(residual_height_share)
elif pull:
_axes_keys += ('pull',)
_height_ratios[0] -= pull_height_share
_height_ratios.append(pull_height_share)

_all_plot_results = []
for i in range(len(self._fits) if self._separate_figs else 1):
Expand All @@ -1339,14 +1377,16 @@ def plot(self, legend=True, fit_info=True, asymmetric_parameter_errors=False,
figsize=figsize,
)

_plot_results = self._plot_and_get_results(plot_indices=(i,) if self._separate_figs else None)
_plot_results = self._plot_and_get_results(
plot_indices=(i,) if self._separate_figs else None)

# set axis scales for the main plot, x-axis is shared with ratio plot
self._get_axes('main').set_xscale(self.x_scale[i])
self._get_axes('main').set_yscale(self.y_scale[i])

if legend:
self._render_legend(plot_results=_plot_results, axes_keys=('main',), fit_info=fit_info,
self._render_legend(plot_results=_plot_results, axes_keys=('main',),
fit_info=fit_info,
asymmetric_parameter_errors=asymmetric_parameter_errors)

self._adjust_plot_ranges(_plot_results)
Expand All @@ -1362,14 +1402,14 @@ def plot(self, legend=True, fit_info=True, asymmetric_parameter_errors=False,
_ratio_label = kc('fit', 'plot', 'ratio_label')
_axis.set_ylabel(_ratio_label)
if ratio_range is None:
_plot_adapters = (self._get_plot_adapters()[i:i+1] if self._separate_figs
_plot_adapters = (self._get_plot_adapters()[i:i + 1] if self._separate_figs
else self._get_plot_adapters())
_max_abs_deviation = 0
for _plot_adapter in _plot_adapters:
_max_abs_deviation = max(_max_abs_deviation, np.max(
(
np.abs(_plot_adapter.data_yerr)
+ np.abs(_plot_adapter.data_y - _plot_adapter.model_y)
np.abs(_plot_adapter.data_yerr)
+ np.abs(_plot_adapter.data_y - _plot_adapter.model_y)
) / np.abs(_plot_adapter.model_y)
))
# Small gap between highest error bar and plot border:
Expand All @@ -1378,12 +1418,13 @@ def plot(self, legend=True, fit_info=True, asymmetric_parameter_errors=False,
_axis.set_ylim((_low, _high))
else:
_axis.set_ylim(ratio_range)

if residual:
_axis = self._current_axes['residual']
_residual_label = kc('fit', 'plot', 'residual_label')
_axis.set_ylabel(_residual_label)
if residual_range is None:
_plot_adapters = (self._get_plot_adapters()[i:i+1] if self._separate_figs
_plot_adapters = (self._get_plot_adapters()[i:i + 1] if self._separate_figs
else self._get_plot_adapters())
_max_abs_deviation = 0
for _plot_adapter in _plot_adapters:
Expand All @@ -1398,6 +1439,27 @@ def plot(self, legend=True, fit_info=True, asymmetric_parameter_errors=False,
else:
_axis.set_ylim(residual_range)

if pull:
_axis = self._current_axes['pull']
_pull_label = kc('fit', 'plot', 'pull_label')
Copy link
Member

Choose a reason for hiding this comment

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

There is no default value implemented. This needs to be added here

residual_label: 'Residual'

Same as the residual label.

_axis.set_ylabel(_pull_label)
if pull_range is None:
_plot_adapters = (
self._get_plot_adapters()[i:i + 1] if self._separate_figs
else self._get_plot_adapters())
_max_abs_deviation = 0
for _plot_adapter in _plot_adapters:
_max_abs_deviation = max(_max_abs_deviation, np.max(
np.abs(_plot_adapter.data_yerr)
+ np.abs(_plot_adapter.data_y - _plot_adapter.model_y)
))
# Small gap between highest error bar and plot border:
_low = -_max_abs_deviation * 1.05
_high = _max_abs_deviation * 1.05
_axis.set_ylim((_low, _high))
else:
_axis.set_ylim(pull_range)

_all_plot_results.append(_plot_results)

self._current_results = _all_plot_results
Expand Down
Loading