Using Matplotlib in Perl 6 (part 3)
This is Part 3 in a series. You can start at the Intro here.
So far this Perl 6 wrapper class for Matplotlib is going well. With the first graph in the gallery under my belt, I moved on to second example, fill_demo.py
. Here's the Python code
import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 1, 500) y = np.sin(4 * np.pi * x) * np.exp(-5 * x) fig, ax = plt.subplots() ax.fill(x, y, zorder=10) ax.grid(True, zorder=5) plt.show()
Being a rather simple graph, you might think that the Perl code would be fairly straight forward, and you'd be right. The only minor curveball is another Numpy function linspace
, and that rather hairy looking operation for the y
value.
To reiterate what I've said in previous parts, I'm not familiar with Numpy at all. Or at least, I wasn't before I started working on these graphs. I inserted a print(x)
into the Python code to see what linspace
did. In hindsight, the name is obvious; It simply creates a linearly spaced sequence of numbers. Here's a quick demonstration
>>> numpy.linspace(0, 1, 3) array([ 0. , 0.5, 1. ]) >>> numpy.linspace(0, 1, 5) array([ 0. , 0.25, 0.5 , 0.75, 1. ]) >>> numpy.linspace(0, 1, 6) array([ 0. , 0.2, 0.4, 0.6, 0.8, 1. ])
In the case of x
in the graph, it's a sequence of 500 numbers evenly spaced from 0 to 1. So how would I do this in Perl. It is a monotonic sequence, so maybe the Sequence operator can help us again. So far we've seen a couple simple Sequences. The most simple sequence defines the start of a number series, and Perl lazily generates the rest.
> 0, 2, 4, 8 ... 1024 (0 2 4 8 16 32 64 128 256 512 1024)
This is nice and all, but I had to define 4 numbers for Perl to understand that sequence. If I dropped the 8
, then it would have generated a sequence of even numbers. Instead, I can define a function which calculates how the next value should be generated.
> 0, 2, * × 2 ... 1024 (0 2 4 8 16 32 64 128 256 512 1024)
Here we see our old friend, the Whatever *
. This time I'm asking for the Sequence starting 0, 2
, then I define a mini-function that takes Whatever the last number in the Sequence was and multiplies it by 2. Sequences are a fascinating part of Perl 6 that could occupy a blog post all their own, but for now, that's enough of a foundation to create a simplified linspace
function.
sub linspace($start, $end, $steps) { my $step = ($end - $start) ÷ ($steps - 1); return $start, * + $step ... $end; } linspace(0, 1, 3); # Result: (0, 0.5, 1) linspace(0, 1, 5); # Result: (0, 0.25, 0.5, 0.75, 1) linspace(0, 1, 6); # Result: (0, 0.2, 0.4, 0.6, 0.8, 1)
This function does what I need, but it could stand to be a little more robust. I'll make a module to house this function, just in case I need it for future plots. I'll also add type constraints to the parameters, and multiple dispatch functions to handle steps of 0 or 1. I'm calling my library Numpl
. Any resemblance to actual libraries is purely coincidental.
class Numpl { proto method linspace(Real $start, Real $end, Int $steps) { * } multi method linspace($start, $end, 0) { Empty } multi method linspace($start, $end, 1) { $start } multi method linspace($start, $end, Int $steps) { my $step = ($end - $start) ÷ ($steps - 1); return $start, * + $step ... $end } }
So that was a very scenic tour around the Sequence operator, but now that we have a linspace
function, the rest is smooth sailing. Getting back to the plot, the final code now looks like this.
use Matplotlib; use Numpl; my $plt = Matplotlib.new; my @x = Numpl.linspace(0, 1, 500); my @y = @x.map(-> $x { sin(4 × π × $x) × exp(-5 × $x) }); my ($fig, $ax) = $plt.subplots(); $ax.fill($@x, $@y, :zorder(10)); $ax.grid(True, :zorder(5)); $plt.show();
Oh yeah, there was that hairy looking operation for the y
values. To refresh your memory, the python looked like this
y = np.sin(4 * np.pi * x) * np.exp(-5 * x)
I can pretty much copy this operation exactly as it appears and put it inside the map. I've put the map operation on it's own line as I think this aids readability.
You might have also noticed I'm using proper @arrays
in this one, and there is a valid reason for this. Sequences are lazy, and typically can only be iterated over once. x
is iterated over in the map and that would prevent any later iterations. Here I've assigned the Sequence to a fully reified Array, which also means the Sequence is evaluated eagerly at assignment (ie. not lazily). As covered in part 2, Python doesn't like Perl arrays as positional arguments, so I coerce them to scalar values when I pass them.
The other way I could have handled this was to coerce my Sequence to a List on assignment like so.
my $x = Numpl.linspace(0, 1, 500).List;
Then I could have assigned it to a scalar variable and still be free to iterate over it as many times as I like. If you have no need for laziness from your linspace
function, you could modify it to return a reified List either coercing the return value as above, or by instructing the Sequence to be eagerly evaluated.
return ($start, * + $step ... $end).List # or return eager $start, * + $step ... $end
But I like to be able to control the laziness of my linspace
result from outside the function.
Whichever way you do it, the end result should look like this
The wrapper module is working out well... a little too well. I wanted to tackle something a little more complex, and scrolling through the Matplotlib gallery I saw it. It's high time for a histogram.
To be continued...