Hello everyone,
In this post I would like to share a programming technique I find interesting, as well as ask for feedback on it.
The problem
The MaTeX package typesets LaTeX directly within Mathematica. Unfortunately, every call to the MaTeX
function involves one run of LaTeX, which is very slow: it takes about 0.5 seconds. Typesetting 20 formulae would take 10 secondsmuch too slow.
To mitigrate this, MaTeX can typeset a list of expressions with a single LaTeX run. This is much faster:
In[37]:= ClearMaTeXCache[]
MaTeX@Range[20]; // AbsoluteTiming
Out[38]= {0.480417, Null}
But it is not always convenient to put everything we want to compile into a single list. MaTeX is meant for creating figure labels and each label would usually appear in a different position in the Mathematica code we write. Could we still gather all these inputs, appearing in different places in the code, and compile them with a single run?
The solution
Here's how we can do this: delay the evaluation. Let's have a function called delayedMaTeX
that doesn't evaluate immediately, but simply acts as a placeholder object. It also collects the inputs into a queue for later evaluation. Then we can trigger batch-evaluation with one command, which assigns the results to the placeholders.
We also want to prevent the unlimited accumulation of compiled results, which take up memory. Let's get rid of them as soon as they are no longer referenced anywhere. This can be achieved using the rarely used Temporary
attribute.
For the following proof-of-concept example, let's forget that MaTeX
can take optionstaking that into account would just make the code longer and harder to understand without being educational. Let's load MaTeX and set some options right away, just so the examples below will look a bit nicer.
<< MaTeX`
SetOptions[MaTeX, "DisplayStyle" -> False, FontSize -> 16];
Turning off history prevents unwanted references to symbols. Then we can observe how unreferenced results get cleared.
$HistoryLength = 0;
The inputs will be queued up for evaluation in this variable:
queue = <||>;
Let the placeholder be called delayed
. It holds a unique Temporary
symbol that allows for reference tracking, as well as the TeX input itself. If the input is not yet in the queue, it adds it:
expr : delayed[s_Symbol, texcode_] /; Not@KeyExistsQ[queue, ToString[s]] :=
(AppendTo[queue, ToString[s] -> texcode]; expr)
When the input is processed, the result will be assigned to the temporary symbol. The result from MaTeX always has head Graphics
. If delayed
sees this, it simply evaluates directly to the result:
delayed[g_Graphics, ___] := g
Finally let's add a nice printed form for these delayed
expressions. This is irrelevant for the functioning of the system; it's purely for beautification.
delayed /: MakeBoxes[expr : delayed[_, texcode_], StandardForm | TraditionalForm] :=
With[{boxes = ToBoxes@Tooltip[
Panel[
Style["$\[Ellipsis]$", "Code", ShowStringCharacters -> False,
Background -> None], FrameMargins -> 2
],
texcode]},
InterpretationBox[boxes, expr]
]
The delayedMaTeX
function simply creates the temporary symbol (named as id$...
) and places it within a delayed
expression along with the TeX input:
delayedMaTeX[texcode_] :=
With[{id = Unique[id]},
SetAttributes[id, Temporary];
delayed[id, texcode]
]
Finally we will have an update[]
function that processes the queue:
update[] :=
Module[{},
queue = KeySelect[queue, NameQ]; (* drop keys that don't have a corresponding temporary symbol *)
With[{syms = Symbol /@ Keys[queue]},
syms = MaTeX[Values[queue]]
];
queue = <||>;
]
We are ready to try it out now.
Let's put some MaTeX tick labels in a plot:
plot = Plot[Sin[x], {x, 0, 2 Pi},
Ticks -> {
Table[{x, delayedMaTeX[x]}, {x, 0, 2 Pi, Pi/3}],
Table[{y, delayedMaTeX[y]}, {y, -1, 1, 1/2}]
}
]
Only the placeholders show for now. Let's process the queue:
In[25]:= update[] // AbsoluteTiming
Out[25]= {0.455054, Null}
It was fast. Let's look at the plot again:
plot
Everything is in place now.
At this moment we have lots of temporary symbols holding the results:
In[17]:= Names["id*"]
Out[17]= {"id", "id$", "id$4271", "id$4272", "id$4273", "id$4274", \
"id$4275", "id$4276", "id$4277", "id$4278", "id$4279", "id$4280", \
"id$4281", "id$4282"}
They are gone as soon as plot
is cleared:
In[18]:= plot =.
In[19]:= Names["id*"]
Out[19]= {"id", "id$"}
To wrap up
I hope you found this technique interesting. Any comments, improvements, additions, criticisms are welcome.
If you use MaTeX yourself and you would like to have this feature properly implemented and integrated into MaTeX, let me know. It would eliminate any remaining disadvantages MaTeX has relative to MathPSFrag. But given that MaTeX already uses a caching system, and that I personally don't find performance to be a problem, I'm not sure it's worth the effort.