Universe

Create project in app

Randomized exam sets from one plain Typst document: question order and MCQ option order shuffle deterministically per set, with auto-generated answer keys and machine-readable grading metadata. For courses mixing multiple choice, fill-in-the-blank, and subjective questions.

Write the paper. That’s it.

#import "@preview/quizforge:0.1.0": *

#show: quiz.with(
  id: "dl-quiz-2",                      // freeze once printed — it seeds the shuffles
  course: "ES 667: Deep Learning",
  title: "Quiz 2 — Optimization",
  sets: ("A", "B", "C", "D"),
  answer-grid: true,
)

= Multiple Choice

Choose the single best answer unless a question says otherwise.

+ #m(2) Adding an L2 penalty $lambda norm(theta)_2^2$ corresponds to which prior?
  - ✓ Gaussian
  - Laplace
  - Uniform
  - None of the above          // auto-pinned to stay last
  #explain[A zero-mean Gaussian prior yields the L2 penalty (L1 ↔ Laplace).]

+ #m(2) Which of the following reduce overfitting?
  - ✓ Dropout
  - ✓ Weight decay             // two ✓ → automatically multi-select
  - Increasing model capacity

= Fill in the Blanks

+ #m(1) Halting training when validation loss stops improving is called
  #blank[early stopping].

= Long Answers #section(shuffle: false)

+ #m(4) Explain why Adam needs bias correction early in training.
  #answer(6cm, rubric: [+2 EMA init at zero; +2 correction factor.])[
    Moment estimates start at zero, so early EMAs are biased toward zero;
    dividing by $1 - beta^t$ corrects this.]

Compiled as-is, this document renders the answer key — ✓ marks highlighted, blanks filled, model answers and rubrics shown, the MCQ answer grid pre-filled. Randomized student papers come from the same file:

typst compile exam.typ set-B.pdf --input set=B --input mode=exam

Each set gets a cover page (set code, name/roll fields, computed totals and per-part subtotals, instructions), per-page set headers/footers, and continuous question numbers. Page furniture is customizable: quiz.with(header: none) turns the header off, footer: [fixed content] replaces it, and footer: info => [...] receives (exam, set, mode, total) for dynamic footers. All marks arithmetic is automatic.

How the randomization works

  • = headings become parts; + items are questions; - items are options; (or #yes) marks correct ones. Question type is inferred: options → MCQ, #blank[...] → fill-in-the-blank, otherwise subjective.
  • Every set is a pure function of (quiz id, set id, question text) — a dependency-free seeded PRNG (FNV-1a → xorshift32 → Fisher–Yates), no clocks, no OS randomness. Rebuilds are identical, on any machine, forever.
  • Fairness is structural: every set contains exactly the same questions and total marks; only order differs. Section order never changes.
  • “None/All/Both of the above” options pin to the last position automatically; #pin / #pin-first pin anything else; #opts(shuffle: false) freezes one question’s options; #section(shuffle: false) freezes a part’s question order.
  • The key and the paper derive from the same realized structure — they cannot disagree.

Grading support

Every compiled document embeds a pure-data <answerkey> metadata node:

typst query exam.typ "<answerkey>" --field value --one --input set=B

returns question order, correct letters (mapped through each set’s permutation), fill-in answers, marks, and the full option permutation per question — everything needed to grade or to drive external tooling. The repository ships a build.py that compiles all sets in parallel, re-verifies the fairness invariants, and writes a combined answer_key.csv + SHA-256 manifest.

Markers reference

Marker Meaning
#m(2) marks for the question (default 1)
, or #yes correct option
#blank[ans], #blank(width: 3cm)[ans] a blank; answer shown only in the key
#answer(6cm)[model], #answer(none, rubric: [...])[...] subjective answer space + key-only model answer; none prints no space (answer-booklet style)
#explain[...] key-only explanation for an MCQ
#pin / #pin-first pin an option last / first
#opts(shuffle: false, columns: 2, compact: true, multiple: true) per-question overrides; compact flows options inline to save space
#section(shuffle: false) freeze question order in a part
#qid("slug") freeze a question’s identity (see below)

Question identity defaults to a hash of the question’s own text: reordering or adding questions never reshuffles the others, and editing one question’s wording reshuffles only its own options. Add #qid("...") when you want a question’s option order stable across wording edits too.

Question banks (the second front-end)

For semester-scale reuse, build banks with constructors and select per exam — same engine, same guarantees, plus filtering and sampling:

#import "@preview/quizforge:0.1.0": *

#let bank = (
  mcq("conv-params", marks: 2, topic: "cnn", difficulty: "medium",
    [How many parameters does a $3 times 3$ conv, 16→32 channels, with bias have?],
    options: (ans[4640], [4608], [1184], [18464]),
    explanation: [$3 dot 3 dot 16 dot 32 + 32 = 4640$.]),
  // ... hundreds more, across files
)

#make-exam(
  exam: (id: "midsem", course: "…", title: "…", sets: ("A", "B")),
  questions: bank,
  sections: (
    (title: "MCQ", filter: (type: "mcq", difficulty: ("easy", "medium")),
     use: ("conv-params",),   // guaranteed to appear
     pick: 10),               // 10 total — the SAME 10 in every set
  ),
)

pick sampling is seeded without the set id, so every set gets the same questions — sampling can never break fairness.

Plays well with other packages

Question bodies, options, and blank answers are ordinary Typst content, so third-party packages work inside them — cetz drawings, unify/metro SI units, physica notation, code blocks, tables. (Answers whose text cannot be extracted statically, such as context-based qty(...), still render in the key; the grading CSV shows (see key) for them.) Global #set/#show styling goes above the #show: quiz line, where it styles the whole paper.

Validation = compilation

Structural mistakes fail the build with a message naming the question: no option marked ✓, a single-option question, a blank/answer mismatch, duplicate or unknown ids, out-of-range pick, free text stranded between shuffled questions, and more. If it compiles, it’s structurally sound — and the student PDF never embeds the answers.

License

MIT