← quantChapter 1 · Your first pricer5 min read

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

A double computes immediately and forgets everything except the final value. An ADouble records each operation as a node on a tape instead, keeping only a handle to that node — the value doesn't exist until the tape is replayed, and it can be replayed forward or backward, once or millions of times.

Did you know?

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:

Java · compile and run in this browser

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.


Questions or corrections? open an issue