Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Annotations] ValueError: lines do not intersect when computing tight bounding box containing arrow with filled paths #12820

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

Closed
marscher opened this issue Nov 16, 2018 · 17 comments · Fixed by #16682
Milestone

Comments

@marscher
Copy link

marscher commented Nov 16, 2018

Bug report

During computation of tight_layout of subplots containing a histogram one gets a ValueError in bezier.py:35: Given lines do not intersect. Please verify that the angles are not equal or differ by 180 degrees.

Could be related to #6076

See the attached script and csv files to reproduce the bug. I have marked three FIXME code lines, that are all conditions for the bug to be triggered.

  1. use subplots with more than one axes.
  2. use annotations with an arrowprops argument.

This seems to be a regression in matplotlib 3, since the LTS version is running fine.

test files: files.zip

import matplotlib.pyplot as plt
import numpy as np


def plot_1D_histogram_trajectories(data, centers=None,dtrajs=None, max_traj_length=200, ax=None):
    for n, _traj in enumerate(data):
        ax.hist(_traj, bins=30, alpha=.33, density=True, color='C{}'.format(n))
    ylims = ax.get_ylim()
    xlims = ax.get_xlim()

    for n, _traj in enumerate(data):
        ax.plot(
            _traj[:min(len(_traj), max_traj_length)],
            np.linspace(*ylims, min(len(_traj), max_traj_length)),
            alpha=0.6, color='C{}'.format(n), label='traj {}'.format(n))
        ax.plot(
                centers[dtrajs[n][:min(len(_traj), max_traj_length)]],
                np.linspace(*ylims, min(len(_traj), max_traj_length)),
                '.-', alpha=.6, label='dtraj {}'.format(n), linewidth=.3)
    ax.annotate(
        '', xy=(0.85 * xlims[1], 0.7 * ylims[1]), xytext=(0.85 * xlims[1], 0.3 * ylims[1]),
        # FIXME: if one omits this argument, it succeeds
        arrowprops=dict(),
    )
    ax.text(0.86 * xlims[1], 0.5 * ylims[1], '$x(time)$', ha='left', va='center', rotation=90)
    ax.legend(loc=2)

# FIXME: if only one subfigure is created it succeeds
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
data = [np.loadtxt('data.csv')]
dtraj = np.loadtxt('dtraj.csv').astype(int)
centers = np.loadtxt('centers.csv')

plot_1D_histogram_trajectories(data, centers=centers, dtrajs=[dtraj], ax=axes[0])
axes[1].plot(x=np.arange(10), y=np.arange(10))
fig.tight_layout()

leads to

  File "/home/mi/marscher/sources/pyemma_tutorials/notebooks/minimal_example.py", line 34, in <module>
    fig.tight_layout()
  File "/srv/public/marscher/miniconda3/envs/nbtut/lib/python3.6/site-packages/matplotlib/figure.py", line 2374, in tight_layout
    pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
  File "/srv/public/marscher/miniconda3/envs/nbtut/lib/python3.6/site-packages/matplotlib/tight_layout.py", line 366, in get_tight_layout_figure
    pad=pad, h_pad=h_pad, w_pad=w_pad)
  File "/srv/public/marscher/miniconda3/envs/nbtut/lib/python3.6/site-packages/matplotlib/tight_layout.py", line 115, in auto_adjust_subplotpars
    tight_bbox_raw = union([ax.get_tightbbox(renderer) for ax in subplots
  File "/srv/public/marscher/miniconda3/envs/nbtut/lib/python3.6/site-packages/matplotlib/tight_layout.py", line 116, in <listcomp>
    if ax.get_visible()])
  File "/srv/public/marscher/miniconda3/envs/nbtut/lib/python3.6/site-packages/matplotlib/axes/_base.py", line 4396, in get_tightbbox
    bbox = a.get_tightbbox(renderer)
  File "/srv/public/marscher/miniconda3/envs/nbtut/lib/python3.6/site-packages/matplotlib/artist.py", line 271, in get_tightbbox
    bbox = self.get_window_extent(renderer)
  File "/srv/public/marscher/miniconda3/envs/nbtut/lib/python3.6/site-packages/matplotlib/text.py", line 2425, in get_window_extent
    bboxes.append(self.arrow_patch.get_window_extent())
  File "/srv/public/marscher/miniconda3/envs/nbtut/lib/python3.6/site-packages/matplotlib/patches.py", line 548, in get_window_extent
    return self.get_path().get_extents(self.get_transform())
  File "/srv/public/marscher/miniconda3/envs/nbtut/lib/python3.6/site-packages/matplotlib/patches.py", line 4228, in get_path
    _path, fillable = self.get_path_in_displaycoord()
  File "/srv/public/marscher/miniconda3/envs/nbtut/lib/python3.6/site-packages/matplotlib/patches.py", line 4258, in get_path_in_displaycoord
    self.get_mutation_aspect())
  File "/srv/public/marscher/miniconda3/envs/nbtut/lib/python3.6/site-packages/matplotlib/patches.py", line 3218, in __call__
    return self.transmute(path, mutation_size, linewidth)
  File "/srv/public/marscher/miniconda3/envs/nbtut/lib/python3.6/site-packages/matplotlib/patches.py", line 3718, in transmute
    tail_width / 2.)
  File "/srv/public/marscher/miniconda3/envs/nbtut/lib/python3.6/site-packages/matplotlib/bezier.py", line 381, in get_parallels
    cos_t2, sin_t2)
  File "/srv/public/marscher/miniconda3/envs/nbtut/lib/python3.6/site-packages/matplotlib/bezier.py", line 35, in get_intersection
    raise ValueError("Given lines do not intersect. Please verify that "
ValueError: Given lines do not intersect. Please verify that the angles are not equal or differ by 180 degrees.

Expected outcome

It works, if the arrowprops are omitted, which means not to draw an arrow.

Matplotlib version

  • Operating system: linux64
  • Matplotlib version: 3.0.2
  • Matplotlib backend (print(matplotlib.get_backend())): module://backend_interagg
  • Python version: 3.6
  • Other libraries: see attached conda_list.txt

Installed with conda from conda-forge.

@marscher
Copy link
Author

fyi @thempel

@marscher marscher changed the title do not intersect ValueError: lines do not intersect when drawing arrow with filled paths Nov 16, 2018
@marscher marscher changed the title ValueError: lines do not intersect when drawing arrow with filled paths ValueError: lines do not intersect when computing tight bounding box containing arrow with filled paths Nov 16, 2018
@anntzer
Copy link
Contributor

anntzer commented Nov 16, 2018

bisects to #11801.

@marscher
Copy link
Author

that was quick!

@marscher marscher changed the title ValueError: lines do not intersect when computing tight bounding box containing arrow with filled paths [Annotations] ValueError: lines do not intersect when computing tight bounding box containing arrow with filled paths Nov 16, 2018
@ImportanceOfBeingErnest
Copy link
Member

Since I have currently no idea at which point in the code this needs to be fixed, a workaround is to not use parallel lines,

ax.annotate('', xy=(0.8500001 * xlims[1], 0.7 * ylims[1]), 
                xytext=(0.85 * xlims[1], 0.3 * ylims[1]), arrowprops=dict(), )

@iruletheworld
Copy link

iruletheworld commented Feb 28, 2019

I would like to report that I got this error also when doing animation with matplotlib and this particular error did not occur in 2017. I only ran into it after updating matplotlib. I found that I would not run into this error if I disable a certain line in the animation.

Also found out that if I downgrade to ver 2.3.3, then it works.

@darienmorrow
Copy link

I also would like to report getting this error. I am on Matplotlib 3.1.3 I will note that by switching the backend to TkAggmatplotlib.use('TkAgg') I was able to save the figure as desired.

@tacaswell tacaswell added this to the v3.2.1 milestone Feb 25, 2020
@tacaswell
Copy link
Member

@darienmorrow do you have a more minimal example that reproduces this?

@ksunden
Copy link
Member

ksunden commented Feb 25, 2020

I was working with @darienmorrow on his figure, I was unable to reproduce on current master (9895288)

@tacaswell
Copy link
Member

Can you try with the v3.2.0 rc (pip install --pre -U matplotlib )?

@ksunden
Copy link
Member

ksunden commented Feb 25, 2020

Okay, so it is backend dependent (I have not done an exhaustive search, but there is something weird going on, that I cannot replicate all behavior in a clean venv)

So (@darienmorrow uses conda on his main machine, for what it is worth, unless otherwise noted, packages are from conda-forge):

In his base venv, the figure fails to build under Agg and Qt5Agg, but succeeds under TkAgg
His base venv is python 3.6.7, mpl 3.1.3, installed via conda-forge.

All other configurations I have tried fail only under Qt5Agg (having tested Agg, Qt5Agg, TkAgg).

This includes clean venv installs only, but including Qt (installed the conda forge matplotlib full package, not just matplotlib-base):

  • Python 3.8, mpl 3.1.3

  • Python 3.6.7, mpl 3.1.3 (would have thought this to be functionally identical to the original venv, but something isn't the same, not sure what)

  • Python 3.8, mpl 3.2.0rc2 (from pip, after first installing 3.1.3 from conda-forge, to maintain all dependencies as identical)

  • Python 3.8, matplotlib freshly pulled from master (dc1f0d9) (On my own machine, not conda, just did more testing, was able to actually reproduce, I hadn't zeroed in on Qt5Agg as the major culprit yet, did not have qt bindings installed yet)

Unfortunately, this figure is burried in a fairly long script, and attempts to break out only the failing parts have not triggered the error. I may have another go at it, though.

@ksunden
Copy link
Member

ksunden commented Feb 26, 2020

Okay, I have a simplified script. It seems to be a very unlucky rounding condition.

import matplotlib as mpl
mpl.use("qt5agg")
import matplotlib.pyplot as plt
import pathlib

print(mpl.get_backend())

fig_type = '.png'
here = pathlib.Path(__file__).parent
save=True

nrows =4
ncols = 4
hspace = wspace = 0.09523809523809526
fig = plt.figure(figsize=[6.5, 6.5])
fig.subplots_adjust(1/6.5, 1/6.5, 1-1/6.5, 1-1/6.5)
gs = mpl.gridspec.GridSpec(nrows, ncols, hspace=hspace, wspace=wspace)
axstark = plt.subplot(gs[2:4,0:2])
axstarkins = axstark.inset_axes([0.0,0.0,0.8,1])
ax = axstarkins
# plot states
y0 = [0.2, .8]
x0 = [.2,.45]
y0p = [.3,.7]
# pump arrows
x = (x0[1] - x0[0]) / 2 + x0[0]
arrowprops={"width":1, "headwidth":7, "headlength":7}
ax.annotate(s='', xy=(x-.04,y0p[1]), xytext=(x-.04, y0[0]), arrowprops=arrowprops)
ax.annotate(s='', xy=(x+.04,y0p[0]), xytext=(x+.04, y0[1]), arrowprops=arrowprops)
# finish up
if save:
    p = "intro_fig"+fig_type
    plt.savefig(here / p)

Some notes:

  • It is very sensitive to the hspace/wspace number (13 sig figs, any larger perturbation, the error disappears). I just made our create_figure function print out what it actually passed to gridspec when it was failing.
  • It did NOT fail if I did not use inset_axes, and just used the whole axis from that subplot (I do not think this is due to the fact that they are inset, just that it changes the positions and proportions to be just right, the way it is proportioned)
  • It does NOT fail if I omit the subplots_adjust call
  • Both annotate calls are needed to trigger the error
  • Many of the numbers (such as x and y positions, figure size) are also sensative, but I have not quatitated how sensitive (just added e.g. a 1 after the least significant digit)
  • Does NOT fail if I take "headlength" out of arrowprops, continues to fail if the other params are removed

Also, the figure script is why I opened up #15049, just noticed that we are still explicitly passing s here... @darienmorrow, may be a good idea to just pass that in as a positional arg, since the name is changing

@tacaswell
Copy link
Member

Yikes, thanks for running this to ground @ksunden !

From a very cursory investigation it looks like we have one part of the code that is checking "are you parallel" using one threshold and then calling code that checks "are you not parallel" using a slightly different threshold.

@anntzer
Copy link
Contributor

anntzer commented Feb 26, 2020

Per @tacaswell's comment, the following patch appears to fix the issue:

diff --git i/lib/matplotlib/bezier.py w/lib/matplotlib/bezier.py
index 9e347ce87..8cec64f15 100644
--- i/lib/matplotlib/bezier.py
+++ w/lib/matplotlib/bezier.py
@@ -389,23 +389,23 @@ def get_parallels(bezier2, width):
     # find cm_left which is the intersecting point of a line through
     # c1_left with angle t1 and a line through c2_left with angle
     # t2. Same with cm_right.
-    if parallel_test != 0:
-        # a special case for a straight line, i.e., angle between two
-        # lines are smaller than some (arbitrary) value.
-        cmx_left, cmy_left = (
-            0.5 * (c1x_left + c2x_left), 0.5 * (c1y_left + c2y_left)
-        )
-        cmx_right, cmy_right = (
-            0.5 * (c1x_right + c2x_right), 0.5 * (c1y_right + c2y_right)
-        )
-    else:
+    try:
         cmx_left, cmy_left = get_intersection(c1x_left, c1y_left, cos_t1,
                                               sin_t1, c2x_left, c2y_left,
                                               cos_t2, sin_t2)
-
         cmx_right, cmy_right = get_intersection(c1x_right, c1y_right, cos_t1,
                                                 sin_t1, c2x_right, c2y_right,
                                                 cos_t2, sin_t2)
+    except ValueError:
+        # Special case straight lines, i.e., angle between two lines is
+        # less than the threshold used by get_intersection (we don't use
+        # check_if_parallel as the threshold is not the same).
+        cmx_left, cmy_left = (
+            0.5 * (c1x_left + c2x_left), 0.5 * (c1y_left + c2y_left)
+        )
+        cmx_right, cmy_right = (
+            0.5 * (c1x_right + c2x_right), 0.5 * (c1y_right + c2y_right)
+        )
 
     # the parallel Bezier lines are created with control points of
     # [c1_left, cm_left, c2_left] and [c1_right, cm_right, c2_right]

Just needs a test, yada yada.

@ksunden
Copy link
Member

ksunden commented Feb 26, 2020

def test_almost_parallel():
    matplotlib.bezier.get_parallels([[1, 1],[1, 2],[1 + 1e-14, 3 ]], 1)

That is a test which fails (raises ValueError) on current master (8ac3ea1), but passes with the above patch applied.

We could add checks that it gives expected results, but really all that we are looking for is that it doesn't error out. Unless I am mistaken, there are no unit tests targeting only bezier.py (though it is called often in the rest of the test suite)

@anntzer
Copy link
Contributor

anntzer commented Feb 27, 2020

I don't think we should worry about such a case; clearly we should be able to pick whatever threshold for parallel-checking makes sense for us (in a sense get_parallels should be private API).

@tacaswell
Copy link
Member

I agree with @anntzer . We don't usually consider tweaking floating point thresholds an API change.

@ksunden
Copy link
Member

ksunden commented Feb 27, 2020

Fair enough, then it is unclear to me that there are any actual tests required in this instance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants