A new key-value interface for Metapost

Posted on September 7, 2019

Recently, Hans announced a new key-value driven interface for MetaFun and posted a MyWay document illustrating its use. In this post, I am going to present an example of how to use this interface.

I teach a course in Linear Systems and occasionally need to draw what is known as a pole zero plot. Here is what it looks like.

Example of a pole zero plot

The circle denotes the location of zero and the crosses denote the location of poles. It is a relatively simple plot to draw, so I thought that this is a good candidate to show how the interface works without worrying too much on the logic of plotting. Let’s start with the bare-bones implementation. I want to draw the above plot using the following interface:

\startMPcode
   draw PZplot
     [
        xmin = -3.5, xmax = 3.5,
        ymin = -3.5, ymax = 3.5,
        poles = { (-1, 1), (-1, -1) },
        zeros = { (-2, 0) },
     ] ;
\stopMPcode

So, how do we create such a key-value driven interface. Let’s go over it step by step. All that follows in Metapost code.

The first step is to define the macro PZplot:

def PZplot = applyparameters "PZplot" "do_PZplot" enddef ;

Here applyparameters is the new MetaFun macro which takes two string arguments. The first ("PZplot" in this case) is the name of the data structure where the key-value pairs are stored. The second ("do_PZplot" in this case) is the name of the MetaPost function that is called after all the key-value pairs have been stored. This is very similar to the old style for defining TeX macros in ConTeXt.

The second (optional) step is to set up the default values of all parameters.

presetparameters "PZplot" [
  xmin = -2.5,  xmax = 2.5,
  ymin = -2.5,  ymax = 2.5,
  dx   = 1,     dy   = 1,
  sx   = 5mm,   sy   = 5mm,
  scale = 0.5,

  grid = false,

  poles = { },
  zeros = { },

  style = "\switchtobodyfont[8pt]",
];

All values must either be a valid MetaPost type (numeric, pair, string, etc.) or a list of value MetaPost types enclosed in braces (like a Lua table).

Finally, we can retrieve a value as follows:

xmin := getparameter "PZplot" "xmin";

The right hand side returns a metapost numeric and one has to be careful to ensure that the variable xmin is declared a numeric beforehand.

For a list (like poles and zeros in the above macros), there are a few other macros:

  1. To get the number of elements in the list, use
getparametercount "PZplot" "poles"
  1. To get the say 2nd element of the list, use
getparameter "PZplot" "poles" 2

That’s it.

There are two other convenience macros. Writing the name of the data structure "PZplot" in all the macros can get tiring. So, one can set the name of the current data structure using

pushparameters "PZplot";

Then,

xmin := getparameter "xmin";

is equivalent to

xmin := getparameter "PZplot" "xmin";

One can then reset the name of the default data structure using

popparameters;

The actual mechanism is more complicated than that because pushparameters can be used in a nested manner, but I’ll ignore that and stick with the simpler explanation that I used above.

So, without further ado, here is the complete implementation of the PZplot macro. As a mater of habit, I define and use this macro in a separate MetaPost instance. That way, I don’t have to worry about my definitions affecting code from other modules.


\defineMPinstance[LTI]  
                 [
                   format=metafun,
                   extensions=yes,
                   initializations=yes,
                   method=double,
                  ]

\startMPdefinitions{LTI}
def pole =
    image(
        draw (1, 1) -- (-1, -1);
        draw (1, -1) -- (-1, 1);
    )
enddef ;

def zero =
    image(
        unfill fullcircle scaled 2;
        draw   fullcircle scaled 2;
    )
enddef;

def PZplot = applyparameters "PZplot" "do_PZplot" enddef ;

presetparameters "PZplot" [
  xmin = -2.5,  xmax = 2.5,
  ymin = -2.5,  ymax = 2.5,
  dx   = 1,     dy   = 1,
  sx   = 5mm,   sy   = 5mm,
  scale = 0.5,

  grid = false,

  poles = { },
  zeros = { },

  style = "\switchtobodyfont[8pt]",

];

vardef do_PZplot =
  image (
    pushparameters "PZplot";

    newnumeric xmin, xmax, ymin, ymax;
    xmin := getparameter "xmin";
    xmax := getparameter "xmax";
    ymin := getparameter "ymin";
    ymax := getparameter "ymax";

    newnumeric sx, sy;
    sx := getparameter "sx";
    sy := getparameter "sy";

    newnumeric dx, dy;
    dx := getparameter "dx";
    dy := getparameter "dy";

    newpath xaxis, yaxis;

    xaxis := (xmin*sx, 0) -- (xmax*sx, 0);
    yaxis := (0, ymin*sy) -- (0, ymax*sy);

    newpath xtick, ytick;
    xtick := (-0.1sx, 0) -- (0.1sx, 0);
    ytick := (0, -0.1sy) -- (0, 0.1sy);

    newstring style;
    style := getparameter "style";

    newboolean grid;
    grid  := getparameter "grid";

    for x = dx step dx until xmax :
        if grid :
            draw yaxis shifted (x*sx, 0) withcolor 0.5white;
        fi
        draw ytick shifted (x*sx, 0);
        label.bot(style & decimal x, (x*sx, 0));
    endfor

    label.lrt(style & "0", origin);

    for x = -dx step -dx until xmin :
        if grid :
            draw yaxis shifted (x*sx, 0) withcolor 0.5white;
        fi
        draw ytick shifted (x*sx, 0);
        label.bot(style & decimal x, (x*sx, 0));
    endfor

    for y = dy step dy until ymax :
        if grid :
            draw xaxis shifted (0, y*sy) withcolor 0.5white;
        fi
        draw xtick shifted (0, y*sy);
        label.lft(style & decimal y, (0, y*sy));
    endfor

    for y = -dy step -dy until ymin :
        if grid :
            draw xaxis shifted (0, y*sy) withcolor 0.5white;
        fi
        draw xtick shifted (0, y*sy);
        label.lft(style & decimal y, (0, y*sy));
    endfor


    drawarrow xaxis;
    drawarrow yaxis;

    label.rt( style & "$σ$",  (xmax*sx, 0));
    label.top(style & "$jω$", (0, ymax*sy));

    newpair p ;

    newnumeric scale;
    scale := getparameter "scale";

    if (getparametercount "zeros") > 0 :
          for i = 1 upto getparametercount "zeros":
            p := getparameter "zeros" i;
            draw (zero xysized(scale*sx, scale*sx))
                 shifted (xpart p*sx, ypart p*sy)
                 withpen pencircle scaled 1bp;
            endfor
    fi

    if (getparametercount "poles") > 0 :
          for i = 1 upto getparametercount "poles":
            p := getparameter "poles" i;
            draw (pole xysized(scale*sx, scale*sx))
                 shifted (xpart p*sx, ypart p*sy)
                 withpen pencircle scaled 1bp;
            endfor
    fi


    popparameters;

  )
enddef;
\stopMPdefinitions

\starttext
\startTEXpage[offset=2mm]
\startMPcode{LTI}
   draw PZplot
     [
        xmin = -3.5, xmax = 3.5,
        ymin = -3.5, ymax = 3.5,
        poles = { (-1, 1), (-1, -1) },
        zeros = { (-2, 0) },
     ] ;
\stopMPcode
\stopTEXpage
\stoptext

Running the code above gives the plot:

Example of a pole zero plot

As an example of the flexibility of the key-value interface, we can get a background grid by using:

\startMPcode{LTI}
   draw PZplot
     [
        . . .
        . . .
        grid = true,
     ] ;
\stopMPcode
Example of a pole zero plot with grid

I am very excited about this new key-value interface for MetaPost. For my course notes, there are many plots that I draw using TikZ because of the convenience of the key-value interface of TikZ. Now, I can draw these with MetaPost, which has the advantage of being 3-5 times faster.


This entry was posted in Metapost and tagged metapost, metafun, separating content and presentation, programming.