dual design
This page explains the mathematics of Dual[T] and why the type has
exactly the operations and instances listed in the dual API.
The dual tutorial uses the type without this
background.
Design goal
Compute exact first derivatives of ordinary programs, written against the
Luna Flow traits, by running the same program on a different number type.
The type has to satisfy the algebraic laws its instances advertise, report
domain failures through the shared arithmetic error values, and stay
independent of any container or polynomial library.
Mathematical background
The algebra of dual numbers
Let be a commutative ring (for floating-point types, the real numbers they approximate). The dual numbers over are the quotient of the polynomial ring by the ideal generated by :
Every class has exactly one representative of degree at most one, so every
element is with unique . This is the pair
(value, tangent). Because is a quotient of a commutative
ring by an ideal, it is itself a commutative ring, and its operations are
those of polynomials followed by dropping :
Units and division
is invertible exactly when is. If is a unit, then
Conversely, if then , so is a unit. In particular is a non-zero element without an inverse, and makes it a zero divisor: is never a field, even when is. For a unit the quotient is
which is the formula Dual::div and Dual::div_checked implement.
Polynomials: where derivatives come from
For the binomial theorem and give
By linearity, every polynomial with coefficients in satisfies
where is the formal derivative. Nothing here needs limits: the identity holds in every commutative ring, including the integers.
Smooth functions and the chain rule
For a differentiable function that is not a polynomial, the package defines the extension to dual numbers by the same identity,
which is the first-order Taylor expansion with the infinitesimal ; the remainder vanishes because . Composition then produces the chain rule without any extra code. For differentiable and :
The sum, product and quotient rules are the ring operations above read with and :
The elementary rules of the package are this definition applied to known derivatives:
| Method | Tangent as computed | ||
|---|---|---|---|
sqrt | with | ||
exp | with | ||
exp2 | with | ||
ln | |||
log2 | |||
log10 | |||
sin | |||
cos | |||
tan |
The value projection is a homomorphism
The map , , preserves
, , , and :
,
and likewise for the others. Every elementary rule also has by definition. Hence the value of any computation on dual
numbers is exactly the result of the same computation on the values: adding
tangents never changes the primal result, not even its rounding.
Design decisions
One generic type over the scalar
Problem. Derivatives are needed for Double, Float, and for scalar
types defined in other Luna Flow packages.
Options. A Double-only dual type; a generic Dual[T] whose operations
ask for the smallest trait set they use.
Choice. Dual[T] is generic, and every method carries its own bound
(Dual::mul needs only Add + Mul, Dual::exp2 needs Exponential + Logarithmic + IntegralHomomorphism + Mul). This follows the Luna Flow rule
of depending on the smallest trait composition. It also lets T itself be a
dual number, which gives higher derivatives by nesting (see the
forward design).
Ring-level instances only
Problem. Which structure traits from luna-generic may Dual[T]
implement?
Choice. Zero, One, AddMonoid, AddGroup, MulMonoid, Semiring
and Ring, each when T has the same structure. These are equational
classes: their laws are identities between terms, and identities are
inherited by the quotient . As a check, associativity of the
product rule:
Field, MulGroup and Inverse are rejected because the
units computation shows that has
no inverse: an Inverse instance would have to return a wrong value for it.
No ordering
Problem. Generic code often branches on <.
Options. Order by value only; order lexicographically by (value, tangent); no order.
Choice. No Compare instance. Ordering by value is not antisymmetric
with respect to the derived Eq ( and
would each be the other without being equal). The lexicographic order
is total but is not a ring order: it makes , and an ordered
ring requires , while . Code that branches compares x.value() explicitly, which
also makes visible that the derivative is the derivative of the branch taken.
An unchecked Div despite not being a field
Problem. Ordinary formulas use /, and MoonBit’s / operator needs the
Div trait; but division is partial on .
Choice. Dual[T] implements Div with the quotient formula and no check,
inheriting T’s behaviour on a zero divisor (for Double, infinities or
NaN). This mirrors the unchecked tier of arithmetic, where Sqrt and
friends follow the IEEE semantics of the scalar. The checked tier is
DivChecked, implemented on top of T’s own DivChecked. Div alone is
not a structure claim; the type still does not implement Field.
Checked forms reuse arithmetic
Problem. Division by zero and square roots of negative numbers must be reportable as data.
Options. A dedicated autodiff error type; the arithmetic traits and
error values.
Choice. Dual[T] implements DivChecked and SqrtChecked from
Luna-Flow/arithmetic and returns its ArithmeticError, so callers handle
dual and scalar failures with one vocabulary. Both the value and the tangent
computation are checked, and the first failure is returned. Only division
and square root have checked forms, because those are the checked traits
arithmetic defines; logarithms and trigonometric functions follow the
unchecked semantics of T.
Constants through the integers
Problem. The rules for sqrt, exp2, log2 and log10 need the
constants and in T.
Options. Require a conversion from Double; build them as one + one;
use the canonical map from the integers.
Choice. IntegralHomomorphism::from_integral(2) and from_integral(10).
The map is the unique ring homomorphism, so it names the
right constant in every ring, and small integers such as and are
exact in every numeric instance. A Double conversion would not exist for
exact types, and repeated addition costs more for .
Reuse the computed value
exp and exp2 compute once and form the tangent from ,
because (up to the factor ). sqrt likewise divides by the
computed root. This saves one elementary evaluation per call and makes the
tangent consistent with the returned value.
The derivative of tan
Two formulas give : , which reuses the value, and , which needs one more cosine. The package uses . Both are mathematically equal; the chosen form is undefined at exactly the same points as itself.
Explicit method promotion
With MoonBit 0.10, trait instances no longer create methods implicitly.
src/dual/extends.mbt promotes the
arithmetic operators, equal, zero, one and div_checked, and keeps
not_equal, to_repr, from_nat, from_integral, pi, e and tau as
hidden deprecated forms for existing callers (see
Deprecated).
Correctness and invariants
Derivatives of whole programs
Let a program compute by a finite sequence of steps, each a ring
operation, a division or one of the elementary functions of the table. Run
it on (Dual::variable(x)) with every other input a
constant. Then every intermediate is represented as .
Proof by induction on the steps. Inputs: the variable is and ; a constant is and .
Step: if the operands are and , the
sum, product and quotient formulas give by the rules derived above, and an elementary function gives
by the chain
rule. The last intermediate is , so tangent is .
The same argument with input tangent gives , and with several
inputs seeded by a vector it gives the directional derivative (see the linalg design). The derivative is that
of the program, not of a mathematical function it approximates: a branch on
value() differentiates the branch taken.
Rounding error
Write with for each floating-point operation ( for Double), and use
the standard lemma that a product of factors
equals with .11 N. J. Higham, Accuracy and Stability of Numerical Algorithms,
2nd ed., SIAM, 2002, Lemma 3.1.
Product tangent. Dual::mul computes :
Quotient tangent. Dual::div computes , five roundings with one of them in the
denominator:
Both bounds have the shape of the error of evaluating the derivative
formula directly; the relative error is only large when and cancel,
which is the conditioning of the derivative itself. They assume that no
intermediate overflows or underflows; in particular underflows long
before does (see the warning on Dual::div_checked). For an elementary
rule the tangent error is the error of T’s implementation of
(for example of cos in sin) plus at most two more roundings.
Comparison with finite differences
The forward difference has two error sources. Taylor’s theorem gives the truncation error, and evaluating in floating point with error at most adds a cancellation error:
Setting the derivative of the right-hand side with respect to to zero,
so even the best step loses about half of the significant digits
( for Double). The central difference
has truncation error and reaches
about . Dual numbers have no truncation term at
all: by the induction above they compute the exact derivative of the
program, and the only error is rounding with the bounds of the previous
section.22 A. Griewank and A. Walther, Evaluating Derivatives, 2nd ed.,
SIAM, 2008, chapters 2–3, treat forward mode and its error analysis in full. The test suite and the
dual tutorial compare both methods.
Cost
Addition, subtraction and negation cost two operations of T instead of
one; multiplication costs three multiplications and one addition; division
costs three multiplications, one subtraction and two divisions; an
elementary function costs the function plus its derivative factor. A program
therefore runs at most a small constant factor (about three to four) slower
on Dual[T] than on T, and needs a constant factor more memory.
Laws to keep
x.value()of any result equals the same computation on values (the projection is a homomorphism).- Constants,
zero(),one(),from_integral,from_natand theConstantsinstance have tangent zero. - Instances stop at
Ring; noField,MulGroup,InverseorCompare. - The product rule assumes commutative multiplication of
T, which every numeric instance in Luna Flow has.
Alternatives rejected
- Finite differences. Simple, but they lose half the digits or more, as derived above, and need a step size per problem.
- Symbolic differentiation. Building derivative expressions needs a term language and simplification, and expressions can grow quickly; that belongs to a CAS layer, not to a scalar type.
- Reverse mode. Better for gradients of many inputs, but it needs a tape or closures that record the computation; it is not implemented.
- Storing a vector of tangents. One pass would give a whole gradient, at
the cost of a vector-valued type and allocation on every operation. The
scalar tangent keeps
Dual[T]a plain two-field value; vector tangents are future work.
Boundaries
- First derivatives only. Higher derivatives come from nesting
Dual[Dual[T]], which costs per order; there is no truncated Taylor (jet) type. - No reverse mode and no symbolic differentiation.
- No
Field,MulGroup,Inverseor order instance, and noShow. - Only
div_checkedandsqrt_checkedcheck their domain. Logarithms, trigonometric functions and the unchecked operators followT. - No validated enclosures: the rounding bounds above are a priori, not
computed at run time. Interval or ball scalars as
Tare neither tested nor documented by this repository.