← quantChapter 1 · Your first pricer5 min read

Recording a tape

Learn how AadRecorder, AadTape, and FlatTape record Java operations into a computational tape for automatic differentiation.

One ADouble is one recorded operation. A real payoff touches dozens of them. What actually holds all those little recordings together into one thing you can replay?

The whole story

A lambda that records ADouble operations runs once through AadRecorder, producing a frozen AadTape stored as parallel arrays — op, argA, argB, constant — in recording order. Every replaying engine then copies that same tape once more into a FlatTape of plain public arrays for its innermost per-scenario loop.

Did you know?

An AadTape isn't a tree of Node objects pointing at their children — it's a handful of parallel primitive arrays (op[], argA[], argB[], constant[], ...) indexed by position. Nothing needs sorting: nodes are appended in the order your code creates them, which is already a valid order for the forward sweep, and its reverse already works for the backward one.

The record step: one lambda, run exactly once

Skip finance entirely — this is the smallest tape that isn't trivial, the same (x+2)*x from adjoint AD for dummies, written the way application code actually writes it:

AadTape tape = AadRecorder.record(rec -> {
  ADouble x = rec.input("x", 3.0);
  ADouble a = x.add(2.0);
  rec.output(a.mul(x));
});
public static AadTape record(Consumer<AadRecorder> body) {
  AadRecorder recorder = new AadRecorder();
  body.accept(recorder);
  return recorder.builder.build();
}

record makes a fresh recorder, runs your lambda exactly once — with x at its recorded value, 3.0 — and hands back whatever .build() produces once every ADouble operation inside the lambda has appended its node. Four nodes come out of these three lines: x.add(2.0) is secretly two operations (a CONST node for the literal 2.0, then the ADD), so the tape ends up INPUT, CONST, ADD, MUL — exactly the four boxes in the picture above.

Walking the tape you just built

AadTape exposes that array by position, not by name — op(node), argA(node), argB(node) — so you can read a recording back the same way the reverse sweep does:

for (int i = 0; i < tape.size(); i++) {
  System.out.println(i + ": " + tape.op(i));
}
// 0: INPUT
// 1: CONST
// 2: ADD
// 3: MUL

Which nodes actually need an adjoint

Not every node costs something in the reverse sweep. AadTape marks each one active or not the moment it's built:

A node is active when its value traces back to at least one INPUT — that's it. Constants and random draws start inactive, and inactivity spreads forward through anything built only from them, so a tape with a lot of market-data constants can skip most of them in the reverse sweep for free.

Here are the same rules in the language of the small (x + 2) * x example:

ExpressionTape nodeActive?Why
rec.input("x", 3.0)INPUTYesIt is a differentiable starting value.
2.0 in x.add(2.0)CONSTNoIt is fixed and has no input dependency.
x.add(2.0)ADDYesIts value depends on x.
a.mul(x)MULYesAt least one operand is active.
rec.randn()RANDNNoThe draw changes by scenario but is not a differentiable input.

The table is about dependency, not whether a value changes. A random draw can change on every scenario and still be inactive; an input can keep the same value on every scenario and still be active.

Did you know?

"Active" means depends on an input, not reaches an output. A node built from x that never flows into rec.output(...) is still active — it still gets an adjoint slot and still costs time in the reverse sweep — because nothing here checks whether it was ever used downstream. AadTape prunes the constant/random subgraph for you; it doesn't prune dead code.

Try it yourself

Add one more line to the lambda above — something derived from x that never reaches rec.output(...):

ADouble unused = x.mul(x).mul(x);

Guess, then check: does tape.size() grow? Does tape.isActive(...) on unused's node come back true or false? (It grows by two nodes, and yes, active — see the sidenote above.)

FlatTape: the same tape, copied once more for the loop

Every engine replays the same tape millions of times, and AadTape.op(int) is a method call — a bounds check and a field load the JIT won't hoist out of a per-scenario inner loop no matter how hot it gets. FlatTape exists to remove that call: it's the identical data, copied once into plain public final arrays, so the sweep reads a single array slot instead of going through an accessor. It's marked @Internal — application code never touches it; it's plumbing for whoever implements an engine.

▶️ Run it

Still nothing replayed against real scenarios — this tape has one input, one output, and no engine attached to it yet — but the recording itself, and the walk over it above, is code you can edit, compile, and run right here:

Java · compile and run in this browser

Try changing rec.output(x.add(two).mul(x)) to add another operation and rerun. The updated expression is recorded again through the production AadRecorder/AadTape API.

Chapter 1.3 is where AadEngines/AadExecutable take a tape like this one and actually replay it, millions of times, for a real option.

⚠️ What this doesn't do

This page stops at a tape that exists in memory. It doesn't show a forward sweep producing a value, a backward sweep producing a gradient, randn()/ randu() turning into scenarios, or a single one of the six engines — all of that needs a tape to already exist, which is exactly what this page built.

What's next

→ Deeper: AadTape.java has the rest of the API this page didn't need — named outputs, multiple random streams, recordedInputs() — everything a replay engine actually reads. → Next: Replaying it, turning a tape shaped exactly like this one into a price, delta, vega, and rho — for a real option.


Questions or corrections? open an issue