Before looking at the code, here are the 5 core Scala concepts you need to know:
class: A blueprint for creating objects (just like Java or Python).object: Declares a Singleton (a class that has exactly one instance). It is used for holding utility functions, constants, or the entry point (mainmethod). Think of it like static methods in Java.case class: A class optimized for holding data. Scala automatically generates a constructor, fields, a readabletoStringmethod, value-based comparison (==), and a.copy()method for you.
A trait is like an interface in Java. It defines a list of methods that other classes must implement:
trait ProblemGenerator[PD] {
def make(): PD // must be implemented by subclasses
}Scala has a unique feature called context parameters.
If a method signature ends with (using ctx: Context) or (implicit ctx: Context), it means you do not have to pass this argument explicitly if there is a matching context variable marked in the scope.
- Providing the context:
using myContextorimplicit val ctx = ... - Receiving the context:
def evaluate(f: Form)(using ctx: Context)
Scala collections are typically manipulated using functional operations:
-
map: Transforms every element in a list. E.g.,List(1, 2).map(x => x * 2)$\to$ List(2, 4). -
filter: Keeps elements matching a condition. E.g.,List(1, 2, 3).filter(x => x > 1)$\to$ List(2, 3). -
forall: Returns true if all elements match the condition. -
exists: Returns true if at least one element matches the condition. -
flatMap: Transforms elements and flattens nested lists. E.g.,List(1, 2).flatMap(x => List(x, x+1))$\to$ List(1, 2, 2, 3).
Pattern matching is like an advanced switch statement. It allows you to match an object against different structures or types and extract its inner fields directly:
expr match {
case Var(name) => println(s"Variable name: $name")
case Lit(value, domain) => println(s"Literal value: $value")
}This file implements the core mathematical and logical engine. It represents variables, numbers, operators, and logical formulas as syntax trees, and provides the functionality to programmatically evaluate them.
Here are some useful use cases:
In your problem generator, you define math elements using the following:
-
Integer Literals:
DInt(5)(represents the number$5$ ) -
Boolean Literals:
true/false -
Named Variables:
Var("x")(represents variable$x$ ) -
Boolean Variables:
BVar("a")(represents boolean variable$a$ )
You can build math trees using standard arithmetic symbols (Scala uses operator overloading to build these automatically):
val term = Var("x") + DInt(5) // Represents: x + 5
val term2 = Var("y") * Var("x") - 3 // Represents: y * x - 3You can compare terms or build boolean logic formulas using custom operators:
- Equality:
Var("x") === 5(maps to sTeX\equals{x}{5}) - Inequality:
Var("x") =!= 5(maps to sTeX\nequals{x}{5}) - Less than / Less-Equal:
Less(Var("x"), 10)orLessEq(Var("x"), 10) - Logical AND / OR / IMPLIES:
// Represents: (x == 5) AND (y < 3) And(Var("x") === 5, Less(Var("y"), 3)) // Represents: a => (b <= 2) Implies(BVar("a"), LessEq(Var("b"), 2))
To find the answer to a generated expression, you feed it to the Evaluator along with a Context containing current values:
// 1. Define the variable values:
val ctx = Context(List("x" -> 3, "y" -> 5))
// 2. Compute a math term:
val sum = Evaluator(Var("x") + Var("y"))(using ctx) // Returns: Lit(8, DInt)
// 3. Compute a logic formula:
val isTrue = Evaluator(Var("x") === 3)(using ctx) // Returns: trueAny expression can be rendered directly to TeX code using .toSTeX:
val term = Var("x") === 5
println(term.toSTeX) // Outputs: \equals{x}{5}This file contains utility methods for generating random values and constructing random mathematical expression trees recursively.
Here are some use cases:
When generating random problem settings, you often need to select from a set of possible options:
- Select a single random item:
// Picks one action randomly val randomAction = Generator.choose(List("up", "down", "left", "right"))
- Select multiple random items:
// Picks 2 to 3 unique variables from the list val randomVars = Generator.chooseSome(List("a", "b", "c", "d"), atLeast = 2, atMost = 3, allowRep = false)
- Flipping a coin (probability):
// Returns true with 70% probability, false with 30% val isStochastic = Generator.chooseBoolean(0.7)
- Choosing a random integer range:
// Picks a random integer between 3 and 5 (inclusive) val gridWidth = Generator.chooseInt(3, 5)
The file contains functions like genTerm and genForm to generate random syntax trees for exam questions (e.g. creating a random transition equation like (s + a) % 5).
It takes a State containing allowed variables, nesting depths, and operators. It recursively calls itself, deciding with a probability (based on current depth) whether to create a leaf node (a variable or literal number) or an operator node (like Plus, Times) with recursively generated sub-terms.
This file bridges Scala objects with LaTeX/sTeX, defining document structure binders and a custom string interpolator to generate formatted LaTeX documents.
Here are some usages:
This file defines standard containers for exam sheets. Printing any of these objects (using .toString) automatically produces \begin{env} ... \end{env} text blocks:
SDocument: The master container (includes\documentclass{article}and\usepackage{stexlight}).SProblem: Represents an exam question (\begin{sproblem}).SSubproblem: Represents a sub-question (\begin{subproblem}[pts=3]).SSolution: Wraps the solution (\begin{solution}[testspace=4.0cm]).
SItemize&SEnumerate: Bulleted or numbered lists.SItemize( x"Variables: ${csp.variables}", x"Constraints: ..." ) // Renders: \begin{itemize} \item Variables: ... \item Constraints: ... \end{itemize}
STabular: Renders tabular grids (used for joint probability tables and state-action transition tables).
This is an implicit syntax wrapper added to Scala's string engine. It lets you write LaTeX text with variables cleanly:
-
Math Mode Switch: Since Scala uses
$for variable injection, you must use§as the LaTeX math mode dollar sign instead of$. -
Auto-Typing: Any variable inside
${}is inspected and formatted automatically:- If it's a
FormorTerm(fromexpression.scala), it gets converted to LaTeX and wrapped in math-mode$...$. - If it's a
Stringor another primitive, it gets printed as plain text.
- If it's a
Example:
val x = Var("x")
val text = x"Assume §$x = 5§. The goal state is ${mdp.goal}."
// Outputs: "Assume $x = 5$. The goal state is $\tup{2,2}$."This file defines the template structure and selection rules for building exam sheets. It implements traits and classes to manage modular subproblems and select them dynamically based on constraints.
Usage:
To create an exam topic, you extend Problem and declare inner Subproblem objects and GroupConstraints:
Extend the Problem trait and specify intro() (the preamble text of the exam question):
case class MyProblem(data: MyData) extends Problem[MyProblem] {
override def intro(): SText = x"Consider the following system §S = ${data.name}§..."
}Inside your problem class, declare objects inheriting from Subproblem:
object subQuestion1 extends Subproblem(id = "theory", pts = 2, testspace = 3) {
// Checks if this question is applicable for the current random data
override def applicable(): Boolean = data.hasValidStates
// Runs right before printing (e.g. to pick a random state to ask about)
override def init(): Unit = { ... }
// The LaTeX question text
def question() = x"State the definition of..."
// The LaTeX solution text
def solution() = x"The definition is..."
}At the bottom of your problem class, use GroupConstraint to tell the generator how to compose the exam sheet:
// Forces the generator to pick exactly one of subQuestion1 or subQuestion2
GroupConstraint(atLeast = 1, atMost = 1, subQuestion1, subQuestion2)When the generator runs, the system automatically resolves constraints:
chooseSubproblems()is called.- It filters out any subproblems where
applicable()is false. - It checks
GroupConstraints, randomly discarding extra subproblems if they exceedatMost. - It calls
init()on the remaining chosen subproblems to bind random variables. - It prints the selected questions via
.toSTeX(...).
This file is the entry point of the system. It orchestrates the lifecycle by generating problem parameters, selecting subproblems, and printing the formatted LaTeX output.
package info.kwarc.probgen
/** simple main method to call generators and see their results */
object Test {
def main(args: Array[String]): Unit = {
val p = MDPGenerator.make() // 1. Run generator search loop for an MDP problem
val subs = p.chooseSubproblems() // 2. Filter & select questions
val stex = p.toSTeX(subs) // 3. Render questions to sTeX
if (args.nonEmpty && args(0) == "--full") {
val d = SDocument("problem", stex) // 4a. Format as standalone document
println(d.toStringFull)
} else {
println(stex) // 4b. Or print raw sTeX snippet
}
}
}Run the generator using sbt run. Example output:
% --- Starting Generator ---
% Found good problem after 5 attempts. (pol. iter.: false, steps: 4)
%%%%%%%%%
\begin{sproblem}
Consider the following agent in a stochastic environment.
The agent moves in a $$3$ \times $3$$ grid.
Its possible actions are $\mathtt{up}$, $\mathtt{down}$, $\mathtt{right}$, $\mathtt{left}$.
These succeed with a probability of 0.6 and fail without movement otherwise.
The agent starts at $\tup{0,0}$. Its goal is to reach $\tup{2,2}$.
We use a reward of 10.0 for the goal and -2.0 otherwise, and a discount factor of $\equals{\gamma}{0.5}$.
We analyze this using value iteration.
\begin{subproblem}[pts={3}]
Give the sets of states $S$ and actions $A$ for this MDP.
\begin{solution}[testspace={5.0cm}]
$\equals{S}{\set{\seq{\tup{0,0},\tup{0,1},\tup{0,2},\tup{1,0},\tup{1,1},\tup{1,2},\tup{2,0},\tup{2,1},\tup{2,2}}}}$, $\equals{A}{\set{\seq{\mathtt{up},\mathtt{down},\mathtt{right},\mathtt{left}}}}$
\end{solution}
\end{subproblem}
\begin{subproblem}[pts={2}]
State the Bellman Optimality Equation for $\apply{U,s}$.
\begin{solution}[testspace={3.0cm}]
$\equals{\apply{U,s}}{\intplus{\apply{R,s},\inttimes{\gamma,\max_{\inset{a}{A}}{\sum_{\inset{s'}{S}}{\inttimes{\CondProb{s'}{s,\,a},\apply{U,s'}}}}}}}$
\end{solution}
\end{subproblem}
\begin{subproblem}[pts={3}]
Assume current utilities $\equals{\apply{U,s}}{0}$ for all states. Perform one value iteration step to calculate $\apply{U,\tup{0,2}}$.
\begin{solution}[testspace={4.0cm}]
With $\equals{U}{0}$, future rewards are $0$. Thus: $\equals{\apply{U,s}}{\apply{R,s}}{-2}$
\end{solution}
\end{subproblem}
\begin{subproblem}[pts={3}]
Suppose we solved the utilities as $\apply{U,s}$ for every state $s$. How do we derive the optimal policy?
\begin{solution}[testspace={4.0cm}]
Calculate the utilities for all actions and pick the max: $\equals{\apply{\pi,s}}{\argmax_{\inset{s'}{S}}{\inttimes{\CondProb{s'}{s,\,a},\apply{U,s'}}}}$.
\end{solution}
\end{subproblem}
\begin{subproblem}[pts={2}]
Calculate the probability of the agent moving along to $\tup{0,0}$ and then to $\tup{1,0}$by taking the actions \mathtt{down} and \mathtt{right}.
\begin{solution}[testspace={3.0cm}]
\[ P = 1.0 \times 0.6 = 0.6 \]
\end{solution}
\end{subproblem}
\begin{subproblem}[pts={3}]
Now assume we use the corresponding POMDP with current belief state $\equals{\apply{b,\tup{0,0}}}{0.5}$, $\equals{\apply{b,\tup{0,1}}}{0.5}$ and apply action $\equals{a}{\tup{right,\tup{1,0}}}$. Calculate the new belief state.
\begin{solution}[testspace={4.0cm}]
$\equals{\apply{b',\tup{1,0}}}{0.3}$, $\equals{\apply{b',\tup{0,0}}}{0.2}$, $\equals{\apply{b',\tup{1,1}}}{0.3}$, $\equals{\apply{b',\tup{0,1}}}{0.2}$
\end{solution}
\end{subproblem}
\end{sproblem}