Adjective Noun

Using matplotlib in Perl 6 (part 6)

2017-03-23 12:35, Tags: raku perl python matplotlib

This is Part 6 in a series. You can start at the Intro here.

By this stage this series is beginning to resemble a bad movie franchise. We're back again for another round, featuring the same central characters and themes. The story is essentially the same each time, just the obstacles to overcome change a little. Which is to say, I'm about ready to wrap this series up, but I ran into a few more interesting things so may as well talk about them.

The next plot I tried was another style sheet example, this time it's plot_dark_background. Here's the Python

import numpy as np
import matplotlib.pyplot as plt

plt.style.use('dark_background')

fig, ax = plt.subplots()

L = 6
x = np.linspace(0, L)
ncolors = len(plt.rcParams['axes.prop_cycle'])
shift = np.linspace(0, L, ncolors, endpoint=False)
for s in shift:
    ax.plot(x, np.sin(x + s), 'o-')
ax.set_xlabel('x-axis')
ax.set_ylabel('y-axis')
ax.set_title("'dark_background' style sheet")

plt.show()

Again, most of this stuff is (or looks) straight-forward, but there's a couple new curve balls. First up I want to tackle the easier problem, which is that linspace is being used here in ways my function doesn't handle. At one point, linspace is called with only 2 arguments, and the other time it is called with an endpoint keyword.

Before I even look at that, I want to quickly fix another issue I ran into when playing with my linspace function. I found that when using an irrational number (such as π) as the $end number, something unexpected happened

> Numpl.linspace(0, 3.14159265358979, 7)[5..7]
(2.6179938779914917 3.14159265358979 Nil)
> Numpl.linspace(0, pi, 7)[5..7]
(2.61799387799149 3.14159265358979 3.66519142918809)

I've asked for a Sequence from 0 to π, divided into 7 linear steps. When given a number literal, it works as expected, where the 7th item (index 6) is "pi" and the 8th item is non-existent, ie. Nil. However, when using the built-in π constant, it doesn't consider the 7th item exactly equal to π and the sequence keeps generating infinitely. To fix this, rather than check for equality I will check for "approximate equality" using the =~= operator... but what does "approximate" mean, anyway?

Perl has a global TOLERANCE variable, which by default is 1e-15, or 0.000000000000001. This global can be modified lexically if I want to compare values with a lower tolerance, but the default value works fine for me.

> pi
3.14159265358979
> pi == 3.14159265358979
False
> pi =~= 3.14159265358979
True

So now that's sorted, I moved on to updating the function to act more like the numpy version. I tested in Python and found that when called with 2 arguments, numpy.linspace will default to 50 steps. I modified the function prototype so that $steps is an optional positional just by appending a ? to the parameter. In the multi-sub, I declared $steps to have a default value of 50, and added a named $endpoint parameter... I just need to do something if $endpoint is False. Maybe there's a fancier way to do this, but I just did a simple if-condition. The end result is this

proto method linspace(Numeric $start, Numeric $end, Int $steps?) { * }
multi method linspace($start, $end, 0 ) { Empty  }
multi method linspace($start, $end, 1 ) { $start }
multi method linspace($start, $end, $steps = 50, :$endpoint = True) {
    if $endpoint {
        my $step = ( $end - $start ) ÷ ( $steps - 1 );
        return $start, * + $step ... * =~= $end
    }
    else {
        my $step = ( $end - $start ) ÷ $steps;
        return $start, * + $step ...^ * =~= $end
    }
}

I've spent a lot of time just talking about linspace and I've still got a lot of ground to cover, so I'll just move right into this:

ncolors = plt.rcParams['axes.prop_cycle']

I'm familiar enough with Python to recognise dictionary syntax when I see it.

A dictionary is what Python calls it's associative array type, which in Perl is called a hash. I choose to use all three terms interchangeably.

It seems that rcParams is a class attribute that returns an associative array which can then be subscripted by key. This is actually a unique problem which can't be fully solved using all my old "tricks", and there are a few reasons for that. Sure, I could use Inline::Python::run to return the dictionary... I found that by wrapping the call to matplotlib.pyplot.rcParams with dict in the Python, Inline::Python dutifully returned a Perl hash.

class Matplotlib::Plot {
    # stuff
    method rcParams {
        $py.run("dict(matplotlib.pyplot.rcParams)", :eval)
    }
}

Then I could call that method from my main program, subscript a random key and all seemed ok. (I'm going to use another key axes.xmargin for demonstrative purposes)

say $plt.rcParams<axes.xmargin>    # Result: 0.05 

But! I imagine some other plots might involve changing some of those "params", so how to overcome that? I need my wrapper to be aware if I'm trying to assign a value to that key and run the appropriate Python code. Perl 6 does indeed provide various special methods that allow you to implement an object that behaves like a hash or a list, for example

$obj.foo('bar'); # Calls 'foo' method with argument 'bar'
$obj[2];         # Calls AT-POS method with argument 2 
$obj{'baz'};     # Calls AT-KEY method with argument 'baz'

So all I need to do is implement the relevant methods in my class, which in this case is AT-KEY and ASSIGN-KEY. You can probably guess the latter is called when calling the object like a hash and assigning something to it. Rather than define a "fully-grown" class just to get this functionality, I implemented an anonymous class.

method rcParams {
    class {
        method AT-KEY($key) {
            $py.run("matplotlib.pyplot.rcParams['$key']", :eval);
        }
        method ASSIGN-KEY($key, $value) {
            $py.run("matplotlib.pyplot.rcParams['$key'] = $value");
        }
    }.new();
}

This is just like my style method on Matplotlib::Plot that instantiates a Matplotlib::Plot::Style class, except I've defined the class inside the method anonymously.
A class by any other name would smell as sweet. Now I can do this

$plt.rcParams<axes.xmargin> = 0.08;
say $plt.rcParams<axes.xmargin>    # Result: 0.08

The code inside $py.run() is essentially a string eval, which means, if I pass it a string it will fail because Python will evaluate the $value variable as a bare word. I need to wrap it in quotes, but only if it's a string. This is easily worked around with multiple dispatch. You also might have also noticed I'm not using the :eval option inside ASSIGN-KEY. When I tried to use :eval I got errors; I don't think Python assignments return anything. Typically in Perl, they return the assignment, so I'm going to manually return the provided value to make it more Perlish.

method rcParams {
    class {
        method AT-KEY($key) { ... } # Same as above
        multi method ASSIGN-KEY($key, Str $value) {
            $py.run("matplotlib.pyplot.rcParams['$key'] = '$value'");
            return $value;
        }
        multi method ASSIGN-KEY($key, $value) {
            $py.run("matplotlib.pyplot.rcParams['$key'] = $value");
            return $value;
        }
    }.new();
}

I'd be done here if it weren't for one minor issue. The actual key want from the dictionary - axes.prop_cycle - is itself a dictionary. It seems Inline::Python does not convert this to a Perl hash, and what I get back is an Inline::Python::PythonObject. I hacked away a bit and settled on the first thing that worked.

method rcParams {
    class {
        multi method AT-KEY($key) {
            $py.run("matplotlib.pyplot.rcParams['$key']", :eval);
        }
        multi method AT-KEY('axes.prop_cycle') {
            $py.run(
                "list(matplotlib.pyplot.rcParams['$key'])", :eval
            ).map(|*.values);
        }
        # stuff...
    }
}

Essentially, when I want that particular key, I run exactly the same Python code except that I wrap it in list(). Oddly, wrapping it in dict() didn't help. What I got back was a list of key/value pairs, except, they were all the same key...

[{color => #1f77b4} {color => #ff7f0e} {color => #2ca02c} {color => #d62728}
 {color => #9467bd} {color => #8c564b} {color => #e377c2} {color => #7f7f7f}
 {color => #bcbd22} {color => #17becf}]

So I just run the list through a Perl map and just extract the values. The | there is the short hand syntax for a Slip, a type that "slips" an item into the surrounding list, kind of like the * in Python 3. This is so I get a single list, rather than a list-of-lists.

Boy would you look at the time! I try to keep these posts relatively short but there was so much to talk about. We can finally get to the crescendo of this entry in the franchise... Here's the Perl

use Matplotlib;
use Numpl;

my $plt = Matplotlib::Plot.new;
my $np  = Numpl.new;

$plt.style.use('dark_background');

my ( $fig, $ax ) = $plt.subplots();

constant L = 6;
my @x = $np.linspace(0, L);

my $ncolors = $plt.rcParams<axes.prop_cycle>;

my @shift = $np.linspace(0, L, +$ncolors, :endpoint(False) );

for @shift -> $s {
    my $wave = @x.map(-> $x { sin($x + $s) });
    $ax.plot($@x, $wave, 'o-' );
}

$ax.set_xlabel('x-axis');
$ax.set_ylabel('y-axis');
$ax.set_title("'dark_background' style sheet");

$plt.show();

The only thing here that I don't think I've covered before is that +$ncolors in my second call to linspace. Here, $ncolors is a list, but I can't just pass it to linspace, which expects an Int as it's 3rd positional argument... so I manually coerce it to an Int by prefixing it with +.

Hey, wait a minute! I didn't even need the values in rcParams<axes.prop_cycle> at all! I just need it's number of elements. I could just as easily replace +$ncolors with a literal 10 and it would do the same thing. Still, I don't consider all that work a waste. It may well come in handy for me later... or maybe you. Anyways, here's the resulting graph

Well that's nice, isn't it... Even if it took a while to get here. As stated at the top of this post, I am almost ready to wrap up the series, but want to cover a few more things.

To be continued...