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

[Bug]: ValueError thrown when levels is set to a lower value than vmin when using contours method of Axes #26531

Closed
ymzayek opened this issue Aug 15, 2023 · 4 comments · Fixed by #26601
Labels
Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.
Milestone

Comments

@ymzayek
Copy link

ymzayek commented Aug 15, 2023

Bug summary

With the recent mpl prerelease, we started to have a failure in the test suite of nilearn, a package that depends on matplotlib for visualization. I reproduced the error with a matplotlib example and it seems that when calling Axes.contours it's not possible to set the levels parameter to values less than vmin using array-like input. This happens even when vmax is also set by the user and it seems to be because vmax ends up being set to 0 internally. The code reproduced below will throw ValueError: minvalue must be less than or equal to maxvalue. I'm not sure if it's a usage error or a bug so let me know if this is expected behavior, however as I understand it the values for levels and vmin and vmax should be independent.

Code for reproduction

import matplotlib.pyplot as plt
import numpy as np
  
feature_x = np.arange(0, 50, 2)
feature_y = np.arange(0, 50, 3)
  
# Creating 2-D grid of features
[X, Y] = np.meshgrid(feature_x, feature_y)
  
fig, ax = plt.subplots(1, 1)
  
Z = np.cos(X / 2) + np.sin(Y / 4)
  
# plots contour lines
ax.contour(X, Y, Z, levels=[0.0, 0.1], vmin=0.2, vmax=0.8)

Actual outcome

ValueError                                Traceback (most recent call last)
[/home/yasmin/.config/Code/User/globalStorage/buenon.scratchpads/scratchpads/47a6066b9dfaca1c2b13ea2e99f4e929/scratch.py](https://file+.vscode-resource.vscode-cdn.net/home/yasmin/.config/Code/User/globalStorage/buenon.scratchpads/scratchpads/47a6066b9dfaca1c2b13ea2e99f4e929/scratch.py) in ()
     [109](file:///home/yasmin/.config/Code/User/globalStorage/buenon.scratchpads/scratchpads/47a6066b9dfaca1c2b13ea2e99f4e929/scratch.py?line=108) Z = np.cos(X [/](https://file+.vscode-resource.vscode-cdn.net/) 2) + np.sin(Y [/](https://file+.vscode-resource.vscode-cdn.net/) 4)
     [111](file:///home/yasmin/.config/Code/User/globalStorage/buenon.scratchpads/scratchpads/47a6066b9dfaca1c2b13ea2e99f4e929/scratch.py?line=110) # plots contour lines
---> [112](file:///home/yasmin/.config/Code/User/globalStorage/buenon.scratchpads/scratchpads/47a6066b9dfaca1c2b13ea2e99f4e929/scratch.py?line=111) ax.contour(X, Y, Z, levels=[0.0, 0.1], vmin=0.2, vmax=0.8)

File [~/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/__init__.py:1465](https://file+.vscode-resource.vscode-cdn.net/home/yasmin/nilearn/nilearn/~/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/__init__.py:1465), in _preprocess_data..inner(ax, data, *args, **kwargs)
   [1462](file:///home/yasmin/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/__init__.py?line=1461) @functools.wraps(func)
   [1463](file:///home/yasmin/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/__init__.py?line=1462) def inner(ax, *args, data=None, **kwargs):
   [1464](file:///home/yasmin/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/__init__.py?line=1463)     if data is None:
-> [1465](file:///home/yasmin/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/__init__.py?line=1464)         return func(ax, *map(sanitize_sequence, args), **kwargs)
   [1467](file:///home/yasmin/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/__init__.py?line=1466)     bound = new_sig.bind(ax, *args, **kwargs)
   [1468](file:///home/yasmin/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/__init__.py?line=1467)     auto_label = (bound.arguments.get(label_namer)
   [1469](file:///home/yasmin/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/__init__.py?line=1468)                   or bound.kwargs.get(label_namer))

File [~/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/axes/_axes.py:6498](https://file+.vscode-resource.vscode-cdn.net/home/yasmin/nilearn/nilearn/~/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/axes/_axes.py:6498), in Axes.contour(self, *args, **kwargs)
   [6489](file:///home/yasmin/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/axes/_axes.py?line=6488) """
   [6490](file:///home/yasmin/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/axes/_axes.py?line=6489) Plot contour lines.
   [6491](file:///home/yasmin/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/axes/_axes.py?line=6490) 
   (...)
   [6495](file:///home/yasmin/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/axes/_axes.py?line=6494) %(contour_doc)s
   [6496](file:///home/yasmin/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/axes/_axes.py?line=6495) """
   [6497](file:///home/yasmin/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/axes/_axes.py?line=6496) kwargs['filled'] = False
-> [6498](file:///home/yasmin/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/axes/_axes.py?line=6497) contours = mcontour.QuadContourSet(self, *args, **kwargs)
...
-> [1361](file:///home/yasmin/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/colors.py?line=1360)     raise ValueError("minvalue must be less than or equal to maxvalue")
   [1362](file:///home/yasmin/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/colors.py?line=1361) else:
   [1363](file:///home/yasmin/miniconda3/envs/nilearn-dev/lib/python3.10/site-packages/matplotlib/colors.py?line=1362)     if clip:

ValueError: minvalue must be less than or equal to maxvalue

Expected outcome

A plot matplotlib.contour.QuadContourSet object

Additional information

The test that led to this failure did pass before this version

Operating system

Ubuntu 22.04

Matplotlib Version

3.8.0rc1

Matplotlib Backend

module://matplotlib_inline.backend_inline

Python version

3.10.4

Jupyter version

No response

Installation

pip

@ksunden
Copy link
Member

ksunden commented Aug 15, 2023

This appears to be related to #25247, which changed the inheritance of the ContourSet.

In this case, it is not actually a fatal error, as far as I can tell... when I run it it continues after printing the traceback.

The problem is that it is doing checks at every step, so vmin gets updated before vmax.

Essentially it is first autoscalling, then applying the vmin/vmax that were specified, so it goes:

vmin   vmax
----   ----
0.0    None
0.0    0.1    Autoscaled
0.2    0.1    vmin set (error happens here)
0.2    0.8    vmax set (back to OK)

This pattern of changed being called is not actually new, but what did change to is that now the callback calls update_scalarmappable where it didn't before, which causes the norm to be evaluated in its incomplete state.

This can be suppressed (on our end) by doing:

diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py
index 79f66b8961..ab88faf28c 100644
--- a/lib/matplotlib/contour.py
+++ b/lib/matplotlib/contour.py
@@ -887,11 +887,12 @@ class ContourSet(ContourLabeler, mcoll.Collection):
         self.set_cmap(cmap)
         if norm is not None:
             self.set_norm(norm)
-        if vmin is not None:
-            self.norm.vmin = vmin
-        if vmax is not None:
-            self.norm.vmax = vmax
-        self._process_colors()
+        with self.norm.callbacks.blocked(signal="changed"):
+            if vmin is not None:
+                self.norm.vmin = vmin
+            if vmax is not None:
+                self.norm.vmax = vmax
+            self._process_colors()
 
         if self._paths is None:
             self._paths = self._make_paths_from_contour_generator()

But perhaps there are more areas where such supression needs to happen? Or perhaps update_scalarmappable is not needed in changed?

@anntzer thoughts?

@anntzer
Copy link
Contributor

anntzer commented Aug 15, 2023

A better fix is likely possible, but your patch seems correct at first glance.

@oscargus oscargus added this to the v3.8.0 milestone Aug 16, 2023
@ymzayek
Copy link
Author

ymzayek commented Aug 17, 2023

Thanks @ksunden and @anntzer for looking into it!

@ksunden ksunden added the Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. label Aug 17, 2023
@anntzer
Copy link
Contributor

anntzer commented Aug 23, 2023

(As a side point, perhaps there should be a helper method that lets one set both vmin and vmax on a norm and only trigger the validation and signals once both have been set -- in essence wrapping the patch written by @ksunden)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants