1

I am working on a fuzzy control example, which uses the skfuzzy library. Its visualization module is built on top of Matplotlib.

Usually, after constructing a fuzzy variable, with the necessary membership function (either using an auto constructor, giving it the number of linguistic variables to be used - 3, 5 or 7,or supplying a custom list of linguistic variables), the .view() method can be called, which returns a plot of the membership function. That plot uses whatever is the default cmap.

This is usually fine, but in my case I'm building an example with temperature control, and the it would have been really nice, if for the membership functions' plot I could use a colour gradient from blue (cold) to red (warm) to represent the various temperature (qualitative) variables. Thus, I want to change the cmap to 'bwr'.

So I need to somehow address the figure or axes of the plot and give it a new cmap. After some digging in the skfuzzy library I found the FuzzyVariableVisualizer class which contains the .view() method and so instead of directly using the .view() on the skfuzzy.control.antecedent_consequent.Antecedent object I have created (which uses a .view().show() and thus does not give access to the underlying figure and axis), I first passed the .Antecedent to the FuzzyVariableVisualizer() and then used its .view() method, which does return the figure and the axes.

But now, I have no idea how to set new cmap for it. Unfortunately google searches yielded only one similar result (this), but it wasn't useful to me. And Matplotlib is a bit too complex for me to dig around (and it will just take too much time).

Here is some code to reproduce the state I'm at. Does anyone have an elegant way to address this?

import numpy as np
import matplotlib.pyplot as plt
import skfuzzy as fuzz
from skfuzzy import control as ctrl
from skfuzzy.control import visualization

room_temp = ctrl.Antecedent(np.arange(0, 11, 1), 'room temperature')
clothing = ctrl.Antecedent(np.arange(0, 11, 1), 'amount of clothing')
temp_control = ctrl.Consequent(np.arange(0, 11, 1), 'temperature control')

temp_qualitative_values = ['absolute zero',
                           'freezing',
                           'extremely cold',
                           'cold',
                           'chilly',
                           'OK',
                           'warm',
                           'hot',
                           'very hot',
                           'hot as hell',
                           'melting']

clothing.automf(3)
clothing.view()

temp_control.automf(5)
temp_control.view()

# I want to chage the cmap of the following figure
room_temp.automf(10, names=temp_qualitative_values)
room_temp_viz = visualization.FuzzyVariableVisualizer(room_temp)
fig, ax = room_temp_viz.view()

plt.show()

I've tried (and did not work):

plt.set_cmap('bwr')
plt.show()
  • The visualiazation module doesn't seem to use a colormap. Instead, it uses matplotlib's color cycle. You need to change the cycler beforehand. E.g. `from cycler import cycler; plt.rc('axes', prop_cycle=cycler(color=plt.get_cmap('RdYlBu', 11).colors))`. See [the tutorial example](https://matplotlib.org/stable/tutorials/intermediate/color_cycle.html) – JohanC Feb 12 '23 at 16:30
  • Thanks for the comment JohanC. I just tried your suggestion, but it throws an error: `AttributeError: 'LinearSegmentedColormap' object has no attribute 'colors'`. I've gone and read about the difference between Listed and LinearSegmented colormaps in Matplotlib, but the colormap reference does not show which is which. I've got the same error for all the different maps I've tried. Hm... should I try to define my own custom listed colormap... – Alexander Grantcharov Feb 13 '23 at 08:27
  • Even better, I just defined 11 RGBA colours from blue to red and passed that to the cycler: `colours = [(1.0, 0.0, 0.0, 1.0), (1.0, 0.1, 0.0, 1.0), (1.0, 0.2, 0.0, 1.0), (1.0, 0.3, 0.0, 1.0), (1.0, 0.4, 0.0, 1.0), (1.0, 0.5, 0.0, 1.0), (0.0, 0.0, 1.0, 1.0), (0.0, 0.1, 1.0, 1.0), (0.0, 0.2, 1.0, 1.0), (0.0, 0.3, 1.0, 1.0), (0.0, 0.4, 1.0, 1.0),] plt.rc('axes', prop_cycle=cycler(color=colours))` – Alexander Grantcharov Feb 13 '23 at 08:37
  • I don't think so. The version is 3.6.0 – Alexander Grantcharov Feb 13 '23 at 10:33
  • 1
    Maybe I installed some dev version. Something that should work with less problems: `from cycler import cycler; plt.rc('axes', prop_cycle=cycler(color=plt.get_cmap('RdYlBu')(np.linspace(0, 1, 11))))`. – JohanC Feb 13 '23 at 10:52
  • Out of curiosity, if the visualizer did not use the cycler, but actually had a cmap defined in the .plot(), and considering the figure and the axis are exposed through the .view() method of the FuzzyVariableVisualizer class, how would you have approached changing it here? Is it possible? – Alexander Grantcharov Feb 13 '23 at 14:05
  • I think you only can change colormaps of things like `scatter` plots, `imshow`, `pcolor`, ..., but not of standard line plots. For line plots, you'd need to change individual colors one by one. By the way, feel free to mark your own answer as accepted. You can still change the mark in the unlikely event somebody else would propose a better answer. – JohanC Feb 13 '23 at 14:33

1 Answers1

1

Here is how I finally approached a solution (Thanks JohanC for pointing me in the right direction):

As JohanC noted in his comment, the visualization module of skfuzzy doesn't use a colour map when calling the plot function for the individual membership functions. Rather, it uses matplotlib's colour cycler. So, by calling the cycler and giving it a custom list to use turns out a simple enough solution.

First I had to import a cycler: from cycler import cycler. Then using JohanC's suggestion, one can use linspace() to sample the desired colormap (in this case 'RdYlBu') for as many colours as necessary (in this case 11), and pass them to the cycler: plt.rc('axes', prop_cycle=cycler(color=plt.get_cmap('RdYlBu')(np.linspace(0, 1, 11))))

Edit: In previous iterations I tried using the .color method on the colormap (as JohanC suggested in his first comment), but it returned an error, since 'RdYlBu' is a LinerSegmentedColormap (and so are 'bwr' and 'coolwarm'), and in matplotlib those don't have the .color method (as they dynamically generate samples). But of course, linspace solves that. One might be able to use ListedColormap (which do have the .color method), but I couldn't find anything in the matplotlib colormap reference documentation, that tells which colormap is of which class.

Or, instead of sampling colours from a colormap, you can also define your own list of colours, as I did in the final version. Just supply the necessary amount of colours as a list (with the RGBA notation matplotlib uses). For my example, I wrote a function to generate a gradient (as many samples from it as needed) given a starting and an ending colour.

Here is the final code:

import numpy as np
import matplotlib.pyplot as plt
import skfuzzy as fuzz
from cycler import cycler
from skfuzzy import control as ctrl
from skfuzzy.control import visualization

def gradient_colour_list(start, end, steps):
    '''
    The function generates 'steps' amount of solid colours,
    sampled from a gradient between the 'start' and 'end' colours.
    'start' and 'end' are defined in RGBA tuples.
    '''
    r_step = (end[0] - start[0]) / (steps - 1)
    g_step = (end[1] - start[1]) / (steps - 1)
    b_step = (end[2] - start[2]) / (steps - 1)
    a_step = (end[3] - start[3]) / (steps - 1)

    gradient = []
    for i in range(steps):
        r = start[0] + (r_step * i)
        g = start[1] + (g_step * i)
        b = start[2] + (b_step * i)
        a = start[3] + (a_step * i)
        gradient.append((r, g, b, a))

    return gradient


room_temp = ctrl.Antecedent(np.arange(0, 11, 1), 'room temperature')
clothing = ctrl.Antecedent(np.arange(0, 11, 1), 'amount of clothing')
temp_control = ctrl.Consequent(np.arange(0, 11, 1), 'temperature control')

temp_qualitative_values = ['absolute zero',
                           'freezing',
                           'extremely cold',
                           'cold',
                           'chilly',
                           'OK',
                           'warm',
                           'hot',
                           'very hot',
                           'hot as hell',
                           'melting']

# Create a list of colours, for as many steps
# as the amount of membership functions, sampled
# from a gradient between blue and red, at 100% opacity.
colours = gradient_colour_list((0.0, 0.0, 1.0, 1.0),
                               (1.0, 0.4, 0.0, 1.0),
                               len(temp_qualitative_values))

clothing.automf(3)
clothing.view()

temp_control.automf(5)
temp_control.view()

# Tell the cycler to use the custom list of colours
plt.rc('axes', prop_cycle=cycler(color=colours))

# Or use a predefined matplotlib cmap (in which case
# the list of custom colours and the function are redundant):
# plt.rc('axes', prop_cycle=cycler(color=plt.get_cmap('RdYlBu')(np.linspace(0, 1, 11))))

# No more need to pass through the FuzzyVariableVisualizer separately 
room_temp.view()

plt.show()