Model Representation
Once your model is defined and your analyses run, you might want to show it off. Transcribing a computational model to equations can be a tedious affair. Furthermore, it is easy to get something wrong. We, therefore, sought to automate that process.
Models created by the Pumas
model macros contain all the information we need to automatically generate LaTeX equations from them using Latexify
.
Let us first define a model so that we can demonstrate this:
using Pumas
model = @model begin
@param begin
tvcl ∈ RealDomain(; lower = 0)
tvvc ∈ RealDomain(; lower = 0)
tvq ∈ RealDomain(; lower = 0)
tvvp ∈ RealDomain(; lower = 0)
Ω_pk ∈ PDiagDomain(4)
σ_prop_pk ∈ RealDomain(; lower = 0)
# PD parameters
tvturn ∈ RealDomain(; lower = 0)
tvebase ∈ RealDomain(; lower = 0)
tvec_50 ∈ RealDomain(; lower = 0)
Ω_pd ∈ PDiagDomain(1)
σ_add_pd ∈ RealDomain(; lower = 0)
end
@random begin
ηpk ~ MvNormal(Ω_pk)
ηpd ~ MvNormal(Ω_pd)
end
@pre begin
CL = tvcl * exp(ηpk[1])
Vc = tvvc * exp(ηpk[2])
Q = tvq * exp(ηpk[3])
Vp = tvvp * exp(ηpk[4])
e_base = tvebase * exp(ηpd[1])
ec_50 = tvec_50
e_max = 1
turn = tvturn
k_out = 1 / turn
k_in0 = e_base * kout
end
@init begin
Resp = e_base
end
@vars begin
conc := Central / Vc
e_drug := e_max * conc / (ec_50 + conc)
k_in := k_in0 * (1 - e_drug)
end
@dynamics begin
Central' = -(CL / Vc) * Central + (Q / Vp) * Peripheral - (Q / Vc) * Central
Peripheral' = (Q / Vc) * Central - (Q / Vp) * Peripheral
Resp' = k_in - k_out * Resp
end
@derived begin
dv ~ @. Normal(conc, sqrt(conc^2 * σ_prop_pk))
resp ~ @. Normal(Resp, sqrt(σ_add_pd))
end
end
PumasModel
Parameters: tvcl, tvvc, tvq, tvvp, Ω_pk, σ_prop_pk, tvturn, tvebase, tvec_50, Ω_pd, σ_add_pd
Random effects: ηpk, ηpd
Covariates:
Dynamical system variables: Central, Peripheral, Resp
Dynamical system type: Nonlinear ODE
Derived: dv, resp
Observed: dv, resp
If we want to extract LaTeX-formatted equations from the model
then we first need to load Latexify
:
using Latexify
We can now latexify
the different model blocks using calls like latexify(model, blockname)
where blockname
is a Symbol
that indicates which model block to latexify
. For example:
latexify(model, :random)
\begin{align*} {\eta}pk &\sim \mathrm{MvNormal}\left( \Omega_{pk} \right) \\ {\eta}pd &\sim \mathrm{MvNormal}\left( \Omega_{pd} \right) \end{align*}
latexify(model, :pre)
\begin{align*} CL &= tvcl \cdot e^{{\eta}pk_{1}} \\ Vc &= tvvc \cdot e^{{\eta}pk_{2}} \\ Q &= tvq \cdot e^{{\eta}pk_{3}} \\ Vp &= tvvp \cdot e^{{\eta}pk_{4}} \\ e_{base} &= tvebase \cdot e^{{\eta}pd_{1}} \\ ec_{50} &= tvec_{50} \\ e_{max} &= 1 \\ turn &= tvturn \\ k_{out} &= \frac{1}{turn} \\ k_{in0} &= e_{base} \cdot kout \end{align*}
latexify(model, :dynamics)
\begin{align*} \frac{dCentral(t)}{dt} =& \frac{Q \cdot Peripheral(t)}{Vp} - \frac{CL \cdot Central(t)}{Vc} - \frac{Q \cdot Central(t)}{Vc} \\ \frac{dPeripheral(t)}{dt} =& \frac{Q \cdot Central(t)}{Vc} - \frac{Q \cdot Peripheral(t)}{Vp} \\ \frac{dResp(t)}{dt} =& k_{in0} \cdot \left( 1 - \frac{e_{max} \cdot Central(t)}{\left( ec_{50} + \frac{Central(t)}{Vc} \right) \cdot Vc} \right) - k_{out} \cdot Resp(t) \end{align*}
latexify
uses the information you input via the model macro, so some models contain information that others do not. Which blocknames
are valid thus differ depending on the model
that you use. In this example, the blockname
s :param
, :random
, :pre
, :init
, :vars
, :dynamics
and :derived
would be valid since these are the model blocks that we explicitly defined.
Using the latexify output
The latexify
function returns a LaTeXString
, which is pretty much a standard String
with some overloads for making it display nicely. You can print
this string to see the generated LaTeX code:
println(latexify(model, :dynamics))
\begin{align}
\frac{\mathrm{d} \cdot Central(t)}{\mathrm{d}t} &= \frac{Q \cdot Peripheral(t)}{Vp} + \frac{ - CL \cdot Central(t)}{Vc} + \frac{ - Q \cdot Central(t)}{Vc} \\
\frac{\mathrm{d} \cdot Peripheral(t)}{\mathrm{d}t} &= \frac{ - Q \cdot Peripheral(t)}{Vp} + \frac{Q \cdot Central(t)}{Vc} \\
\frac{\mathrm{d} \cdot Resp(t)}{\mathrm{d}t} &= k_{in0} \cdot \left( 1 + \frac{ - e_{max} \cdot Central(t)}{Vc \cdot \left( ec_{50} + \frac{Central(t)}{Vc} \right)} \right) - k_{out} \cdot Resp(t)
\end{align}
Different editors/environments have different support for more complex rendering. latexify
automatically displays as nicely rendered equations in some environments, notably including notebooks. In other environments, like VSCode, you can call render
to render the equations:
render(latexify(model, :dynamics))
When we created Latexify
, we first thought that rendering would be somewhat frivolous, but quick access to readable equations has proven helpful during model development. Try for yourself.
Configurations
You can tune the latexify
output to different keyword arguments. Many are provided directly from Latexify
package, and some are specific to Pumas
models.
kwarg | Options | Description |
---|---|---|
show_t | true /false | Toggle (t) for the variables. |
italicize | true /false | Toggle variable italics. |
cdot | true /false | Toggle explicit/implicit multiplication operator. |
index | :subscript , :bracket | How to render indexing. |
For example:
latexify(model, :dynamics; show_t = false, italicize = false, index = :bracket)
\begin{align*} \frac{\mathrm{dCentral}}{dt} =& \frac{Q \cdot \mathrm{Peripheral}}{Vp} - \frac{CL \cdot \mathrm{Central}}{Vc} - \frac{Q \cdot \mathrm{Central}}{Vc} \\ \frac{\mathrm{dPeripheral}}{dt} =& \frac{Q \cdot \mathrm{Central}}{Vc} - \frac{Q \cdot \mathrm{Peripheral}}{Vp} \\ \frac{\mathrm{dResp}}{dt} =& k_{in0} \cdot \left( 1 - \frac{e_{max} \cdot \mathrm{Central}}{\left( ec_{50} + \frac{\mathrm{Central}}{Vc} \right) \cdot Vc} \right) - k_{out} \cdot \mathrm{Resp} \end{align*}
For more options and features, have a look at Latexify
's documentation.
Stitching things together
Since the latexify
output works well with other strings, you can quickly stitch different model parts together
model_representation = """
\\section{Model dynamics}
Here we might want to describe the model dynamics, as given by
$(latexify(model, :dynamics)).
\\section{Random effects}
The random effects are given by
$(latexify(model, :random)).
\\section{Conclusion}
You have a lot of power to automate your model representation.
Don't forget to escape your backslashes.
"""
println(model_representation)
\section{Model dynamics}
Here we might want to describe the model dynamics, as given by
\begin{align}
\frac{\mathrm{d} \cdot Central(t)}{\mathrm{d}t} &= \frac{Q \cdot Peripheral(t)}{Vp} + \frac{ - CL \cdot Central(t)}{Vc} + \frac{ - Q \cdot Central(t)}{Vc} \\
\frac{\mathrm{d} \cdot Peripheral(t)}{\mathrm{d}t} &= \frac{ - Q \cdot Peripheral(t)}{Vp} + \frac{Q \cdot Central(t)}{Vc} \\
\frac{\mathrm{d} \cdot Resp(t)}{\mathrm{d}t} &= k_{in0} \cdot \left( 1 + \frac{ - e_{max} \cdot Central(t)}{Vc \cdot \left( ec_{50} + \frac{Central(t)}{Vc} \right)} \right) - k_{out} \cdot Resp(t)
\end{align}
.
\section{Random effects}
The random effects are given by
\begin{align}
{\eta}pk &\sim \mathrm{MvNormal}\left( \Omega_{pk} \right) \\
{\eta}pd &\sim \mathrm{MvNormal}\left( \Omega_{pd} \right)
\end{align}
.
\section{Conclusion}
You have a lot of power to automate your model representation.
Don't forget to escape your backslashes.
A note about future releases
The overall functionality described here is considered part of our public API and should thus not break during anything but a major release (Pumas
follow semantic versioning). However, only the intention behind a latexify
command should be considered stable across non-breaking releases, not the exact formatting of the output string.