Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Introducing Spectral Colors#29517

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
Sven-J-Steinert wants to merge3 commits intomatplotlib:main
base:main
Choose a base branch
Loading
fromSven-J-Steinert:Introduce-Scientific-Spectral-Colors

Conversation

Sven-J-Steinert
Copy link

PR summary

  • Why: Scientific plots as e.g.spectral line plots, could not directly access colors through wavelengths.

  • Solution: Define colors through wavelength strings e.g. '405nm' that get mapped to RGB values

  • Method: Precomputed RGB values. XYZ values from the2006-TUIL-2° spectral value function converted to sRGB with gamma correction.

This code:

import matplotlib.pyplot as pltplt.figure(figsize=(10,1))for i in range(390,781):    plt.plot([i,i],[0,1],color=f'{i}nm')plt.xlabel('$\lambda$ (nm)')plt.show()

yields:

result

PR checklist

  • no related issue
  • new and changed code is tested
  • Plotting related features are demonstrated in an example, added in color docs
  • New Features andAPI Changes are noted with adirective and release note
  • Documentation complies withgeneral anddocstring guidelines

@github-actionsgithub-actionsbot added topic: color/color & colormaps Documentation: examplesfiles in galleries/examples Documentation: user guidefiles in galleries/users_explain or doc/users Documentation: APIfiles in lib/ and doc/api labelsJan 24, 2025
Copy link

@github-actionsgithub-actionsbot left a comment

Choose a reason for hiding this comment

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

Thank you for opening your first PR into Matplotlib!

If you have not heard from us in a week or so, please leave a new comment below and that should bring it to our attention. Most of our reviewers are volunteers and sometimes things fall through the cracks.

You can also join uson gitter for real-time discussion.

For details on testing, writing docs, and our review process, please seethe developer guide

We strive to be a welcoming and open project. Please follow ourCode of Conduct.

@Sven-J-Steinert
Copy link
Author

I believe the 1 failed check of the AppVeyor build is not caused by my code.

Traceback (most recent call last):  File "C:\Users\appveyor\AppData\Roaming\mamba\envs\mpl-dev\Lib\shutil.py", line 636, in _rmtree_unsafe    os.rmdir(path)PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: 'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\tmp84p9waxs'During handling of the above exception, another exception occurred:Traceback (most recent call last):  File "C:\Users\appveyor\AppData\Roaming\mamba\envs\mpl-dev\Lib\tempfile.py", line 893, in onerror    _os.unlink(path)PermissionError: [WinError 5] Access is denied: 'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\tmp84p9waxs'During handling of the above exception, another exception occurred:

[...]

================================== FAILURES ===================================_________________________ test_fontcache_thread_safe __________________________[gw0] win32 -- Python 3.11.11 C:\Users\appveyor\AppData\Roaming\mamba\envs\mpl-dev\python.exe    def test_fontcache_thread_safe():        pytest.importorskip('threading')    >       subprocess_run_helper(_test_threading, timeout=10)lib\matplotlib\tests\test_font_manager.py:288: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _lib\matplotlib\testing\__init__.py:126: in subprocess_run_helper    proc = subprocess_run_for_testing(lib\matplotlib\testing\__init__.py:94: in subprocess_run_for_testing    proc = subprocess.run(C:\Users\appveyor\AppData\Roaming\mamba\envs\mpl-dev\Lib\subprocess.py:550: in run    stdout, stderr = process.communicate(input, timeout=timeout)C:\Users\appveyor\AppData\Roaming\mamba\envs\mpl-dev\Lib\subprocess.py:1209: in communicate    stdout, stderr = self._communicate(input, endtime, timeout)_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _self = <Popen: returncode: 1 args: ['C:\\Users\\appveyor\\AppData\\Roaming\\mamba\\...>input = None, endtime = 1968.875, orig_timeout = 10    def _communicate(self, input, endtime, orig_timeout):        # Start reader threads feeding into a list hanging off of this        # object, unless they've already been started.        if self.stdout and not hasattr(self, "_stdout_buff"):            self._stdout_buff = []            self.stdout_thread = \                    threading.Thread(target=self._readerthread,                                     args=(self.stdout, self._stdout_buff))            self.stdout_thread.daemon = True            self.stdout_thread.start()        if self.stderr and not hasattr(self, "_stderr_buff"):            self._stderr_buff = []            self.stderr_thread = \                    threading.Thread(target=self._readerthread,                                     args=(self.stderr, self._stderr_buff))            self.stderr_thread.daemon = True            self.stderr_thread.start()            if self.stdin:            self._stdin_write(input)            # Wait for the reader threads, or time out.  If we time out, the        # threads remain reading and the fds left open in case the user        # calls communicate again.        if self.stdout is not None:            self.stdout_thread.join(self._remaining_time(endtime))            if self.stdout_thread.is_alive():>               raise TimeoutExpired(self.args, orig_timeout)E               subprocess.TimeoutExpired: Command '['C:\\Users\\appveyor\\AppData\\Roaming\\mamba\\envs\\mpl-dev\\python.exe', '-c', "import importlib.util;_spec = importlib.util.spec_from_file_location('matplotlib.tests.test_font_manager', 'C:\\\\projects\\\\matplotlib\\\\lib\\\\matplotlib\\\\tests\\\\test_font_manager.py');_module = importlib.util.module_from_spec(_spec);_spec.loader.exec_module(_module);_module._test_threading()"]' timed out after 10 seconds

@timhoffm
Copy link
Member

Yes the CI error is a known issue that happens irregularly.

I’m 50/50 on the addition.

Con:

  • This is quite specific, I expect the number of users interested in this is quite small.
  • Quite a few entries resolve to the same number.

Unclear:
Are color names the right approach? Alternatives would be a function or a colormap. This depends a lot on the anticipated use cases. Can you please give some examples where you would like to use these colors?

Pro:
We are science-oriented so possibly can afford to be a bit nerdy and support color specification via wavelength. While these are many values, the naming is conceptually clear and should not lead to confusion. There should also be no performance impact and basically no maintenance overhead.

@Sven-J-Steinert
Copy link
Author

  • specific: yes, i agree, but since its light weight i think it doesn't harm. I think this would help everyone dealing with optics, absorption, emission lines, all spectroscopy applications, laser etc.

Colormap: i think its not supposed to be a colormap, as the full spectrum would not be the usual use case. One would rather only plot specific emission lines (one by one), where i personally would prefer them as single colors.

e.g. Iron 26
Fe_bar
Fe_Spike

or Xenon 54
Xe_bar
Xe_spike

Perhaps additionally a colormap would be nice too, yet i think to have them singular has its application.

Unclear:
A function was my first approach, however that fit introduces artifacts especially towards the high wavelengths, even with avery high order of 30.

rgb

fit
At this point i thought its almost taking as much space as i define them in a static dict, that way nothing needs to be executed.
One alternative would've been Scipy CubicSplines, they would fit great, but i didn't want to add a requirement for this.

Same Values:
Yes that also led me to quite some thinking - that actually happens during conversion into sRGB range as values are clipped.

it starts with this X,Y,Z values fromTable A.21

xyz

That is transformed via the transformation matrixM^-1 to linear RGB

linrgb

which is thengamma corrected for sRGB
grgb
and then clipped to 0 and 1. (This is the current implementation)

rgb

Alternatives

if i would leave away the gamma correction, it doesn't look better (violet is missing)
rgb_nogamma

If i clip 0 but normalize each curve (yellow is missing):
rgb_norm

If i clip 0 and normalize to a common maximum (yellow still missing):
rgb_norm_com

which made me conclude that the implementation i supplied is the most accurate, despite featuring many same values in e.g. the green spectrum, but perhaps that's related to our eyes and sRGB.

@timhoffm
Copy link
Member

A function was my first approach, however that fit introduces artifacts especially towards the high wavelengths, even with avery high order of 30.

I meant a python function such asmatplotlib.colors.color_from_wavelength(632.8). The advantage is that you can directly turn any int or float to a color and not have to turn it into a str first. You also can issue more meaningful error messages if out of range (“wavelength 800 cannot be convertered. The supported range is 390…780” vs. “800nm is not a valid color”).
The function could also get optional parameters like with/without gamma correction - if that’s a reasonable feature.

The downside is that for a single hard-coded usagecolor=color_from_wavelength(632) is more cumbersome thancolor=“632nm”.

The function can still use a lookup table internally.

@Sven-J-Steinert
Copy link
Author

Ah i understand. I see the benefit of the parsing and error handling, however personally I prefer the simplicity ofcolor="405nm".
So for now I would "vote" for that way.

I think to leave out gamma correction (even as an optional feature) doesn't make sense, I just wanted to show you with that how the identical values came about and that i believe it is still the most accurate result.

@jklymak
Copy link
Member

First, this is almost certainly a colormap and not individual colors. Wavelengths are not quantized integers. I don't think there is much justification for this to be named colors.

Second we already have two spectral colormaps. Is this substantively different than those?

A lookup function isn't a bad idea but I'm not sure it's a Matplotlib feature

@timhoffm
Copy link
Member

I think it really depends on the use case. Since there's a direct mapping from value to color, such a colormap would primarily make sense when used with a norm ofvmin=390, vmax=780. While wavelengths are not integers, every operation in the end is quantized. The question here would be whether (i) the degree of quantization is reasonable (more / less values?), which depends on the intended application, and (2) whether we are willing to accept such a large number of new names, which is a design decision. Fundamentally, there is nothing wrong with naming a certain color "632nm".

@jklymak
Copy link
Member

jklymak commentedJan 25, 2025
edited
Loading

Fundamentally, there is nothing wrong with naming a certain color "632nm".

Agreed— just as there's nothing wrong with calling a color "green," there's nothing inherently wrong with using "632nm." However, very few people intuitively know what color 632nm represents, making it unhelpful for users selecting colors. The real use case is programmatically specifying the color asf'{lambda:03d}nm'. In my view, this could just as easily be handled withmycmap(mynorm(lambda)) ormyfunction(lambda). Since all other named colors have qualitative meanings, I’m not in favor of introducing quantitative lookups.

@Sven-J-Steinert
Copy link
Author

First, this is almost certainly a colormap and not individual colors. Wavelengths are not quantized integers. I don't think there is much justification for this to be named colors.

Of course nature is a continuum, but that doesn't change anything about it being useful to be able to address a wavelengths color specifically. I don't know how you imagine that, but i don't see how i can pull a wavelength color from a color map. The use here is coming from the mapping between nm and color.

Second we already have two spectral colormaps. Is this substantively different than those?

i assume you are referring to 'Spectral' and 'nipy_spectral'.

image
image
image

I think its obvious to see that 'Spectral' is just a color fade, pretty far away from physics.
'nipy_spectral' is much closer, and probably inspired by the physical spectrum, however not physically accurate (its a 21 pointdefinitionwith flat numbers). So i think, yes, for me that's a substantial difference, of being physically correct or not.

However, very few people intuitively know what color 632nm represents,

This is meant for scientific work, where you sometimes explicitly use wavelength definitions as I have mentioned before (e.g. in every optics related field). Sure this wont be any useful for the ordinary user, this is a scientific contribution for scientific use-cases.

@jklymak
Copy link
Member

My point still stands that this is going to be accessed programatically, in which case they are better expressed as a colormap or a function rather than hundreds of colors named with integers. I wouldn't object to a colormap that follows a well-documented standard wavelength->RGBA mapping.

@Sven-J-Steinert
Copy link
Author

I think that i have in fact implemented a well documented wavelength to RGB mapping - so this debate is now specifically if it should be a colormap with a function that can parse a float "nm" value or a singular definition through dict strings?

I can see advantages in both, yes a colormap function can have more features, but it is also way less intuitive to use in my opinion.
as you said it would becolor='405nm' vs.color=plt.cm.Wavelengths.parse(405).

Even if its programmatically more clean, this is taking away the ease of use in my opinion.
For example, imagine you're just going to plot some Laser performance data from different Lasers in one plot that you want to differentiate by color like this

image
and you think "would be cool to use their actual colors"

I would never get the idea to search for a colormap and its associated methods to find out i can pull a specific color for that wavelength. Instead, I would check the color definitions, where i could find the string definition '405nm' and done.

@rcomer
Copy link
Member

If it was a colormap, I believe the emission lines examples from#29517 (comment) could be done more efficiently withvlines. Sincevlines uses aCollection, you could pass the wavelengths asarray.

jklymak reacted with thumbs up emoji

@jklymak
Copy link
Member

I think that i have in fact implemented a well documented wavelength to RGB mapping - so this debate is now specifically if it should be a colormap with a function that can parse a float "nm" value or a singular definition through dict strings?

I think thats correct.

I would never get the idea to search for a colormap and its associated methods to find out i can pull a specific color for that wavelength. Instead, I would check the color definitions, where i could find the string definition '405nm' and done.

If the colormap already existed, I think you would search and find:https://matplotlib.org/stable/gallery/color/individual_colors_from_cmap.html. Of course if this colormap goes in, it may benefit from an additional example that specifies what the minimum and maximum wavelengths are so the correct norm can be used. Are there other visualization packages that refer to colors via an index into the visual wavelengths?

believe the emission lines examples from#29517 (comment) could be done more efficiently withvlines. Since vlines uses a Collection, you could pass the wavelengths asarray.

@rcomer that is a cool idea, though I don't see thatvlines accepts a colormap and norm so maybe that is a feature request?

Overall I can think of lots of cases where mapping to the visual wavelengths might make sense, and only pretty specialized cases where a named line color makes sense (eg you know a particular wavelength you want to color). My opinion is that this would be less controversial as a colormap.

@timhoffm
Copy link
Member

timhoffm commentedJan 26, 2025
edited
Loading

You can do

norm = Normalize(390, 780)cmap = ...vlines(..., colors=cmap(norm(wavelengths)))

I don't see that vlines accepts a colormap and norm so maybe that is a feature request?

Well technically it has because it produces a collection. There's only no parameter to feed a quantity to be mapped in. This should work (untested because no pc around)

linecoll = vlines(..., norm=Normalize(390, 780), cmap=...)linecoll.set_array(wavelengths)
jklymak and rcomer reacted with thumbs up emoji

@rcomer
Copy link
Member

rcomer commentedJan 26, 2025
edited by timhoffm
Loading

Here is a mockup with v3.9.2

importmatplotlib.pyplotaspltfrommatplotlib.colorsimportNormalizeimportnumpyasnprng=np.random.default_rng(42)wavelengths=rng.uniform(200,900,60)strengths=rng.random(60)fig, (ax1,ax2)=plt.subplots(nrows=2)cmap=plt.get_cmap('Spectral').with_extremes(under='black',over='black')norm=Normalize(400,800)ax1.vlines(wavelengths,0,1,cmap=cmap,norm=norm,array=wavelengths,alpha=strengths)ax2.vlines(wavelengths,0,strengths,cmap=cmap,norm=norm,array=wavelengths)plt.show()

image

timhoffm and scottshambaugh reacted with thumbs up emoji

@scottshambaugh
Copy link
Contributor

scottshambaugh commentedJan 28, 2025
edited
Loading

My opinions here:

  • I'm -1 on named colors for each of the visible nanometer wavelengths
  • I'm -0.5 on acolor_from_wavelength or similar function, it seems like discoverability is low and doesn't fit our current usage patterns
  • I'm +1 on a physically accurate spectral colormap, and an example of how to use it like@rcomer's mockup. It would be really nice with an actual emission spectra

To tag on to the example, getting and using the RGBa value of a wavelength once the colormap is defined is pretty simple:

importmatplotlib.pyplotaspltfrommatplotlib.colorsimportNormalizeimportnumpyasnpcmap=plt.get_cmap('nipy_spectral').with_extremes(under='black',over='black')norm=Normalize(400,800)# Getting the RGBa value for a wavelengthc750nm=cmap(norm(750))print(c750nm)# (0.9242019607843137, 0.0, 0.0, 1.0)# Using that color in a plotx=np.arange(0,1,0.01)plt.plot(x,np.sin(x),c=c750nm)plt.show()# red line
jklymak, rcomer, and timhoffm reacted with thumbs up emoji

@timhoffm
Copy link
Member

Name suggestion for the colormap: "spectrum390-780". IMHO including the limits in the name is essential.

scottshambaugh reacted with thumbs up emoji

@rcomer
Copy link
Member

The mappings seem to come from someone’s PhD dissertation. Do we need to get permission from someone to use them?

@Sven-J-Steinert
Copy link
Author

The mappings seem to come from someone’s PhD dissertation. Do we need to get permission from someone to use them?

No, to use published results does not need permission of the author, it does need a citation. In this case the result is a spectral function standard named after her institution 2006-TUIL-2° which is meant to be an improved version of the commonCIE color space - the whole point of that PhD was to create a mapping that is supposed to be used.

I am taking away from this discussion that an implementation as colormap is preferred, which i also come to agree with now. I will implement it as such, however I am not sure when i have time to do it.

rcomer reacted with thumbs up emoji

@jklymak
Copy link
Member

It would also be preferable if this mapping were recognized beyond a PhD thesis. Is this an actual standard in wide use?

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@github-actionsgithub-actions[bot]github-actions[bot] left review comments

At least 1 approving review is required to merge this pull request.

Assignees
No one assigned
Labels
Documentation: APIfiles in lib/ and doc/apiDocumentation: examplesfiles in galleries/examplesDocumentation: user guidefiles in galleries/users_explain or doc/userstopic: color/color & colormaps
Projects
Status: Waiting for author
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

5 participants
@Sven-J-Steinert@timhoffm@jklymak@rcomer@scottshambaugh

[8]ページ先頭

©2009-2025 Movatter.jp