How I had to translate Matlab code into Maple

Posted on Tue 18 August 2020 in Posts

In this short post, I wanted to point out one interesting application of regular expressions I had to work on for my PhD research project. The code was meant as a technical tool to help tranlate some ordingary differential equation models from numerical (Matlab) to symbolic (Maple) code.

The original code

The original *.m files were pulled from this webpage using wget and their own API. Ordingary differential equation (ODE) models in those files contain a special function called xdot. It returns an array of n elements, which form right-hand side of an ODE.

The function is usually defined in the form

function xdot=f(x,t)
    % define parameters as
    C = 1.0;
    % ...
    xdot = zeros(..., ...);
    % define each xdot(i) separately
end

What I wanted to see in the maple code was the following:

#  define parameters as symbolic constants
C := C:

#  define system of odes as array

sigma := [
    diff(x1(t), t) = <...>,
    <...>
    diff(xn(t), t) = <...>
]:

The best way to solve it I saw was to use regular expressions.

Step by step

Firstly, I had to get rid of Matlab comment and turn them into Maple compatible ones, hence the line

out_program = re.findall(
                r"function xdot=f\(x,t\)(.*?)end", content, re.DOTALL
            )[0].replace("%", "#")

After that, we want to look at if statements. In Maple, conditional structure like if statements are written as

if <condition> then <code> end:

To do that, we utilize groups: (..). The regex is

out_program = re.sub(
                r"if(\(\w*\))(.*;)end",
                r"if \1 then \2\nend if:",
                out_program,
                flags=re.DOTALL,
            )

The if(\(\w*\))(.*;)end regex gets the condition \(\w*\) and the code (.*;) between if / end to place those between if, then, end in positions marked by \1 and \2.

Remeber, that we do not want to have xdot(i)=... assigned manually anymore in Maple, we want to see Maple syntax: diff(x(t),t) = ..., so we do the following regex:

out_program = re.sub(
                r"xdot\((\d+)\) \=( .*);", r"\ndiff(x\1(t), t) = \2,", out_program
            )

Again, notice the groups that capture stuff we want to preserve, namely the index of xdot and the right-hand sides.

Next few lines do some cosmetic work, namely, rename any x(i) appearance into xi(t), then make all variable assingments symbolic constants (i.e. if we have c=0 we want to have c:=c). Finally, if we have Matlab assignments that do not use constants (i.e. c=0; x = c+x;) we want to keep them (i.e. c:=c: x:=c+x:).

out_program = re.sub(r"x\((\d+)\)", r"x\1(t)", out_program) # x(i) -> xi(t)
out_program = re.sub(r"(\w+)\=\d*\..*", r"\1:=\1:", out_program) # c=NUMBER -> c:=c:
out_program = re.sub(r"(\w+)\=(.*);", r"\1:=\2:", out_program) # Left-side = ANYTHING NOT METNIONED ABOVE -> same but with := sign

Finally, putting it all together we can run:

import re
from glob import glob
from tqdm.auto import tqdm

files = glob("files/*/*/*.m")

for i in tqdm(range(len(files))):
    with open(files[i], "r") as f:
        try:
            content = f.read()
        except:
            print(files[i], "could not read")
        try:
            out_program = re.findall(
                r"function xdot=f\(x,t\)(.*?)end", content, re.DOTALL
            )[0].replace("%", "#")
            out_program = re.sub(
                r"if(\(\w*\))(.*;)end",
                r"if \1 then \2\nend if:",
                out_program,
                flags=re.DOTALL,
            )
            out_program = re.sub(
                r"xdot\((\d+)\) \=( .*);", r"\ndiff(x\1(t), t) = \2,", out_program
            )
            out_program = re.sub(r"(xdot=zeros.*)", r"# \1", out_program) # comment out declaration of xdot

            out_program = re.sub(r"x\((\d+)\)", r"x\1(t)", out_program)
            out_program = re.sub(r"(\w+)\=\d*\..*", r"\1:=\1:", out_program)
            out_program = re.sub(r"(\w+)\=(.*);", r"\1:=\2:", out_program)
            # cosmetic work to make maple run and not complain about trailing comma
            out_program = re.sub(
                r"(diff.*)", r"sigma := [\n\1]:", out_program, flags=re.DOTALL
            ).replace(",\n]", "\n]")

            outname = files[i].split(".")[0] + ".mpl"

            with open(outname, "w") as output:
                output.write(out_program)
            import os

            os.system("mkdir -p new_examples")
            os.system(f"cp {outname} new_examples")
        except:
            print(files[i], "Index not Found.")

Some stuff really specific to our project was omitted for brevity, but one can definitely add any other Maple/Matlab interaction here.