Skip to main content

Dual Number Library

Project description

dual-number

Exact, algebraic automatic differentiation via dual numbers for Python.

Python License: MIT Version


What Are Dual Numbers?

Dual numbers extend the real numbers by adjoining a nilpotent unit ε (epsilon), satisfying the single defining property:

ε² = 0,   ε ≠ 0

Every dual number takes the form a + bε, where a and b are real numbers. Arithmetic follows naturally from this rule. The key identity that makes dual numbers powerful is:

f(a + bε) = f(a) + b·f'(a)ε

for any differentiable function f. This means evaluating a function on a dual number simultaneously computes the function value (real part) and the derivative (dual part) exactly, with no approximation.


Installation

Clone the repository and install in editable mode:

pip install dual-number

Then in your code:

from dual-number import Dual, Epsilon, derivative, gradient, jacobian

Quick Start

from dual-number import Dual, Epsilon, derivative, gradient, jacobian
 
# --- Basic construction ---
x = Dual(3, 1)       # 3 + 1ε  (seed for differentiation at x=3)
y = Dual(4, 2)       # 4 + 2ε
 
# --- Arithmetic ---
print(x + y)         # Dual(7, 3)
print(x * y)         # Dual(12, 10)
print(x ** 2)        # Dual(9, 6)   ← real: 3², dual: 2·3 = f'(3) for f=x²
 
# --- Derivative of f(x) = x³ at x = 2 ---
f = lambda x: x ** 3
result = f(Dual(2, 1))
print(result.real)   # 8.0   ← f(2)
print(result.dual)   # 12.0  ← f'(2) = 3·2² = 12
 
# --- Using the calculus helpers ---
print(derivative(lambda x: x**3, 2.0))          # 12.0
print(gradient(lambda x, y: x**2 + x*y, 3, 4)) # [10.0, 3.0]

The Dual Class

Construction

Dual accepts 0, 1, or 2 arguments:

Dual()             # 0 + 0ε  (zero)
Dual(3)            # 3 + 0ε  (pure real)
Dual(3, 4)         # 3 + 4ε  (full dual)
Dual(other_dual)   # copy constructor
Dual([3, 4])       # from any iterable of two real numbers

Arithmetic

All standard operators are supported between Dual and Dual, and between Dual and int/float:

Expression Real Part Dual Part
X + Y a + c b + d
X - Y a - c b - d
X * Y ac ad + bc
X / Y a/c (bc − ad) / c²
X ** Y aᶜ bc·aᶜ⁻¹ + d·aᶜ·ln(a)
X // Y ⌊a/c⌋ 0
X % Y a mod c b − d·⌊a/c⌋
x = Dual(3, 1)
y = Dual(2, 1)
 
x + y    # Dual(5, 2)
x * y    # Dual(6, 5)
x / y    # Dual(1.5, -0.25)
x ** 2   # Dual(9, 6)
x // y   # Dual(1, 0)
x % y    # Dual(1, -1)

Reflected operators (__radd__, __rmul__, etc.) are fully supported, so 3 + Dual(1, 2) works as expected.

Mathematical Functions

x = Dual(1, 1)
 
x.exp()            # (e¹, e¹)        ← d/dx eˣ = eˣ
x.log()            # (0, 1)          ← d/dx ln(x) = 1/x
x.log(base=10)     # (0, 1/ln10)
x.gamma()          # (1, -γ)
x.erf()            # (~0.843, ~0.415)
abs(x)             # (|a|, b·sign(a))

Trigonometric Functions

x = Dual(1, 1)
 
x.sin()    # (sin(1), cos(1))
x.cos()    # (cos(1), -sin(1))
x.tan()    # (tan(1), sec²(1))
x.sec()    # (sec(1), sec(1)·tan(1))
x.csc()    # (csc(1), -csc(1)·cot(1))
x.cot()    # (cot(1), -csc²(1))

Their inverse, hyperbolic, and hyperbolic inverses are fully supported.


The Epsilon Class

Epsilon is a convenience subclass of Dual representing the pure dual unit ε = 0 + 1ε:

from dual-number import Epsilon
 
e = Epsilon()      # Dual(0, 1)
 
# Build dual numbers compositionally
x = 3 + 4 * Epsilon()   # Dual(3, 4)
 
# Verify the nilpotency property
Epsilon() ** 2     # Dual(0, 0)  ← ε² = 0  ✓
Epsilon() ** 3     # Dual(0, 0)  ← ε³ = 0  ✓
 
# Use as a differentiation seed
f = lambda x: x ** 3
f(2 + Epsilon()).dual    # 12.0  ← f'(2) = 3·2² = 12

Automatic Differentiation

All calculus helpers live in dual.calculus and are exported from dual directly.

derivative

Computes the exact derivative of a scalar function at a point:

from dual-number import derivative
 
derivative(lambda x: x**3,   2.0)   # 12.0   ← 3x² at x=2
derivative(lambda x: x.sin(), 0.0)  # 1.0    ← cos(0)
derivative(lambda x: x.exp(), 1.0)  # 2.718  ← eˣ at x=1

gradient

Computes all partial derivatives of a scalar-valued function, one forward pass per variable:

from dual-number import gradient
 
# f(x, y) = x² + xy,  ∂f/∂x = 2x+y,  ∂f/∂y = x
gradient(lambda x, y: x**2 + x*y, 3, 4)
# → [10.0, 3.0]

jacobian

Computes the full m×n Jacobian matrix of a vector-valued function:

from dual-number import jacobian
 
# f: R³ → R³
f = lambda x, y, z: (x**2 + y + z,  x*y,  z + x)
jacobian(f, 2, 3, 4)
# → [[4, 3, 1],    ∂/∂x: 2x=4, y=3, 1
#    [1, 2, 0],    ∂/∂y: 1, x=2, 0
#    [1, 0, 1]]    ∂/∂z: 1, 0, 1

gradient and derivative are both special cases of jacobian:

derivative  →  n=1, m=1  →  a single number
gradient    →  m=1        →  a vector of length n
jacobian    →  full n×m   →  a matrix

Accuracy

Dual number differentiation is exact to float64 precision (~15–16 significant digits). It has none of the approximation error intrinsic to finite differences:

import math as M
from dual-number import derivative
 
x = 1.5
h = 1e-7
 
# f(x) = sin(x²),  f'(x) = 2x·cos(x²),  f'(1.5) = 3·cos(2.25) ≈ -1.8845208...
finite_diff = (M.sin((x+h)**2) - M.sin(x**2)) / h    # -1.8845212823581647  ← error from h
dual_result = derivative(lambda x: (x**2).sin(), 1.5) # -1.8845208681682175
exact       = M.cos(x**2) * 2 * x                     # -1.8845208681682175  ✓

The dual result matches the analytical exact value to all 16 digits. The finite difference accumulates error from both the finite step size h and floating point cancellation when subtracting two close values.


Limitations

Piecewise constant functionsfloor, ceil, round, and sign have zero derivative almost everywhere but are undefined (Dirac delta) at their discontinuity points. The library raises a ValueError there:

Dual(2.0, 1).floor()   # ✅  a=2.0 is an integer → raises ValueError
Dual(2.5, 1).floor()   # ✅  a=2.5, not an integer → Dual(2.0, 0)

Negative base with dual exponentX**Y requires ln(a), so a must be strictly positive:

Dual(-2, 1) ** Dual(3, 1)   # raises ValueError — ln(-2) undefined
Dual(-2, 1) ** 3             # ✅ scalar exponent is fine → Dual(-8, 12)

Singularities are the same as their real counterparts — log(0), tan(π/2), asin(1), etc. all raise the expected errors.

Symbolic inspection — dual numbers are numerical, not symbolic. The derivative of f cannot be printed as a formula. Use sympy if you need symbolic expressions.

Bit operations&, |, ^, <<, >> are not implemented. They have no meaningful derivative and operate only on integers, making dual number semantics undefined. Extract the real part explicitly if needed: int(x.real) & int(y.real).


License

MIT License — see LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dual_number-0.0.1.tar.gz (16.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dual_number-0.0.1-py3-none-any.whl (10.6 kB view details)

Uploaded Python 3

File details

Details for the file dual_number-0.0.1.tar.gz.

File metadata

  • Download URL: dual_number-0.0.1.tar.gz
  • Upload date:
  • Size: 16.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for dual_number-0.0.1.tar.gz
Algorithm Hash digest
SHA256 5143bebabb75fe08d17d26afa5ff7ad2c2e11b5bd207758388b37833eba3073c
MD5 f4aa651d13e279f6fc3ea732aec767d5
BLAKE2b-256 4bfd3ce7ba0a5dc1fc8fa446718a38fc0ccb078c4fcad0325f66bd01667f3f76

See more details on using hashes here.

Provenance

The following attestation bundles were made for dual_number-0.0.1.tar.gz:

Publisher: python-publish.yml on BaguetteBunny/dual-lib

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file dual_number-0.0.1-py3-none-any.whl.

File metadata

  • Download URL: dual_number-0.0.1-py3-none-any.whl
  • Upload date:
  • Size: 10.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for dual_number-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8ff7e2d820473911e0692d855921cb8154f54418c9fb78e377ddc4f061e560e7
MD5 e4971b2afd415a75d5e20bcf7a0ff9a2
BLAKE2b-256 199ce3b55f1fb7fe246f61eff57c870f4e5338237b9a3797962d01066f782266

See more details on using hashes here.

Provenance

The following attestation bundles were made for dual_number-0.0.1-py3-none-any.whl:

Publisher: python-publish.yml on BaguetteBunny/dual-lib

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page