Compound Exam

This example demonstrates pygacity’s primary strength: generating multiple, individually unique exam versions from a single set of source files. Numerical questions are parameterized — each serial number gets a different set of values drawn from a defined pick-list — and question pools (short-answer, fill-in-the-blank, true/false, and multiple-choice) are randomly sampled and shuffled per serial. All files are in the examples/compound_exam/ directory.

compound_exam/
├── CompoundExam.yaml                  # pygacity configuration
├── compound_arithmetic.tex            # Problem 1: parameterized numerics
├── simple_integration.tex             # Problem 3: static with answer registration
├── derivatives_short_answer.yaml      # Problem 2: short-answer pool
├── geography_fill_in_the_blank.yaml   # Problem 4: fill-in-the-blank pool
├── true_false_questions.yaml          # Problem 5: true/false pool
└── multiple_choice_questions.yaml     # Problem 6: multiple-choice pool

The Configuration File

document:
  preamble:
    pagestyle: ~
    font: |
      \setmainfont{Lato}
    commands:
      Universityname: University of Nowhere
      Departmentname: Mechanical Engineering
      Instructorname: Eustace T. Smartypants
      Instructoremail: ets@unow.edu
      Termcode: 202525
      Termname: Winter 2025-2026
      Coursename: BIO 5678 - Applied Pyrohydrodynamics
  structure:
    - pythontex:
        - setup
    - text: |
        \examheader{Exam I}{January 13, 2026}
        \noindent\textbf{Instructions:} This exam is closed book.
    - question_number: 1
      source: compound_arithmetic.tex
      points: 30
    - question_number: 2
      source: short.tex
      points: 30
      config: derivatives_short_answer.yaml
    - question_number: 3
      source: simple_integration.tex
      points: 20
    - question_number: 4
      source: short.tex
      points: 20
      config: geography_fill_in_the_blank.yaml
    - question_number: 5
      source: short.tex
      points: 10
      config: true_false_questions.yaml
    - question_number: 6
      source: short.tex
      points: 10
      config: multiple_choice_questions.yaml
    - pythontex:
        - teardown
build:
  seed: 12345
  copies: 3
  job-name: ExamI
  paths:
    build-dir: ./build

Several features here go beyond the Simple Assignment example.

build.seed and build.copies — instead of one document, pygacity generates copies: 3 independently seeded versions. The seed value seeds the random number generator used to derive each serial’s unique seed, making the set of exams fully reproducible. Each version receives a unique serial number embedded in the header and filename.

pythontex blocks — the first pythontex: [setup] block initializes pygacity’s pythontex session: it creates the Pick, AnsSet, and rng objects that parameterized source files use. The matching pythontex: [teardown] block at the end persists the collected answers so that the answer set document can be assembled afterwards.

question blocks — each block with a question_number key is a numbered question rendered as an \item in the compiled document. The points key sets the point value displayed next to each question. The optional config key points to a YAML question-pool file used by short.tex.

short.tex reuse — the same packaged short.tex template appears four times, each time with a different config: YAML file specifying the question pool, question type, count, and instructions for that section.

Parameterized Problem: compound_arithmetic.tex

\begin{pycode}
part_a = Pick.pick_state({'A': {'default': 12, 'pick': {'pickfrom': [10, 12, 14]}},
                          'B': {'default': 3,  'pick': {'pickfrom': [2, 3, 4]}}})
part_a.answer = part_a.A * part_a.B
part_b = Pick.pick_state({'C': {'default': 45, 'pick': {'pickfrom': [40, 45, 50]}},
                          'D': 5})
part_b.answer = part_b.C // part_b.D
part_c = Pick.pick_state({'E': 7,
                          'F': {'pick': {'pickfrom': [8, 9, 10]}}})
part_c.answer = part_c.E + part_c.F
AnsSet.register(qno, label='a', value=part_a.answer)
AnsSet.register(qno, label='b', value=part_b.answer)
AnsSet.register(qno, label='c', value=part_c.answer)
\end{pycode}
\textbf{Simple Arithmetic.} Solve the following arithmetic problems:

\begin{enumerate}

\item[a.] What is \py{part_a.A} \(\times\) \py{part_a.B}?
\ifshowsolutions\ \ \ \textcolor{blue}{\py{part_a.answer}}
\else
\vspace{1cm}
\fi
\item[b.] What is \py{part_b.C} \(\div\) \py{part_b.D}?
\ifshowsolutions\ \ \ \textcolor{blue}{\py{part_b.answer}}
\else
\vspace{1cm}
\fi
\item[c.] What is \py{part_c.E} \(+\) \py{part_c.F}?
\ifshowsolutions\ \ \ \textcolor{blue}{\py{part_c.answer}}
\else
\vspace{1cm}
\fi

\end{enumerate}

This is the heart of pygacity’s compound-exam capability. Pick.pick_state draws one value for each named parameter from its pickfrom list (or uses the default if no pick list is given). The draws are seeded by the serial number, so the same serial always produces the same values. Pick.pick_state returns a Namespace whose members are accessed via dot notation.

AnsSet.register records each sub-part answer (labeled a, b, c) together with the problem index, so the answer set document can tabulate all answers across all serials automatically.

The \py{...} commands embed the drawn values directly in the typeset question text, so no manual editing of question numbers is ever needed.

Static Problem with Answer Registration: simple_integration.tex

\textbf{Integration Problems.} Compute the following integrals:
\begin{enumerate}

\item[a.] What is the indefinite integral of \(f(x)=3x^2+2x+1\)?
\ifshowsolutions\ \ \ \textcolor{blue}{\(\int f(x)dx = x^3 + x^2 + x + C\)}
\else
\vspace{1.4cm}
\fi
\begin{pycode}
if 'AnsSet' in locals():
    AnsSet.register(qno, label='a', value=r'\(x^3 + x^2 + x + C\)')
\end{pycode}

\item[b.] What is the definite integral of \(f(x)=\cos(x)\) from \(0\) to \(\pi/2\)?
\ifshowsolutions\ \ \ \textcolor{blue}{\(\int_0^{\pi/2} \cos(x) dx = 1\)}
\else
\vspace{1.4cm}
\fi
\begin{pycode}
if 'AnsSet' in locals():
    AnsSet.register(qno, label='b', value=r'\(1\)')
\end{pycode}

\item[c.] What is the indefinite integral of \(f(x)=e^{3x}\)?
\ifshowsolutions\ \ \ \textcolor{blue}{\(\int f(x)dx = \frac{1}{3} e^{3x} + C\)}
\else
\vspace{1.4cm}
\fi
\begin{pycode}
if 'AnsSet' in locals():
    AnsSet.register(qno, label='c', value=r'\(\frac{1}{3} e^{3x} + C\)')
\end{pycode}

\end{enumerate}

Static problems — whose content does not vary between serials — still need to register their answers so they appear in the answer set. The AnsSet.register calls are guarded by if 'AnsSet' in locals() so the file also works in contexts where no answer set is being built (e.g. the Simple Assignment example).

Question Pool Files

Each short.tex section is driven by a YAML file that specifies the question pool. The count and shuffle: true config keys mean each serial draws a random subset in a random order from the pool.

derivatives_short_answer.yaml — 8 derivatives, 3 drawn per serial:

config:
  count: 3
  shuffle: true
  instructions: |
    \textbf{Derivatives.} Provide the correct derivative for each of the following functions.
  type: short_answer
content:
  - Q: \(f(x) = x^2\)
    A: \(f^\prime(x) = 2x\)
    text: The power rule states that the derivative of \(x^n\) is \(nx^{n-1}\).
  - Q: \(f(x) = 2x^3 + 3x^2\) 
    A: \(f^\prime(x) = 6x^2 + 6x\)
    text: The power rule states that the derivative of \(x^n\) is \(nx^{n-1}\), and the derivative of a sum is the sum of the derivatives.
  - Q: \(f(x) = \sin(x)\) 
    A: \(f^\prime(x) = \cos(x)\)
    text: |
      The derivative of \(\sin(x)\) is \(\cos(x)\) according to standard differentiation rules.  Or, you can consult the
      series expansion of \(\sin(x)\) and differentiate term by term.
  - Q: \(f(x) = \ln(x)\) 
    A: \(f^\prime(x) = \frac{1}{x}\)
    text: The derivative of \(\ln(x)\) is \(\frac{1}{x}\) according to standard differentiation rules.
  - Q: \(f(x) = e^{2x}\) 
    A: \(f^\prime(x) = 2e^{2x}\)
    text: The derivative of \(e^{kx}\) is \(ke^{kx}\) according to standard differentiation rules.
  - Q: \(f(x) = x^4 - 4x^3 + 6x^2 - 4x + 1\) 
    A: \(f^\prime(x) = 4x^3 - 12x^2 + 12x - 4\)
    text: The power rule states that the derivative of \(x^n\) is \(nx^{n-1}\), and the derivative of a sum is the sum of the derivatives.
  - Q: \(f(x) = \cos(x)\) 
    A: \(f^\prime(x) = -\sin(x)\)
    text: The derivative of \(\cos(x)\) is \(-\sin(x)\) according to standard differentiation rules.
  - Q: \(f(x) = \tan(x)\) 
    A: \(f^\prime(x) = \sec^2(x)\)
    text: The derivative of \(\tan(x)\) is \(\sec^2(x)\) according to standard differentiation rules.

geography_fill_in_the_blank.yaml — 11 fill-in-the-blank geography questions, 4 drawn per serial:

config:
  count: 4
  shuffle: True
  instructions: |
    \textbf{State and World Capitals.} Write the correct US State, Canadian Province, or World Country or its capital in the blank provided.
  type: fill_in_the_blank
content:
  - Q: The capital of Texas is ___.
    A: Austin
    text: Austin became the capital of Texas in 1839.
  - Q: The capital of Ontario is ___.
    A: Toronto
    text: Toronto is the capital city of the province of Ontario in Canada.
  - Q: The capital of France is ___.
    A: Paris
    text: Paris has been the capital of France since the 10th century.
  - Q: The capital of Japan is ___.
    A: Tokyo
    text: Tokyo has been the capital of Japan since 1868.
  - Q: The capital of Germany is ___.
    A: Berlin
    text: Berlin has been the capital of Germany since 1990 following reunification.
  - Q: The capital of ___ is Ottawa.
    A: Canada
    text: Ottawa is the capital city of Canada.
  - Q: The capital of ___ is Canberra.
    A: Australia
    text: Canberra is the capital city of Australia.
  - Q: The capital of ___ is Bras\'ilia.
    A: Brazil
    text: Bras\'ilia has been the capital of Brazil since 1960.
  - Q: The capital of ___ is New Delhi.
    A: India
    text: New Delhi has been the capital of India since 1911.
  - Q: The capital of ___ is Moscow.
    A: Russia
    text: Moscow has been the capital of Russia since 1918.
  - Q: The capital of ___ is Cairo.
    A: Egypt
    text: Cairo has been the capital of Egypt since 972 AD.

true_false_questions.yaml — 8 true/false statements, 4 drawn per serial:

config:
  count: 4
  shuffle: true
  instructions: |
    \textbf{True/False Questions.} Indicate whether each of the following statements is true (``T'') or false (``F'').
  type: true_false
content:
  - Q: On a piano, the frequency of middle C is approximately 261.63 Hz.
    A: T
    text: The frequency of middle C (C4) is indeed approximately 261.63 Hz.
  - Q: The chemical symbol for gold is Ag.
    A: F
    text: The chemical symbol for gold is Au; Ag is the symbol for silver.
  - Q: The Great Wall of China is visible from space with the naked eye.
    A: F
    text: This is a common myth; the Great Wall is not easily visible from space without aid.
  - Q: Water boils at 100 degrees Celsius at standard atmospheric pressure.
    A: T
    text: At standard atmospheric pressure (1 atm), water boils at 100 degrees Celsius.
  - Q: The capital of Australia is Sydney.
    A: F
    text: The capital of Australia is Canberra, not Sydney.
  - Q: Humans have four lungs.
    A: F
    text: Humans have two lungs, not four.
  - Q: The speed of light in a vacuum is approximately \(3.00 \times 10^8\) meters per second.
    A: T
    text: The speed of light in a vacuum is approximately 299,792,458 meters per second, often rounded to \(3.00 \times 10^8\) m/s.
  - Q: The largest planet in our solar system is Saturn.
    A: F
    text: The largest planet in our solar system is Jupiter, not Saturn.
    

multiple_choice_questions.yaml — 13 multiple-choice questions, 4 drawn per serial with choices shuffled. Two questions illustrate per-question shuffle control: one uses shuffle_choices: false because a choice cross-references others by label; another uses fixed_last: true to pin a None of the above option while freely shuffling the remaining choices:

config:
  count: 4
  shuffle: true
  instructions: |
    \textbf{Multiple choice questions.}  Circle the letter of the correct answer from the choices provided.
  type: multiple_choice
  shuffle_choices: true
content:
  - Q: What is the capital of Italy?
    choices:
      A: Madrid
      B: Rome
      C: Berlin
      D: Lisbon
    A: B
    text: Rome is the capital city of Italy.
  - Q: Which element has the chemical symbol 'O'?
    choices:
      a: Gold
      b: Oxygen
      c: Osmium
      d: Silver
    A: b
    text: Oxygen has the chemical symbol 'O'.  Note that the case of the choice keys are preserved in the final document.
  - Q: Who wrote "Romeo and Juliet"?
    choices:
      A: Charles Dickens
      B: Mark Twain
      C: William Shakespeare
      D: Jane Austen
    A: C
    text: William Shakespeare is the author of "Romeo and Juliet".
  - Q: What is the largest mammal in the world?
    choices:
      A: African Elephant
      B: Blue Whale
      C: Giraffe
      D: Great White Shark
    A: B
    text: The Blue Whale is the largest mammal in the world.
  - Q: What is the boiling point of water at standard atmospheric pressure?
    choices:
      A: 90 degrees Celsius
      B: 100 degrees Celsius
      C: 110 degrees Celsius
      D: 120 degrees Celsius
    A: B
    text: Water boils at 100 degrees Celsius at standard atmospheric pressure (1 atm).  
  - Q: Which planet is known as the Red Planet?
    choices:
      A: Venus
      B: Mars
      C: Jupiter
      D: Saturn
    A: B
    text: Mars is known as the Red Planet due to its reddish appearance caused by iron oxide
  - Q: Who painted the Mona Lisa?
    choices:
      A: Vincent van Gogh
      B: Pablo Picasso
      C: Leonardo da Vinci
      D: Claude Monet
    A: C
    text: The Mona Lisa was painted by Leonardo da Vinci.
  - Q: What is the smallest prime number?
    choices:
      A: 0
      B: 1
      C: 2
      D: 3
    A: C
    text: The smallest prime number is 2, which is the only even prime number.
  - Q: In which year did the Titanic sink?
    choices:
      A: 1910
      B: 1912
      C: 1914
      D: 1916
    A: B
    text: The Titanic sank in the North Atlantic Ocean on April 15, 1912, after hitting an iceberg.
  - Q: What is the chemical formula for table salt?
    choices:
      A: H2O
      B: CO2
      C: NaCl
      D: C6H12O6
    A: C
    text: The chemical formula for table salt is NaCl, which stands for sodium chloride.
  - Q: Who is known as the "Father of Modern Physics"?
    choices:
      A: Isaac Newton
      B: Albert Einstein
      C: Galileo Galilei
      D: Niels Bohr
    A: B
    text: Albert Einstein is often referred to as the "Father of Modern Physics" for his contributions to the field, including the theory of relativity.
  - Q: "Which of the following statements about Newton's laws of motion are correct?
      (Select the best answer.)"
    shuffle_choices: false
    choices:
      A: The first law states that an object at rest stays at rest unless acted on by a net force.
      B: The second law states that F = ma.
      C: Both A and B are correct.
      D: Neither A nor B is correct.
    A: C
    text: Both statements are correct. Choice C cross-references A and B by label,
      so shuffling choices would make the cross-references wrong. \texttt{shuffle\_choices} is
      set to false for this question to override the global \texttt{shuffle\_choices} setting.
  - Q: Which of the following is a fossil fuel?
    fixed_last: true
    choices:
      A: Solar energy
      B: Wind energy
      C: Natural gas
      D: None of the above.
    A: C
    text: Natural gas is a fossil fuel. The other options are renewable energy sources.
      The last choice ``None of the above''' is pinned in place with \texttt{fixed\_last}, while
      choices A-C are shuffled freely.

Building the Document

From inside the compound_exam/ directory, run:

pygacity build CompoundExam.yaml

Pygacity generates one student PDF and one solutions PDF per serial, then assembles a single answer set PDF covering all serials.

Output

The build/ directory contains:

  • ExamI-{serial}.pdf — student exam for each serial

  • ExamI_soln-{serial}.pdf — instructor copy with solutions for each serial

  • answerset.pdf — consolidated answer key for all serials

  • buildfiles.zip / solnbuildfiles.zip — zipped LaTeX sources

  • tex_artifacts.zip — intermediate LaTeX artifacts

Two serials side by side — the arithmetic problem values differ because each serial’s Pick.pick_state draws independently:

Exam page 1, serial 11364882

ser. 11364882: 14x4, 40÷5, 7+10

Exam page 1, serial 50082762

ser. 50082762: 14x3, 45÷5, 7+8

Solutions for serial 11364882 (page 1):

Solutions page 1, serial 11364882

Answer set — one table per question, one row per serial:

Answer set PDF