-
Notifications
You must be signed in to change notification settings - Fork 5
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
Add residual plot overloading the -
operator
#275
Closed
Closed
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
f74f6fd
initial draft of residual plot
nvaytet 77fe116
share xaxis and hide mpl toolbar
nvaytet 552686a
improve residual plot appearance
nvaytet 625a8fe
raise for 2d inputs
nvaytet c0c5f0d
docstring
nvaytet 5e977dc
add notebook on operators
nvaytet 86857e7
add unit tests
nvaytet 00b288e
isort
nvaytet d0c3c0c
Merge branch 'main' into residual-plot
nvaytet 953a316
Merge branch 'main' into residual-plot
nvaytet 65108b1
Merge branch 'residual-plot' of github.com:scipp/plopp into residual-…
nvaytet 77b0c09
use new make_figure
nvaytet File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
# SPDX-License-Identifier: BSD-3-Clause | ||
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) | ||
|
||
import matplotlib.pyplot as plt | ||
|
||
from ..protocols import FigureLike | ||
from .utils import copy_figure, make_figure | ||
|
||
|
||
class ResidualPlot: | ||
def __init__(self, main_panel: FigureLike, res_panel: FigureLike): | ||
self.main_panel = main_panel | ||
self.res_panel = res_panel | ||
self.panels = [self.main_panel, self.res_panel] | ||
|
||
def __getitem__(self, key): | ||
return self.panels[key] | ||
|
||
def __repr__(self): | ||
return f"ResidualPlot(main_panel={self.main_panel}, res_panel={self.res_panel})" | ||
|
||
def _repr_mimebundle_(self, *args, **kwargs) -> dict: | ||
return self.main_panel._repr_mimebundle_(*args, **kwargs) | ||
|
||
|
||
def residuals(main_fig: FigureLike, reference: FigureLike) -> ResidualPlot: | ||
""" | ||
Create a residual plot from two figures, using the data from the second figure as | ||
the reference the residuals are computed from. | ||
|
||
Parameters | ||
---------- | ||
main_fig: | ||
The main figure. | ||
reference: | ||
The reference figure. | ||
|
||
Returns | ||
------- | ||
: | ||
A figure with a main panel showing the data from both the main and reference | ||
figures, and a smaller 'residuals' panel at the bottom displaying the difference | ||
between the data from the main figure with the data from the reference figure. | ||
""" | ||
# If there is a colormapper, we are dealing with a 2d figure | ||
if hasattr(main_fig._view, 'colormapper') or hasattr( | ||
reference._view, 'colormapper' | ||
): | ||
raise TypeError("The residual plot only supports 1d figures.") | ||
if len(reference.artists) != 1: | ||
raise TypeError( | ||
"The reference figure must contain exactly one line to " | ||
"compute residuals." | ||
) | ||
|
||
fig = make_figure(figsize=(6.0, 4.0)) | ||
gs = fig.add_gridspec( | ||
2, | ||
1, | ||
height_ratios=(4, 1), | ||
hspace=0.0, | ||
) | ||
|
||
main_ax = fig.add_subplot(gs[0]) | ||
res_ax = fig.add_subplot(gs[1], sharex=main_ax) | ||
main_panel = copy_figure(main_fig, ax=main_ax) | ||
main_canvas = main_panel.canvas | ||
if main_canvas.is_widget(): | ||
fig.canvas.toolbar_visible = False | ||
fig.canvas.header_visible = False | ||
ref_node = next(iter(reference.graph_nodes.values())) | ||
data = ref_node() | ||
if not data.name: | ||
data.name = "reference" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This modifies the input, right? Seems like you should make a copy of |
||
ref_node.add_view(main_panel) | ||
main_panel._view.render() | ||
# main_view._view.artists[ref_node.id]._line.set_zorder(-10) | ||
if main_canvas._legend: | ||
main_ax.legend() | ||
diff_nodes = [n - ref_node for n in main_fig.graph_nodes.values()] | ||
res_panel = reference.__class__(reference._view.__class__, *diff_nodes, ax=res_ax) | ||
|
||
main_ax.tick_params( | ||
top=True, labeltop=True, bottom=False, labelbottom=False, direction='out' | ||
) | ||
main_ax.secondary_xaxis("bottom").tick_params( | ||
axis="x", | ||
direction="in", | ||
top=False, | ||
labeltop=False, | ||
bottom=True, | ||
labelbottom=False, | ||
) | ||
res_ax.tick_params( | ||
top=False, labeltop=False, bottom=True, labelbottom=True, direction='out' | ||
) | ||
res_ax.secondary_xaxis("top").tick_params( | ||
axis="x", | ||
direction="in", | ||
top=True, | ||
labeltop=False, | ||
bottom=False, | ||
labelbottom=False, | ||
) | ||
|
||
return ResidualPlot(main_panel=main_panel, res_panel=res_panel) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
# SPDX-License-Identifier: BSD-3-Clause | ||
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) | ||
|
||
import numpy as np | ||
import pytest | ||
|
||
import plopp as pp | ||
from plopp.backends.matplotlib.residuals import residuals | ||
from plopp.data.testing import data_array | ||
|
||
|
||
def test_single_line(): | ||
ref = data_array(ndim=1) | ||
a = ref.copy() | ||
a.values += np.random.uniform(-0.25, 0.25, size=len(a)) | ||
fig1 = ref.plot() | ||
fig2 = a.plot() | ||
fig = residuals(fig2, fig1) | ||
assert len(fig.main_panel.fig.get_axes()) == 2 | ||
assert len(fig.main_panel.artists) == 2 | ||
assert len(fig.res_panel.artists) == 1 | ||
|
||
|
||
def test_three_lines(): | ||
ref = data_array(ndim=1) | ||
a = ref.copy() | ||
a.values += np.random.uniform(-0.25, 0.25, size=len(a)) | ||
b = ref.copy() | ||
b.values += np.random.uniform(-1.0, 1.0, size=len(b)) | ||
c = ref.copy() | ||
fig1 = ref.plot() | ||
fig2 = pp.plot({'a': a, 'b': b, 'c': c}) | ||
fig = residuals(fig2, fig1) | ||
assert len(fig.main_panel.artists) == 4 | ||
assert len(fig.res_panel.artists) == 3 | ||
|
||
|
||
def test_with_bin_edges(): | ||
ref = data_array(ndim=1, binedges=True) | ||
a = ref.copy() | ||
a.values += np.random.uniform(-0.25, 0.25, size=len(a)) | ||
fig1 = ref.plot() | ||
fig2 = a.plot() | ||
fig = residuals(fig2, fig1) | ||
for artist in fig.main_panel.artists.values(): | ||
assert artist._line.get_linestyle() == '-' | ||
for artist in fig.res_panel.artists.values(): | ||
assert artist._line.get_linestyle() == '-' | ||
|
||
|
||
def test_raises_when_given_2d_plots(): | ||
da1d = data_array(ndim=1) | ||
da2d = data_array(ndim=2) | ||
msg = "The residual plot only supports 1d figures." | ||
with pytest.raises(TypeError, match=msg): | ||
residuals(da2d.plot(), da1d.plot()) | ||
with pytest.raises(TypeError, match=msg): | ||
residuals(da1d.plot(), da2d.plot()) | ||
with pytest.raises(TypeError, match=msg): | ||
residuals(da2d.plot(), da2d.plot()) | ||
|
||
|
||
def test_raises_when_reference_contains_multiple_lines(): | ||
ref = data_array(ndim=1) | ||
a = ref.copy() | ||
a.values += np.random.uniform(-0.25, 0.25, size=len(a)) | ||
b = ref.copy() | ||
b.values += np.random.uniform(-1.0, 1.0, size=len(b)) | ||
fig1 = pp.plot({'a': a, 'b': b}) | ||
fig2 = ref.plot() | ||
with pytest.raises( | ||
TypeError, | ||
match="The reference figure must contain exactly one line", | ||
): | ||
residuals(fig2, fig1) | ||
|
||
|
||
def test_single_line_operator_minus(): | ||
ref = data_array(ndim=1) | ||
a = ref.copy() | ||
a.values += np.random.uniform(-0.25, 0.25, size=len(a)) | ||
fig1 = ref.plot() | ||
fig2 = a.plot() | ||
fig = fig2 - fig1 | ||
assert len(fig.main_panel.fig.get_axes()) == 2 | ||
assert len(fig.main_panel.artists) == 2 | ||
assert len(fig.res_panel.artists) == 1 | ||
|
||
|
||
def test_three_lines_operator_minus(): | ||
ref = data_array(ndim=1) | ||
a = ref.copy() | ||
a.values += np.random.uniform(-0.25, 0.25, size=len(a)) | ||
b = ref.copy() | ||
b.values += np.random.uniform(-1.0, 1.0, size=len(b)) | ||
c = ref.copy() | ||
fig1 = ref.plot() | ||
fig2 = pp.plot({'a': a, 'b': b, 'c': c}) | ||
fig = fig2 - fig1 | ||
assert len(fig.main_panel.artists) == 4 | ||
assert len(fig.res_panel.artists) == 3 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missed this earlier, but wouldn't it make more sense to accept a data array instead of a figure? Why would you require the user to plot the reference data first?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I'm starting to think that the whole approach using operators is a bad idea. I also ran into problems if, for example, the user made a figure where they changed the line style or the line color. Then I had to make sure that was copied over.
I think we need to go back to some of the suggestions in #201 and use a dedicated function, and drop the operators.