SaneTeX
Using SageTex
in larger articles in a sane way.
Introduction
I've recently been using SageTex in my research writing. There's been ups and downs, benefits but also multiple drawbacks to my workflows until I finally settled on how to do things in a sane way to overcome the limitations in the naive approaches you may have after getting to work immediately after looking at the official tutorials. After lessons learnt, I've come up with some top tips for using sagetex in larger projects. I'll be starting with short, easily-actionable tips that aren't present in the official tutorials, and then finishing with a couple larger pieces advice around overall project structure.
To set the expectations, let me clarify my personal reasons for including SageMath code into my document building:
1) Programmatically generate LaTeX expressions to avoid errors
(TODO add tiny example)
At the point when I decided to start using SageTex, I was frequently making mistakes in the expressions I was deriving. Particularly when specializing expressions to different contexts, where there are different coefficients involved which were easily mixed up. Sometimes the expressions were just long. Having a CAS do these calculations added an extra degree of certainty to my work. Furthermore, including the code inside the project (as opposed to doing it separately) just keeps the calculation steps on record easily checked in case I worry I had done something incorrectly.
2) Include plot creation into the document build
(TODO add tiny example)
Instead of generating plots programmatically inside the LaTeX document, most people use separate tools to create diagrams. Idealogically, this impacts reproducibility of the research. But via SageTex, it's also easier to use the same expressions to generate LaTeX formulae, as creating a plot.
TIP 1: SageMath plot sizing
Although, not present in the official tutorials, the
official example
shows some invaluable options for the \sageplot
function to adjust sizing
among other things.
The most common usages I have are:
\sageplot[width=\textwidth]{...}
\sageplot[width=\linewidth]{...}
If you have trouble sizing plots correctly, especially in subfigures, this might be your solution.
Another takeaway here is to have a look at the official example to come across more advanced usage of SageTex than the minor snippets in the tutorial page. The rest of the tips here however, is not in the official material.
TIP 2: Cut down on manual steps in document build
Problem: annoying compilation steps
The default way of compiling a document main.tex
using sagetex
is to:
- Attempt the compilation once (with say
pdflatex
) - Watch it fail because it cannot find the
main.sagetex.sout
file. But be prompted to run the generated SageMath script. - Run
sage
on said generated SageMath script, creating themain.sagetex.sout
file required - Run the LaTeX compiler (however many times necessary for references, etc...), but successfully this time
If you were to script this, it would look something like:
pdflatex main.tex
sage main.sagetex.sage
pdflatex main.tex # possibly repeated a few times
Furthermore, if you were to be using latexmk
, either directly, or via you editor, then that first step may involve extra unnecessary compilation attemtps.
Solution: latexmk + Makefile
Firstly, using latexmk
is generally useful. If you aren't using it, maybe check out my other post about it. Also, make
is standard CLI tool for specifying how to build a project, you may already have, or can easily install.
One neat feature that you can enable in latexmk
, by either
- invoking it with the
-use-make
flag (latexmk -use-make ...
) - or by add the line
$use_make_for_missing_files = 1;
to thelatexmkrc
file for the project
... is to have latexmk
invoke make <filename>
whenever the LaTeX compiler complains about a file <filename>
being missing, before the subsequent calls to pdf/lualatex
. If we use it in our scenario, then make main.sagetex.sout
would be invoked after the first time latexmk
calls pdf/lualatex
.
To have make main.sagetex.sout
actually create the file required, add a Makefile
file in your project with the following:
# This may need to be adjusted to your filename, jobname, aux file location etc...
main.sagetex.sout: main.sagetex.sage
sage main.sagetex.sage
# possibly add other dependencies after the colon you
# may want for running your sagescript
# (such as notebook.py seen later in
# section "Solution: Jupyter notebooks")
With this ^ Makefile
created, you can open the project and run in the terminal:
# -use-make ommitable if setting already in latexmkrc
latexmk -use-make -pvc
... to build your document and watch to rebuild on any changes to the .tex
files.
The same can be achieved in LaTeX editors if set up to use latexmk
in the appropriate way.
Caveat
latexmk
will only call make main.sagetex.sout
if it is not present. If you make changes to any of the SageMath code after the first compile, you would need to run make main.sagetex.sout
yourself. The upside is that if you use latexmk -pvc
, then this will trigger a rebuild of the project automatically.
This isn't necessarily a bad thing, and could allow you to isolate your tasks without worrying about the main document braking on any given change.
TIP 3: Use dmath environment for long expressions
When generating long latex expressions into equation environments, you may run into the equations extending into the margins, or even off the page.
% demonstrate coefficients of particularly long binomial expansion
\begin{sagesilent}
var("x y") # declare symbols for 'x' and 'y'
\end{sagesilent}
\begin{equation}
\sage{expand((x+y)^10)}
\end{equation}
(TODO mini example)
The easy fix for this is to use the dmath
environment from the breqn
package. This environent behaves very similarly to the equation
environment,
however it breaks the equation over multiple lines appropriately.
% in preamble:
\usepackage{breqn}
% ... in document body
\begin{sagesilent}
var("x y") # declare symbols for 'x' and 'y'
\end{sagesilent}
% demonstrate coefficients of particularly long binomial expansion
\begin{dmath}
\sage{expand((x+y)^10)}
\end{dmath}
(TODO show render)
TIP 4: Don't put everything into sagesilent
Problem: Bad SageMath experience inside LaTeX
The path of least resistance when writing Sage Code to be used by SageTex in a
latex document, is to write it into sagesilent
or sageblock
environments.
\begin{sagesilent}
# long calculations:
# ...
final_expression = ...
illustrative_plot = ...
\end{sagesilent}
\begin{equation}
\sage{final_expression}
\end{equation}
\sageplot{illustrative_plot}
But when iterating on the code to do the correct thing (especially so for creating plots), the feedback loop is very slow (run latex, run sage on generated script, ... then run latex again...). Furthermore, any syntax error will make the second step (running the generated SageMath script) fail completely, making you repeat the first two steps again. As the document gets larger, it gets even slower and irritating. SageMath provides a brilliant Jupyter Notebook experience, especially with the cells which you can run individually to make small adjustments on the fly when creating the code. It's natural to do the initial development in notebook and copy-paste into the SageTex environments, however you may decide to develop on the code some more but will now find yourself in a more difficult environent. My tip here is to never copy-paste the code into SageTex environments and hence never leave the Jupyter notebook.
There are also a few extra knacks which I have with developing in the SageTex environments, but these are more likely to be noticed by people with more general software experience:
- Syntax highlighting is poor/non-existant in some editors
- No language intelligence i.e. no find-references, rename-symbol (this is available for SageMath notebooks opened in VSCode)
- No debugger (again available for notebooks in VSCode)
Solution: Jupyter Notebooks
As mentioned above, I recommend writing most of the SageMath code in Jupyter notebooks, and create well-labeled variables for the expressions and plots to be used in the LaTeX document.
An object, say final_expression
, can be imported from a Python script
(but not a SageMath script), say notebook.py
as follows:
\begin{sagesilent}
from notebook import final_expression
\end{sagesilent}
\begin{equation}
\sage{final_expression}
\end{equation}
But I just said to do your SageMath work in a notebook, not a Python script...
Suppose, we have a SageMath notebook notebook.ipynb
defining
final_expression
instead, we need to create a corresponding Python script
notebook.py
.
The following steps in a unix-style shell would do this (including SageMath shell on Windows):
- From the notebook, create a SageMath script with
jupyter nbconvert --to script characteristic_curves.ipynb
(with erroneous.py
extension) - Correct
.py
extension to.sage
withmv notebook.py notebook.sage
- Remove the ipython call generated by
%display latex
which would crash normal Python/Sage withsed -e "/get_ipython/d" -i notebook.sage
- Convert SageMath script to valid Python with
sage --preparse notebook.sage
(creating file namednotebook.sage.py
) - Rename to required filename with
mv notebook.sage.py notebook.py
There's multiple steps here but this can be scripted.
Furthermore the make
utility is available in most environments where
SageMath is installed, so we can conveniently add rules to a Makefile which
recreates the required Python script whenever the notebook changes.
Have a look at the example repository that comes with this blog post (TODO)
to see this in action.
notebook.py: notebook.ipynb
jupyter nbconvert --to script characteristic_curves.ipynb
mv notebook.py notebook.sage
sed -e "/get_ipython/d" -i notebook.sage
sage --preparse notebook.sage
mv notebook.sage.py notebook.py
p.s. don't forget to include %display latex
to the first cell of the
notebook.
TIP 5: Make use of the latex_name named argument for var
Problem: LaTeX Representations
A big limitation of using the \sage
function to generate formulae in a latex
document, is that symbols in SageMath, unlike in
SymPy, must be valid Python
identifiers (i.e. can be a variable name).
The LaTeX representations are then derived from the variable name.
Typically, the latex representation is precisely the identifier, such as:
x
is displayed $x$ (with latex x
), and a_t
would be displayed $a_t$
(with latex a_t
).
Although the backslash \
is disallowed in Python identifiers, certain latex
command names are recognised and treated appropriately, for example:
Phi_t
is displayed $\Phi_t$ (with latex \Phi_t
).
sage: # var() creates SageMath variables so need to give valid var names
sage: var("x_2 Phi")
(x_2, Phi)
sage: latex(x_2) # latex generally matches variable name
x_{2}
sage: latex(Phi) # exceptions are made for common latex commands
\Phi
In particular, the caret symbol ^
is disallowed in Python identifiers
(it's an operator),
making it impossible to create symbols which display with superscripts.
But also, the -
symbol is also disallowed, making it impossible, for
example, to create a SageMath symbol for $\beta_{-}$.
sage: var("a^t")
ValueError: The name "a^t" is not a valid Python identifier.
sage: var("beta_{-}")
ValueError: The name "beta_{-}" is not a valid Python identifier.
On the face of it, this limits the expressiveness of the expressions that can be generated with SageMath into LaTeX documents. But there are ways around this.
Solution: latex_name
Turns out var
takes a named argument latex_name
to provide a LaTeX representation as a string. A tip here is to use "raw" strings (r"..."
) to avoid having to escape all the backslashes you are likely using.
For the last examples:
sage: var("a_to_the_t", latex_name="a^t")
a_to_the_t
sage: var("beta_min", latex_name=r"\beta_{-}")
beta_min
sage: latex(beta_min) # in a notebook with %display latex, the following latex would be rendered
{\beta_{-}}
Chaotic Solution
Another way of getting round this, at the cost of potential chaotic hackery, is to
make use of commands in (La)Tex such as \let
, \def
, and \renewcommand
to
move the responsibility of displaying the symbols the way you want, onto the
main LaTeX document, instead of SageMath. For some cases, this might be the more desirable solution.
Suppose you want to create an expression from SageMath involving $\beta^{n}{-}$.
In the SageMath code, you could just define a symbol for beta
(displayed as
$\beta$).
However, with the following latex code, you can redefine the \beta
command,
generated by SageMath to instead produce $\beta^{n}{-}$:
\let\originalbeta\beta
\renewcommand\beta{{\originalbeta^{n}_{-}}}
\begin{sagesilent}
var("beta")
\end{sagesilent}
\begin{equation*}
\sage{ (beta + 1)^2 }
\end{equation*}
This will render to the following: $$(\beta^n_- + 1)^2$$
Limiting the bad stuff
Redefining default values for things is inherently ugly, so here are a couple of ways of mitigating the harm.
We can tweak the above example to limit the scope of the redefinition for the
\beta
command with \begingroup
and \endgroup
, removing possible
unintended consequences in other parts of the document:
\begingroup % beginning of scope redefining \beta
\let\originalbeta\beta
\renewcommand\beta{{\originalbeta^{n}_{-}}}
...
\begin{equation*}
\sage{ (beta + 1)^2 }
\end{equation*}
\endgroup % end of scope redefining \beta
In this example, $\beta$ is redefined to be a relatively similar $\beta^n_-$.
The variable name in the Sage code is still beta
, but happens to still fit
quite well.
However, you may want to create a symbol which displays to something very
different to any standard symbol in SageMath, say $\mathrm{ch}_2^\beta(v)$, in
which case the variable name will not match very well.
In such an example, it would make sense to redefine a symbol you are unlikely
to use, and store it to an appropriate variable name in the SageMath code:
\begingroup % beginning of scope redefining \kappa (second_twisted_chern)
\let\originalkappa\kappa
\renewcommand\kappa{{\mathrm{ch}_2^\beta(v)}}
\begin{sagesilent}
# Create symbol for kappa (to display in the latex differently)
# But store it with a variable name better suited the intended mathematical
# meaning
second_twisted_chern = var("kappa")
\end{sagesilent}
\begin{equation*}
\sage{ (second_twisted_chern + 1)^2 }
\end{equation*}
\endgroup % end of scope redefining \kappa (second_twisted_chern)
This would render to: $$\left(\mathrm{ch}_2^\beta(v) + 1\right)^2$$