Skip to main content

pset

Build assignments from a repo of LaTeX problems, each carrying its own solution and rubric.

A problem is worth more than the assignment it was written for. Keep each one in its own .tex file with its solution and rubric beside the question, and an assignment becomes a short document that \inputs the ones it wants: reusable across terms, reviewable in version control, and buildable into a student copy, an answer key and a grading rubric from the one source.

pip install pset
pset --example        # a working repo to copy the layout from

The layout

problems/                  every problem, grouped by topic
    number_rep/
        base_convert01.tex
        2s_comp_overflow01.tex
    logic/
        boolean_simplify01.tex
pset.tex                   \prob, \sol, \rub, \stud (generated)
packages.tex               shared by every assignment
command.tex                your styling, inputs pset.tex
hw1/
    hw1.tex                metadata, then the problems it inputs

pset --example writes exactly that, five problems and an assignment that builds, so the fastest way to read the rest of this is to build it:

pset --example
cd pset_example/hw_example
pset hw_example

Building an assignment

pset quiz1a              # quiz1a.pdf, quiz1a_sol.pdf, quiz1a_rub.pdf
pset quiz1*              # every version at once
pset quiz1/              # every document in a folder

All three copies are the default, minus any that would come out empty: a document with no \rub anywhere in it gets no rubric copy, since that copy would only duplicate the answer key. It says so when it skips one.

==> exam1a.tex
    exam1a.pdf  exam1a_sol.pdf
    no \rub in it, rub copy skipped

The check follows \input through the whole tree, because the content macros usually sit in the problem files rather than the document. Where the tree cannot be read to the end — a missing file, or an \input whose path is built from a macro — it builds the copy rather than guess. --rub forces one, --no-rub refuses it outright, and the same pair exists for --sol.

Arguments are files, globs or directories, with the .tex suffix optional. Globs are expanded by the tool rather than left to the shell, so quiz1* works the same on Windows. Two filters keep a wide pattern honest: sibling builds (quiz1a.pdf, quiz1a_sol.pdf) collapse onto the .tex they came from, and a fragment with no \documentclass is skipped.

The pdflatex transcript is hidden, leaving one line per document:

==> quiz1a.tex
    quiz1a.pdf  quiz1a_sol.pdf  quiz1a_rub.pdf

A failure prints the error pdflatex reported and keeps that document's .log, but does not stop the rest of the batch; the failures are listed at the end and the exit status is non-zero. -v restores the full transcript.

In the document

Three macros divide the source, and each copy is the same document with a different subset of them switched on.

macro student _sol _rub what goes in it
plain text yes yes yes the question
\stud yes space for the work
\sol yes yes the answer
\rub yes how to mark it

\sol and \rub are hidden by default and revealed by their own build. \stud is the reverse: on by default and dropped from both keys.

You do not write those definitions. pset --init generates them into a pset.tex at your repo root, and each document inputs it beside whatever other shared preamble it already has:

\input{../pset.tex}

The path is relative to the document being built, not to the file holding the line, so putting it in a command.tex that every document already inputs wires up the whole repo at once. --init prints the line rather than editing your documents, and re-running it upgrades the file in place (it refuses to touch a pset.tex it did not write).

Generating it is not just convenience. Hand-rolled, the definitions are easy to get subtly wrong, and the failure mode is a student PDF with the answers in it.

Find the decision boundary.

\stud{\vfill}
\sol{The boundary is $x = 3$.}
\rub{6 pts: correct slope.  3 pts if the intercept is off.}

\stud exists because the student copy and the answer key want opposite things in the same spot. The student needs room to work, so a problem ends with \stud{\vfill} or \stud{\vspace{2in}}; the key wants that space gone, or every answer sits alone on its own page and the grader pages through five sheets to mark one problem. Anything else addressed only to the student belongs in it too: \stud{Show your work.}, a fill-in table, a tear-off answer sheet.

Note it wraps content rather than switching a mode, so it takes an argument like the other two: \stud{...}, not \begin{stud}. Nesting works, so \sol{... \rub{...}} puts a per-part rubric inside the answer.

Styling them

pset.tex carries the mechanism, which is pset's, and leaves the look to you through three hooks. They default to printing their argument plainly, so the generated file needs no packages at all and cannot collide with your package set. Redefine any of them after the \input:

\renewcommand{\solstyle}[1]{
  \begin{tcolorbox}[colback=blue!3!white,title=Solution,breakable]#1\end{tcolorbox}}

\renewcommand{\rubstyle}[1]{
  \begin{tcolorbox}[colback=green!3!white,title=Rubric,breakable]#1\end{tcolorbox}}

\renewcommand{\probstyle}[2]{\section*{Problem #1 #2}}

\probstyle takes the number and the title separately. \prob itself is mechanism, not taste: -p reads the points out of \prob{[20 pts]: title}, so the macro name is part of the contract even though its look is yours.

The flags pset defines are \showsol and \showrub, deliberately not \sol and \rub. A document cannot both define a macro and ask whether it is defined: \newcommand{\sol} would make \ifdefined\sol true on every build, so the student copy would silently print the answers.

If your repo calls the macros something else, name them in pset.toml:

[macros]
sol = 'answer'
rub = 'rubric'

Reading the library

pset --browse

renders every problem into problems_pdf/, one PDF per topic folder of problems/, with the solutions shown. Worth doing before writing a new problem, and it catches the one that has stopped compiling. A topic that fails does not stop the others; the failures are listed at the end.

The generated document comes from a template which inputs your repo's packages.tex and command.tex. Drop your own pset_browse.tex at the repo root to change the layout.

Which problems have I already used?

pset --usage

writes prob_file_pair.json, pairing each problem with the assignments that \input it and each assignment with its problems. It flags any problem used by more than one assignment, which is how the same question lands on both the homework and the exam, and counts the ones never used (they keep an empty list, so grep '\[\]' finds them). -o writes elsewhere, which is how a per-term snapshot gets kept.

--browse and --usage both act on the whole repo and find its root by walking up from the working folder, so they run from anywhere inside it. The root is the folder holding problems/.

Counting points

-p sums the points per problem, using sum-pts: pip install "pset[pts]".

pset -p quiz1a

The default expects a bracket at the front of a \prob{...} title, and reads both spellings:

\prob{[20 pts (8, 12)]: Bayes Net}
\prob{[16 points (4 pts each)]: Ecology System}

If that is not your convention, a [points] table in a pset.toml at your repo root overrides it for every document underneath. The keys are sum-pts' own parameters, so its documentation is the reference:

[points]
prefix = ' *\\question'
points = '(marks|pts?)'

When the patterns do not fit, pset says so and links to docs/points.md, which explains each key and the traps. Points are read from the source, so -p still reports on a document that failed to build.

Setting up a repo

pset --init            # write pset.tex, print the line that inputs it
pset --readme          # write README_pset.md beside problems/
pset --example         # or scaffold a whole working repo from scratch

--init is the one an existing repo needs; it writes pset.tex and nothing else, since a repo with assignments in it already has a layout and a preamble, and neither is pset's to rewrite. --readme adds a short guide to the layout and commands for whoever clones the repo next; --example offers one too, and remembers the answer in ~/.config/pset/pset.toml so it only asks once.

Requires

pdflatex on the PATH, and Python 3.9 or newer.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pset-0.6.0.tar.gz (35.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pset-0.6.0-py3-none-any.whl (35.9 kB view details)

Uploaded Python 3

File details

Details for the file pset-0.6.0.tar.gz.

File metadata

  • Download URL: pset-0.6.0.tar.gz
  • Upload date:
  • Size: 35.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.13

File hashes

Hashes for pset-0.6.0.tar.gz
Algorithm Hash digest
SHA256 e69b6905bef68d080cc756c69f9fdda1c37ca2473f80b764e84b12f81781dd2e
MD5 998ec59be99842c902d2326ad261e747
BLAKE2b-256 22102ff08898557e7244b5aed140221cf63480499940d1304abc6bc7e000c47c

See more details on using hashes here.

File details

Details for the file pset-0.6.0-py3-none-any.whl.

File metadata

  • Download URL: pset-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 35.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.13

File hashes

Hashes for pset-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7a0770c7a03b90d87a5302d683da3cabb52aba6f2081a5179ffb548b3cdf689d
MD5 cf761321033645fbbbd361f41ccf14bb
BLAKE2b-256 1766e82fad3e5a9f7ca66830a2720ba743afd1aef6999377540428a2d5ec6832

See more details on using hashes here.

Release history Release notifications | RSS feed

0.6.1

2 files

This release

0.6.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page