Adjective Noun

Using matplotlib in Perl 6 (part 7)

2017-04-11 23:32, Tags: perl python matplotlib

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

This will be the last entry in this series on using Matplotlib in Perl 6. As I said in the Intro, I was just playing around, found it interesting, and decided to write about it. It also provided a handy excuse to talk about some of the nice features in Perl 6. I never had any intention of creating a full-featured wrapper for Matplotlib, but my hope is that - through this series - I have shown how entirely possibly it is to use Python modules in a transparent manner in Perl 6 via a wrapper.

Last time I took way too long to get to one graph, so I wanna show a few this time. I've covered enough ground that my Matplotlib wrapper can handle basic graphs nicely. Let me just bask in the warmth of how I can churn out a few simple plots without having to fix anything. This time I'm eyeing a few scatter and polar plots. First up is is scatter_demo.

use Matplotlib;
my $plt = Matplotlib::Plot.new;

constant N = 50;
my $x = rand xx N;
my $y = rand xx N;
my $c = rand xx N;
my $s = ( rand xx N ).map( π × ( 15 × * )² );

$plt.scatter( $x, $y, :$c, :$s, :alpha(0.5) );
$plt.show();

Which produces a random variation on this

Jumping straight into another scatter plot, this one is polar_scatter_demo. As you might glean from the name, this one uses a polar projection, as well as the always pleasant-looking HSV color map.

use Matplotlib;

my $plt = Matplotlib::Plot.new;

constant N = 150;
my $r      = ( rand xx N ).map( * × 2 ).List;
my $theta  = ( rand xx N ).map( * × τ );

my $s = $r.map( 200 × *² );
my $c = $theta;

my $ax = $plt.subplot( 111, :projection<polar> );
$ax.scatter( $theta, $r, :$c, :$s, :cmap<hsv>, :alpha(0.75) );

$plt.show()

Which gives me a random variation on this

Nice stuff here.
Using the great list repetition operator covered in Part 2, simple passing of named arguments, mapping an operation over a list of items using real unicode math symbols... and they're nice looking plots too!

But this wouldn't a complete post if I didn't get a curve ball, which I will encounter in the next example, polar_bar_demo. Here's the Python.

import numpy as np
import matplotlib.pyplot as plt

# Compute pie slices
N = 20
theta = np.linspace(0.0, 2 * np.pi, N, endpoint=False)
radii = 10 * np.random.rand(N)
width = np.pi / 4 * np.random.rand(N)

ax = plt.subplot(111, projection='polar')
bars = ax.bar(theta, radii, width=width, bottom=0.0)

# Use custom colors and opacity
for r, bar in zip(radii, bars):
    bar.set_facecolor(plt.cm.viridis(r / 10.))
    bar.set_alpha(0.5)

plt.show()

There's nothing here that looks like it shouldn't work. Last time I compared this series to a predictable movie franchise, but this time there's a plot twist. It's not a method call that threw me this time. Don't get me wrong, I still need to handle that .cm method, so I'll cover that first. Taking a scene from the Part 6 script, I implemented another anonymous class to handle the call to plt.cm

class Matplotlib::Plot {
    method cm {
        class {
            method FALLBACK($name, $idx) {
                $py.run("matplotlib.pyplot.cm.{$name}($idx)", :eval);
            }
        }.new();
    }
    # stuff ...
}

This run looks a little different due to the use of the curlies around the $name variable. This is because had I tried to eval "matplotlib.pyplot.cm.$name(...)" Perl will interpret the $name() as a Perl function

my $f = *.uc;
say $f('hello');    # Result: 'HELLO'

So I need a way to prevent it from being interpreted as a function call. I initially used a a sprintf style statement, but on freenode #perl6, timotimo suggested using curlies instead. This is one of my favourite syntax features of Perl 6 so I can't believe I didn't think of it, and haven't talked about it. Essentially, in Perl 6, any time you see a pair of curlies, it's a code block... even inside a string!

my ( $x, $y ) = ( 100, 3 );
say "$x ÷ $y is { floor( $x ÷ $y ) }, with a remainder of { $x % $y }";
# OUTPUT: 100 ÷ 3 is 33, with a remainder of 1

You can even nest these contructs without Perl getting confused. I should also mention that anytime you see curlies, you should also think: new lexical scope. Any lexical variables declared inside a code block are not visible outside of that code block.

So now the .cm method is working as expected, but that's not the curve ball. No, the real problem occurs later on with this line

my $bars = $ax.bar( $theta, $@radii, :$width, :bottom(0.0) );

for $bars Z @radii -> ( $bar, $r ) { ... }

Perl has a zip() function, but I prefer the versatility of the Z infix operator, because it can also be paired with any other infix operator for easy map-like operations

> (1, 2, 3) Z+ (4, 5, 6)
(5 7 9)
> ( 'A'..'Z' Z~ 1..26 )[^6]
(A1 B2 C3 D4 E5 F6)

However - as far as Perl is concerned - the $bars variable I get back from $ax.bar() is not iterable ie... It is another Inline::Python::PythonObject. I tried to convert it to something iterable via Inline::Python but nothing was working. In desperation, I checked to see if there were any native Python methods on the object that could use to get at the list of things inside.

I modified the python version to run print(dir(bars)) after I got the bars variable and saw a few of the usual suspects: __str__, __repr__, __len__, and others.
Again, maybe there's a better way I could have got at the elements in this thing, but ultimately what worked for me was __getslice__(0, N). This gives me a list slice from the first (0-th) element, up to the N-th element; I could then zip those with the elements in radii. Here's my working code

use Numpl;
use Matplotlib;

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

# Compute pie slices
my constant N = 20;

my $theta = $np.linspace( 0.0, τ, N, :endpoint(False) );

my @radii = ( rand xx N ).map( * × 10 ).map( *.Num );
my $width = ( rand xx N ).map( π ÷ 4 × * );

my $ax = $plt.subplot( 111, :projection<polar> );
my $bars = $ax.bar( $theta, $@radii, :$width, :bottom(0.0) );

for $bars.__getslice__(0, N) Z @radii -> ( $bar , $r ) {
    my $rgb = $plt.cm.viridis($r ÷ 10);
    $bar.set_facecolor($rgb);
    $bar.set_alpha(0.5);
}

$plt.show()

The only other change I made here was using τ instead of π × 2 because why not. Here's the final plot I will do from the gallery.

But it doesn't feel right to end on an example plot like this. I haven't yet done any plots outside of what exists in the Matplotlib gallery.

In keeping with a Perl 6 kind of flavour, there is curve known as the butterfly curve. The maths behind it is a little beyond me... I tried and failed to get the polar equation in that wikipidia article working, but eventually gave up and got the parametric equation working this way.

use Numpl;
use Matplotlib;

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

my @v = (0, 0.02 ... 72);

sub comp($n) { e ** cos($n) - 2 × cos(4 × $n) - sin($n ÷ 12)⁵ }

my $x = @v.map(-> $t { sin($t) × comp($t) });
my $y = @v.map(-> $t { cos($t) × comp($t) });

$plt.plot( $x, $y );

$plt.show();

Nothing special code-wise here. Just creating a range of values, mapping those values over a given computation, then plotting them.

This looks nice enough, but I couldn't leave it without experimenting with colour. I imagined I could change the colour of the curve based on location, but unless I'm mistaken, it seems Matplotlib doesn't have that functionality. You can, however, mimic that behaviour with a scatter plot and lotsa points... So, I changed a few lines, played with the numbers a bit, and ended up with this

my @v = (0, 0.005 ... 72); # more points!

# stuff...

# $plt.plot($x, $y); # don't need this anymore
my $m = 2;
my $c = ([Z] $x, $y).map(-> ($a, $b) { cos($a × $m) + sin($b × $m) });
$plt.scatter( $x, $y, :$c );

$plt.show();

And now we have this beautifully colourful butterfly curve.

So this is the end of my exploratory series on using Matplotlib in Perl 6. If you're hankering for more, moritz has been covering similar ground over on his blog recently. He's also more knowledgeable than me on all things Perl 6.

I feel confident - and I hope you do too - that given the need to do some plotting, I would reach for Matplotlib without hesitation... even if only for the fact that I'm now more familiar with it than I am with any other plotting library.

If you'd like to play around with my Matplotlib or Numpl modules as they existed after this series, I've put them up in a gist. You're welcome to fork them and do whatever you will.

Thanks for coming on this journey with me.