I wrote my PhD thesis on AI and Natural Language Processing entirely in Typst! The overall experience was great, especially compared to my previous thesis, which I wrote in LaTeX (and which involved many hours of frustration…).
I didn’t use any official template, as my university didn’t provide one. Instead, I built the entire document from scratch, which turned out to be much easier than I expected. I was able to achieve almost exactly the layout and style I had in mind, with the possible exception of the bibliography, which seems to be a common challenge for thesis writing in Typst.
I could probably have taken advantage of more packages, but in the end I mainly relied on cetz and fletcher for diagrams, hydra for chapter headers, and flagada for national flags (I included several examples featuring text translated into different languages). I would also have loved to recreate my result charts using lilaq , but time was running short, so I ended up embedding them as PDFs.
I hope this might be useful or encouraging to someone considering Typst for a large academic project. Below are a couple of screenshots, along with some code snippets!
Source code
The code for the Transformer architecture figure from Vaswani et al. (2017) (code is a bit convoluted here, I warn you):
#import "@preview/fletcher:0.5.8" as fletcher: diagram, node, edge
#import fletcher.shapes: house, hexagon, trapezium
#import "@preview/cetz:0.5.2": canvas, draw.bezier
#set par(leading: 0.5em)
#let blob(
pos,
label,
tint: color.white.transparentize(100%),
..args
) = node(
pos,
align(center, label),
width: 3cm,
fill: tint.lighten(60%),
stroke: 1pt + tint.darken(20%),
corner-radius: 5pt,
inset: 5pt,
..args,
)
// Renders a "block" of blobs with the main element in the center, the "Add & Norm" blob on top, and the dashed arrow on the side.
#let blob-add-norm(
pos,
group-name,
body,
tint: orange,
incoming-arrow-points: (-1, +1),
..args
) = {
if type(group-name) == label {
group-name = str(group-name)
}
// Main blob
let main-label = label(group-name + "-main")
blob(pos, name: main-label, [#body], tint: tint, ..args)
edge()
// Add & Norm blob
let add-label = label(group-name + "-add")
blob((rel: (0cm, 0.5cm), to: label(group-name + "-main.north")), name: add-label, [Add & Norm], tint: yellow, shape: hexagon, width: 2.8cm)
// Discriminate encoder (left) and decoder (right) blobs
let side = if group-name.starts-with("enc") {-2.2cm} else {2.2cm}
// Edge from init to Add & Norm
let btm-distance = if incoming-arrow-points.len() > 0 {0.8cm} else {0.4cm}
let bottom-origin-point = (rel: (0cm, -btm-distance), to: label(group-name + "-main.south"))
edge(
bottom-origin-point,
(rel: (side, 0cm), to: bottom-origin-point),
(rel: (side, 0cm), to: add-label),
add-label,
"--|>",
)
for x in incoming-arrow-points {
edge(
(rel: (0cm, -0.4cm), to: label(group-name + "-main.south")),
(rel: (x * 1cm, -0.4cm), to: label(group-name + "-main.south")),
(rel: (x * 1cm, 0cm), to: label(group-name + "-main.south")),
"-|>",
)
}
node(
enclose: (main-label, add-label, (rel: (side, 0cm), to: bottom-origin-point)),
stroke: none,
inset: 1pt,
name: label(group-name),
)
}
#diagram(
edge-stroke: 1pt,
edge-corner-radius: 5pt,
mark-scale: 70%,
{
blob((0cm, -0.5cm), name: <input>, [Inputs], stroke: none)
edge()
blob((0cm, 1cm), name: <input-embs>, [Input\ Embeddings], width: auto, tint: red.lighten(50%), shape: trapezium)
edge(<enc-mhatt-main>, "-|>")
blob-add-norm((0cm, 5cm), <enc-mhatt>, tint: orange)[Multi-Head\ Attention]
edge(<enc-mhatt-add>, <enc-ff-main>, "-|>")
blob-add-norm((0cm, 8cm), <enc-ff>, tint: aqua, incoming-arrow-points: ())[Feed\ Forward]
node(
name: <enc>,
enclose: (
<enc-mhatt>,
<enc-ff>
),
inset: 3mm,
fill: gray.transparentize(80%),
stroke: 1pt + gray,
corner-radius: 14pt,
)
let s = 5cm
blob((s, -0.5cm), [Outputs\ (shifted right)], name: <output>, stroke: none)
edge()
blob((s, 1cm), name: <output-embs>, [Output\ Embeddings], width: auto, tint: red.lighten(50%), shape: trapezium)
edge(<dec-mask-main>, "-|>")
blob-add-norm((s, 5.2cm), <dec-mask>, tint: orange)[Masked\ Multi-Head\ Attention]
edge(<dec-mask-add>, <dec-mhatt-main>, "-|>")
blob-add-norm((s, 8.7cm), <dec-mhatt>, tint: orange, incoming-arrow-points: (1, ))[Multi-Head\ Attention]
edge(<dec-mhatt-add>, <dec-ff-main>, "-|>")
blob-add-norm((s, 11.5cm), <dec-ff>, tint: aqua, incoming-arrow-points: ())[Feed\ Forward]
node(
name: <dec>,
enclose: (
<dec-mask>,
<dec-mhatt>,
<dec-ff>
),
inset: 3mm,
fill: gray.transparentize(80%),
stroke: 1pt + gray,
corner-radius: 14pt,
)
edge(
<enc-ff-add.north>,
(rel: (0cm, 1cm)),
(rel: (s / 2, 0cm)),
(rel: (-s / 2, -0.5cm), to: <dec-mhatt-main.south>),
(rel: (-0.4cm, -0.5cm), to: <dec-mhatt-main.south>),
(rel: (-0.4cm, 0cm), to: <dec-mhatt-main.south>),
"-|>",
)
edge(<dec-mhatt-add>, <linear>, "-|>")
blob((s, 14cm), name: <linear>, [Linear], tint: purple.lighten(50%))
edge("-|>")
blob((s, 15.2cm), name: <softmax>, [Softmax], tint: green)
edge("-|>")
blob((s, 16.8cm), [Output\ Probabilities],)
node((rel: (-0.5cm, 0cm), to: <enc.west>))[N$times$]
node((rel: (0.5cm, 0cm), to: <dec.east>))[N$times$]
let pos-enc = circle(radius: 12pt)[#canvas(bezier((-12pt, 0), (12pt, 0), (0, 16pt), (0, -16pt)))]
node((0cm, 2.5cm), inset: 0mm, label: <enc-plus>)[#text(size: 1.5em)[$plus.o$]]
edge()
node((-2cm, 2.5cm), inset: 0mm)[#stack(dir: ltr, spacing: 8pt, align(right)[Positional\ Encoding], pos-enc)]
node((s, 2.5cm), inset: 0mm, label: <dec-plus>)[#text(size: 1.5em)[$plus.o$]]
edge()
node((s + 2cm, 2.5cm), inset: 0mm)[#stack(dir: ltr, spacing: 8pt, pos-enc, align(left)[Positional\ Encoding])]
}
)
Source code
#let tt(body) = {
highlight(fill: aqua.transparentize(80%), radius: 30%)[#text(
font: "Manrope", // Any sans-serif font would work
size: 0.85em,
fill: blue.darken(50%),
)[#body]]
}
#let ve(body) = math.dash(math.bold(body))
#let veq = ve($q$)
#let vek = ve($k$)
#let vev = ve($v$)
#let vex = ve($x$)
#let vez = ve($z$)
#let vector-txt(dim, color: green) = {
let cell-size = 0.25cm
stack(
spacing: 0pt,
dir: ltr,
..range(dim).map(i => box(
fill: color.lighten(20%),
stroke: 1pt + color.darken(30%),
width: cell-size,
height: cell-size,
radius: 25%,
))
)
}
#let table-row(name, dim, formula, color: green) = (
align(right)[#name],
stack(text(color)[$formula_1$], vector-txt(dim, color: color), dir: ltr, spacing: 2mm),
stack(text(color)[$formula_2$], vector-txt(dim, color: color), dir: ltr, spacing: 2mm),
)
#figure(
table(
columns: 3,
stroke: none,
fill: none,
[], h(1.5em)+tt[thinking], h(1.5em)+tt[machines],
..table-row("Embeddings", 6, $vex$),
..table-row("Queries", 3, $veq$, color: purple),
..table-row("Keys", 3, $vek$, color: orange),
..table-row("Values", 3, $vev$, color: blue),
align(right)[Score], [#text(purple)[$veq_1$] $dot$ #text(orange)[$vek_1$] $ \/ sqrt(b)$], [#text(purple)[$veq_1$] $dot$ #text(orange)[$vek_2$] $ \/ sqrt(b)$],
align(right)[Softmax], table.cell(colspan: 2)[
#stack(
dir: ltr,
spacing: 2mm,
vector-txt(2, color: yellow),
)
],
..table-row([Softmax $times$ Values], 3, $vev$, color: blue),
align(right)[Sum], table.cell(colspan: 2)[
#stack(
dir: ltr,
spacing: 2mm,
text(red)[$vez_1$],
vector-txt(3, color: red),
)
]
),
caption: [Steps of the Self-Attention procedure using the sentence #tt[thinking machines].],
kind: image,
placement: auto,
)
Source code
#figure(
table(
columns: (1fr, 1fr, 1fr, 1fr, 2.5cm),
align: center + horizon,
stroke: (x: none, y: 0.5pt),
table.header([Metric], [Normalized\ in $[0; 1]$?], [Applicable to rectangular matrices?], [Range for rectangular matrices], [Ref.]),
table.hline(stroke: 1pt),
[Chi squared\ ($chi^2$)], sym.ballot, sym.ballot.check.heavy, [$[0; + oo)$], [],
[Phi coefficient\ ($phi$)], sym.ballot.check.heavy, sym.ballot, [n.a.], [],
[Tschuprow's T], sym.ballot.check.heavy, sym.ballot.check.heavy, [$[0; a] subset.neq [0; 1] $], [],
[Cramér's V], sym.ballot.check.heavy, sym.ballot.check.heavy, $[0; 1]$, [],
),
caption: [Resume table for the available correlation metrics for categorical variables. As it can be observed, Cramér's V metric is the only one that is both normalized and applicable to rectangular matrices, making it the most suitable for our bias detection method.],
)
<tab:label>


