ADouble, the number that remembers
Learn how NablaTensor ADouble values record arithmetic operations on an adjoint tape to calculate financial sensitivities.
A double forgets the instant it's computed. x.add(y) runs the addition,
hands you back a number, and by the time you look at it, x, y, and the
add itself are gone — there's no way to ask a double how it got its
value. What if a number didn't forget?
The whole story
An ADouble never stores the number it represents — not even the one it was
created with. Its entire field list is a reference to the recorder that made
it and one int: which node on the tape it is. rec.input("S0", 100.0)
records 100.0 as that node's default replay value, but the handle you get
back, the ADouble itself, carries no number at all until something actually
replays the tape.
The recorder is the builder and owner of that tape. You give it a piece of
valuation code once; each input and operation adds a node to the recorder's
graph. When the code finishes, the recorder seals the graph as an AadTape.
That tape can later be replayed with different inputs, and a reverse sweep can
walk it to recover sensitivities. The recorder is not another kind of number —
it is the context in which ADouble numbers are created and connected.
Inputs and constants are different
In the portfolio example, rec.input("stock", 100.0) and
rec.input("bond", 98.0) declare the quantities that may change from one
replay to the next. The second argument is only the input's default replay
value. It is stored on the tape so the example has a complete starting market;
it is not used to calculate anything while recording. At that point stock
and bond are only ADouble handles, and no value exists for either one.
Just before replay(), the example supplies the actual market for this run:
executable.setInput("stock", 100.0);
executable.setInput("bond", 98.0);
Those values could be 101.0 and 97.5 on the next replay without recording
or compiling the formula again. If setInput is omitted, the input's recorded
default is used.
The 2.0 and 3.0 values play a different role:
ADouble value = stock.mul(2.0).add(bond.mul(3.0));
They are fixed portfolio weights, not market inputs. mul(double) turns each
weight into a constant node in the tape, so there is no setInput call for
those values before replay. They stay the same every time this tape runs. If a
weight should vary between scenarios, it must be declared as an input instead:
ADouble stockWeight = rec.input("stockWeight", 2.0);
ADouble value = stock.mul(stockWeight);
That choice — input or constant — is part of designing the recorded formula.
A double computes; an ADouble records
Valuation code written against ADouble looks identical to code written
against double — that's deliberate, it's the whole reason a payoff is a
"seam" you can swap in three lines. But the two types do opposite things when
you call the same method:
double x = 100.0;
double y = x + 37.0; // computes 137.0 right now, on the spot
ADouble x = rec.input("S0", 100.0);
ADouble y = x.add(37.0); // appends an ADD node to the tape, returns a
// handle to it — no arithmetic happens yet
ADouble.add, straight from the source, doesn't touch a number at all — it
asks the recorder for a new node and wraps the result. The overload accepting a
plain double first makes that value a constant node:
public ADouble add(double value) {
return add(recorder.constant(value));
}
The same pattern is used by sub(double), mul(double), and div(double).
The double is not calculated eagerly and then forgotten; it becomes part of
the graph that the recorder owns.
sub, mul, div, exp, log, sqrt, abs, max, and min all follow
the same shape — every one of them is binary(...) or unary(...) under the
hood, appending exactly one node. The important part is that the surrounding
valuation code still reads like ordinary numerical code.
Try it yourself
Open ADouble.java
and trace the tiny portfolio example in the runnable cell from left to right.
Before running it, predict the portfolio value. Then change one of the
weights or input values and predict the new result. Finally, find the
add(double) overload. It calls recorder.constant(value) first, so
stock.mul(2.0) still belongs to the recorded graph rather than becoming an
eager Java calculation.
▶️ Run it
The example is deliberately small, but it is finance-shaped: two market
inputs become a weighted portfolio value. The recorder captures the formula once, including the fixed weights 2.0 and
3.0; the replay supplies the stock and bond values for this run without
rebuilding the valuation code. Try the example
below and check the result:
Chapter 1.2 is where a handful of these turn into a whole tape you can replay for real, and 1.3 is where a full pricer runs in the same editor.
⚠️ What this doesn't do
This page is the whole ADouble type, and it's small on purpose: eleven
arithmetic operations (add, sub, mul, div, neg, exp, log,
sqrt, abs, max, min — a few doubled up to also take a plain
double), no trig, no comparisons beyond max/min. It shows how a
sequence of these becomes an AadTape, with one simple replay to evaluate
it. Replaying the same tape across many market scenarios and turning it into
Greeks are the next two pages. And a single ADouble can't tell you
anything about a market, a payoff, or a bank; it's a building block, not a
pricer.
What's next
→ Deeper: Adjoint AD for dummies
walks the same (x+2)*x idea all the way through to a hand-computed reverse
sweep, node by node.
→ Next: Recording a tape, turning a few
ADouble operations into something you can actually replay.