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...