Skip to content

[Bug]: Wrong scaling of NonUniformImage with logarithmic axes #29368

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
david-zwicker opened this issue Dec 21, 2024 · 13 comments
Closed

[Bug]: Wrong scaling of NonUniformImage with logarithmic axes #29368

david-zwicker opened this issue Dec 21, 2024 · 13 comments
Labels
Community support Users in need of help.

Comments

@david-zwicker
Copy link

Bug summary

I'm trying to produce a density plot of logarithmically spaced data. Using NonUniformImage used to work before matplotlib version 3.10, where I know exhibit a weirdly scaled axis.

Code for reproduction

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.image import NonUniformImage

x = np.linspace(0, 1, 2)
y = np.geomspace(1e1, 1e5, 3)
z = np.random.uniform(size=(len(x), len(y)))

_, (ax1, ax2) = plt.subplots(ncols=2)

# create the non-uniform image
im = NonUniformImage(ax1, extent=[0, 1, 1e0, 1e6], origin="lower")
im.set_data(x, y, z.T)
ax1.add_image(im)
ax1.set_yscale("log")
ax1.set_ylim(1e0, 1e6)  # omitting this leads to very weird behavior
ax1.set_title("NonUniformImage")

# use pcolormesh to get reference image
ax2.pcolormesh([0, 0.5, 1], [1e0, 1e2, 1e4, 1e6], z.T)
ax2.set_yscale("log")
ax2.set_title("pcolormesh")

Actual outcome

The code above produces the following image

b32e9b79-dd12-44f9-b472-6ef09f886ac3

Expected outcome

I expect the left panel to look like the right panel, which was produced using pcolormesh. The reason I don't want to use pcolormesh for these plots is that the quality in PDFs is often much worse (with lines showing up between patches) and the file size can easily be 10 times larger.

Additional information

I recently asked a related question on Stackoverflow, which has some additional details.

Operating system

OS/X

Matplotlib Version

3.10.0

Matplotlib Backend

module://matplotlib_inline.backend_inline

Python version

3.12.8

Jupyter version

7.3.1

Installation

conda

@david-zwicker david-zwicker changed the title [Bug]: Wrong NonUniformImage with logarithmic axes [Bug]: Wrong scaling of NonUniformImage with logarithmic axes Dec 21, 2024
@timhoffm
Copy link
Member

timhoffm commented Dec 21, 2024

I've not bisected, but the change is possibly related to #27964.

I think the new behavior is actually correct. Pixel values are specified at their centers (y=[10^1, 10^3, 10^5]). The values in between are determined by the interpolation method (default: "nearest"). Therefore the pixel boundary between the pixels at 10^3 and 10^5 is at (10^5 - 10^3)/2 = 49500, which matches the image. This interpolation happens in data space and therefore is independent of the axis scaling.

I believe the 3.10 behavior is correct and it's a topic on expectation/documentation of the interpolation behavior.

@timhoffm timhoffm added the Community support Users in need of help. label Dec 21, 2024
@jklymak
Copy link
Member

jklymak commented Dec 21, 2024

For large pcolormesh in pdf use "rasterized=True".

@rcomer
Copy link
Member

rcomer commented Dec 21, 2024

Confirmed that the behaviour changed at #27964, but before that commit and back to v3.6 I get
test

@david-zwicker
Copy link
Author

For large pcolormesh in pdf use "rasterized=True".

I don't like this solution, because it is difficult to predict the right resolution. If the resolution is too coarse, details of the density will be lost. If the resolution is too fine, the file size is increased. In any case, the resolution would need to be much finer than the actual data to not move boundaries around.

I think there should be a stable solution for adding images with rectangular, equal-sized pixels with logarithmic scaling!

@jklymak
Copy link
Member

jklymak commented Dec 21, 2024

I don't like this solution, because it is difficult to predict the right resolution

Your output device sets the resolution. Indeed it looks to me like NonUniformImage just makes a 72 dpi raster for a PDF, which is worse than what you will get from setting the dpi of the pcolormesh raster. The reason is that PDF and ps do not have any concept of a "NonUniformImage", and hence when we write the output we can either rasterize (at some dpi) or we can make polygons. We choose to rasterize the NonUniformImage, which is exactly what happens with pcolormesh(..., rasterized=True). The alternative is to draw individual polygons, which is what pcolormesh does by default, but this will create the anti-aliasing artifacts you are seeing in PDF (btw those go away if you turn anti-aliasing off in your PDF viewer).

The only reason I think NonUniformImage exists is to be faster than pcolormesh. I'm not sure it particularly matters in 2024, and maybe we should just deprecate it.

@rcomer
Copy link
Member

rcomer commented Dec 21, 2024

Can you just use imshow and add a secondary axis for the ticks?

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mtick
from matplotlib.scale import InvertedLogTransform

x = np.linspace(0, 1, 2)
y = np.geomspace(1e1, 1e5, 3)
z = np.random.uniform(size=(len(x), len(y)))

fig, (ax1, ax2) = plt.subplots(ncols=2)

tr = InvertedLogTransform(10)

# create the image and add a secondary axis for log-scaled ticks
ax1.yaxis.set_visible(False)
im = ax1.imshow(z.T, extent=(0, 1, 0, 6), aspect="auto", origin="lower")
sec = ax1.secondary_yaxis("left", functions=tr)
sec.yaxis.set_major_locator(mtick.LogLocator())
sec.yaxis.set_minor_locator(mtick.LogLocator())
sec.yaxis.set_major_formatter(mtick.LogFormatterSciNotation())
sec.yaxis.set_tick_params(which="both", left=True)
ax1.set_title("imshow")

fig.colorbar(im, orientation="horizontal")

# use pcolormesh to get reference image
mesh = ax2.pcolormesh([0, 0.5, 1], [1e0, 1e2, 1e4, 1e6], z.T)
ax2.set_yscale("log")
ax2.set_title("pcolormesh")

fig.colorbar(mesh, orientation="horizontal")

plt.show()

image

I'm afraid I haven't figured out how to make the minor ticks show up. Hopefully someone else can help with that.

@david-zwicker
Copy link
Author

The only reason I think NonUniformImage exists is to be faster than pcolormesh. I'm not sure it particularly matters in 2024, and maybe we should just deprecate it.

That's helpful! I understand now that NonUniformImage in principle does lead to the same result as a rasterized pcolormesh. I played around with these two options and their behavior indeed seems to be very similar.

However, I actually don't need a non-uniform image in my case – all I want is to display my uniform image with a log-scaled axis. This used to work several years ago, but now imshow rescales the image when I set the axis to log-scale. I thought NonUniformImage would be a good work-around, but I now see that this is not really the case.

Can you just use imshow and add a secondary axis for the ticks?

This works in principle, but it becomes awkward when one adds a colorbar later, which might steal space from the original axis.

@rcomer
Copy link
Member

rcomer commented Dec 21, 2024

I updated my comment to include colorbars.

@david-zwicker
Copy link
Author

Thanks – this is certainly a work-around for specific situations, but I would like to provide a general function densityplot, which as arguments simply takes a 2d array of intensity values and two 1d arrays indicating the points along which the data has been sampled (and in most cases the two 1d arrays will be either linear or log-spaced). The densityplot function should then in principle set the axes automatically and allow the user to add colorbars etc afterwards. In essence, I'm envisioning a slightly modified pcolormesh, except that the density plot should in principle be a simple image, which hopefully can be shown with imshow. The workaround suggested above and in the StackOverflow question unfortunately don't provide the necessary flexibility.

@jklymak
Copy link
Member

jklymak commented Dec 21, 2024

Perhaps you could provide an example where pcolormesh(..., rasterized=True) actually returns a worse output than NonUniformImage, and that would help clarify the problem.

@david-zwicker
Copy link
Author

I played around with different options, and I'm pleasantly surprised by the rasterized pcolormesh. This seems to solve my original issue, so I will adopt it in the future. Thanks for the help!

However, the original problem discussed in this issue still stands (even though I will now not be affected by it).

@timhoffm
Copy link
Member

What exactly do you as the "original problem"? Comparing NonuniformImage and pcolormesh directly is difficult. In both cases, you have a discrete 2D array of values. You have to somehow determine the coordinates for the transition from one value to the next. pcolormesh does this explicitly (you've specified the 4 y-values of the borders). In contrast, for NonUniformImage you only give the pixel centers. There's no explicit specification where the borders should be. What we do is nearest neighbor interploation in data coordinates. Therefore, as written in #29368 (comment) I believe the behavior is correct.

@david-zwicker
Copy link
Author

Thank you for the detailed explanation! I agree that the current behavior is valid and nothing needs to be changed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Community support Users in need of help.
Projects
None yet
Development

No branches or pull requests

4 participants