ddx
Differentiate expressions built while the program runs. Write a model over named symbols — terms looped over a data file, a model read from configuration, an expression a user typed, a loss that switches shape on a comparison — and ask it for values, gradients, Jacobians and Hessians. One point at a time or a NumPy batch of thousands in a single call, interpreted or compiled to machine code through LLVM.
pip install ddx-ad
The distribution is ddx-ad and the import is ddx; pip install ddx is an
unrelated project. There is one wheel per platform and CPython version
(3.11–3.14), and a wheel needs nothing installed beside it — LLVM is inside
the library.
| Wheel | JIT | OpenCL |
|---|---|---|
| Linux x86_64 (glibc 2.28+) | yes | no |
| Windows x64 | no — calls interpret | no |
There is no macOS wheel; elsewhere, pip install . from a checkout of the
repository builds from source.
ddx.has_jit says whether the copy you have was built with the LLVM backend,
and ddx.has_opencl whether it was built with the OpenCL one. Everything below
works either way; without them, calls interpret.
Models
equation takes a model — a callable of no arguments returning one expression,
or a tuple of them for a system — as a call or a decorator:
import ddx
@ddx.equation
def f():
x = ddx.var("x")
y = ddx.var("y")
return ddx.exp(x) * ddx.sin(y)
Symbols are named inside the model with ddx.var(name) and order
alphabetically. f.symbols lists them, f.arity counts them, f.outputs
counts the model's outputs. A bare number mixes into an expression without
wrapping, so an accumulator can start at 0.0:
@ddx.equation
def fit():
a, b = ddx.var("a"), ddx.var("b")
loss = 0.0
for t, y in data:
loss += (y - a * ddx.exp(-b * t)) ** 2
return loss
| Kind | Available |
|---|---|
| Arithmetic | + - * / ** unary - abs() |
| Unary | sin cos tan exp log log10 sqrt cbrt abs sign asin acos atan sinh cosh tanh asinh acosh atanh erf |
| Binary | pow atan2 hypot max min |
| Comparison | < <= > >= == != — each answers 1.0 or 0.0 |
| Conditional | select(cond, if_true, if_false) |
add, mul, div and neg are the operators spelled as functions.
Where a rule has a choice to make, it is made the same way everywhere: abs
differentiates to 0 at zero and sign to 0 throughout, max and min
give each side half at a tie, and pow(a, b) at a == 0 answers the zeros its
constant arms say — a⁰ is 1 and 0ᵇ is 0.
Choosing between two expressions
A comparison is an expression, not a bool, so a model cannot branch on one
with if. select is the conditional, and it is not a branch: both arms are
evaluated and the condition picks one, so a batch keeps every point on the
same path.
@ddx.equation
def capped():
x = ddx.var("x")
return ddx.select(x < 1.0, x * x, 2.0 * x - 1.0) # C¹ at x = 1
The derivative is the taken arm's, and the condition is never
differentiated, so the symbols it tests get no partial through it. A
condition is any nonzero value; a comparison against NaN is false, while a
NaN used directly as a condition is true. A range test is
(a < b) * (b < c).
Written down instead
A model can be a string, or a list of strings for a system:
g = ddx.equation("exp(x) * sin(y)")
sys = ddx.equation(["x*x + y*y - 4", "x*y - 1"])
Free identifiers become the symbols, still ordered alphabetically. The grammar
is Python's arithmetic: + - * /, ** for exponentiation (^ is not an
operator), parentheses, unary signs, decimals and exponent notation, and the
functions in the table above. Comparisons bind looser than arithmetic, so
select(x*x < y + 1, x, y) needs no parentheses, and there is one
comparison per expression: a < b < c is refused rather than read. A string
that does not parse raises ddx.Error with bad_syntax, unknown_function or
wrong_argument_count.
Points
A point is a sequence, or a dict keyed by symbol name. A 2-D array is a
batch — shape (symbols, points), one row per symbol:
f.jacobian([2.0, 3.0]) # positional, alphabetical
f.jacobian({"x": 2.0, "y": 3.0}) # by name
f.jacobian(np.array([[2.0], [3.0]])) # a batch of one
f.jacobian(np.array([[2.0, 2.5], [3.0, 3.5]])) # a batch of two
Values and derivatives
Each call returns everything up to and including what it names, so a gradient never costs a second evaluation:
| Call | Returns |
|---|---|
evaluate(x) |
f |
jacobian(x) |
(f, J) |
gradient(x) |
J alone, from a graph that does not compute f |
hessian(x) |
(f, J, H) |
jvp(v, x) |
(f, J·v) — the directional derivative of f along v |
vjp(w, x) |
(f, wᵀJ) — the gradient of w · f |
hvp(v, x) |
(f, ∇f, H·v) — the directional derivative of ∇f along v |
value, gradient = f.jacobian([2.0, 3.0])
value, gradient, hessian = f.hessian([2.0, 3.0])
hessian.shape # (2, 2)
value, gradient, hv = f.hvp([1.0, 0.0], [2.0, 3.0])
hv.shape # (2,) — H·v, without forming H
Shapes follow the point: a single point gives a scalar f, an (n,) gradient
and an (n, n) Hessian; a batch of p appends that axis, giving (p,),
(n, p) and (n, n, p). A system prepends its output axis. The Hessian
arrives dense.
The three products never form the matrix they are named after, which is what
makes them worth having when n² storage is the problem — though not when speed
is, since one product costs about what the whole matrix does. J·v and H·v
take a direction over symbols, so it accepts the same spellings a point does —
a sequence, a dict, or a (symbols, points) batch alongside a batched point.
wᵀJ takes one weight per function, so it is positional only. hvp needs a
single-output model.
Calling in a loop
The calls above allocate their answers. buffer(x) binds the point and the
answers once and hands back a Call: write the next point into x, call it,
read the blocks back. Same arrays every time, so nothing is allocated per call.
call = f.buffer(np.array([2.0, 3.0]))
for _ in range(steps):
call() # fills call.value and call.jacobian
call.x[:] = next_point(call.jacobian)
want chooses how far it goes, and a block nobody asked for is one nobody
computes:
want |
Fills |
|---|---|
Want.VALUE |
value |
Want.JACOBIAN (default) |
value, jacobian |
Want.GRADIENT |
jacobian |
Want.HESSIAN |
value, jacobian, hessian |
Reading a block the call did not ask for raises errc.wrong_column_count, and
so does binding Want.HESSIAN on a system.
Shapes are the ones the allocating calls answer with, value included: one
output at one point is a float, and everything else is an array. The point is
bound as an array whatever was passed, so call.x is writable even when the
argument was a list.
Remembering the last call
remember=True keeps the last call: a repeated point is answered off the last
one, a point one symbol away sweeps only what that symbol reaches, and the
numbers are unchanged.
f = ddx.equation(model, remember=True)
value = f(x) # swept
grad = f.gradient(x) # its own lane
again = f(x) # nothing swept
It applies to a point at a time and not to an array of them.
Errors
ddx.Error is a RuntimeError carrying a code:
try:
f.jacobian({"z": 1.0})
except ddx.Error as e:
print(e) # "no symbol of that name"
e.code is ddx.errc.unknown_symbol # True
errc is an IntEnum, so it compares and formats as its number — e.code.name
is the spelling.
errc |
Means |
|---|---|
wrong_arity |
the point does not supply one value per symbol |
wrong_direction |
a direction does not supply one value per symbol, or a covector one per function |
short_point |
a named point leaves a symbol unreached |
unknown_symbol |
a named point uses a name the equation does not have |
wrong_column_count |
a batch block has the wrong number of columns |
no_arena |
a symbol was named outside a model |
no_graph |
the model is a bare number, naming no function |
bad_syntax |
a string the grammar does not accept |
unknown_function |
a string calls a function that does not exist |
wrong_argument_count |
a string calls one with the wrong arity |
archive_io |
the file could not be read or written |
bad_archive |
not a ddx file, or a format this build does not read |
archive_corrupt |
the file's checksum or structure does not hold |
archive_mismatch |
the file loads, but does not describe this equation |
no_device, device_compile, device_launch |
no OpenCL device answered, its compiler refused the kernel, or a launch failed |
jit_target, jit_module, jit_object, jit_verify, jit_lookup |
the compiler could not produce or link a kernel |
Compiling
Options is a frozen pydantic model, validated on the way in. eq.options
reads and assigns it; eq.compile() sets backend=COMPILE, waits for the
kernel, and returns the equation, so a configure-and-use reads in one line:
f.compile(points=batch.shape[1]).jacobian(batch)
f.uses_kernel # True, once it has landed
f.options = ddx.Options(backend=ddx.Backend.INTERPRET) # discards the kernel
compile() blocks by construction — it is wait_for_kernel() with the options
set first. Assigning options does not: calls interpret until the kernel lands
and switch over when it does.
Backend |
Calls |
|---|---|
INTERPRET |
walk the graph |
COMPILE |
start the compile at once, interpret until it lands |
ADAPT |
compile a lane once it has been asked for warm_points batch points |
DEVICE |
build the graph for the OpenCL device device names |
points is the batch you intend to hand one call, stated because the kernel is
built before any call exists to infer it from. It decides the lane width and
nothing else: a call carrying some other number is answered correctly, just not
by the kernel that number would have built. cache_dir keeps compiled objects
between runs, so a second run links instead of compiling — roughly three orders
of magnitude quicker; an empty string disables it.
f.compile(backend=ddx.Backend.DEVICE) builds for an OpenCL device instead. An
empty Options.device takes the first GPU with double precision; anything else
is matched, ignoring case, against the platform and device name — "NVIDIA",
"gfx1035", "Intel". f.device_status names the device, is None under
any other backend, and raises ddx.Error when no device answers. No wheel
carries the device backend; a source build asks for it with
-C cmake.define.DDX_BUILD_OPENCL=ON. Where it is missing, ddx.has_opencl is
False, DEVICE interprets, and device_status raises errc.no_device.
Saving and loading
eq.save("f.ddx")
same = ddx.load("f.ddx") # no model runs, nothing is rebuilt
@ddx.equation # or pair a model with a file, as a cache
def model() -> ddx.Expression:
x, y = ddx.var("x"), ddx.var("y")
return ddx.exp(x) * y
cached = ddx.equation(model, cache="f.ddx")
cached.loaded # False the first run, True after
A string model caches the same way: ddx.equation("exp(x) * y", cache="f.ddx").
save, load and verify raise ddx.Error rather than answering False:
unreadable, unloadable and "a different equation" are three different errc
values, and only the code says which.
Reference
| Member | Is |
|---|---|
arity, outputs, symbols |
properties — symbol count, output count, canonical names |
evaluate(x), __call__(x) |
f at the point or batch |
jacobian(x) |
(f, J) |
gradient(x) |
J alone |
jvp(v, x), vjp(w, x), hvp(v, x) |
(f, J·v), (f, wᵀJ), (f, ∇f, H·v) |
hessian(x) |
(f, J, H), dense |
options |
property — read or assign an Options |
compile(**fields) |
set Options, block for the kernel, return self |
uses_kernel, wait_for_kernel(*, want) |
whether a call runs compiled code, and blocking for it — for the Jacobian lane unless want names another |
device_status |
property — under DEVICE, the device answering; None otherwise; raises when none answers |
hessian_colors |
groups in the Hessian's compression |
buffer(x, *, want) |
a Call bound to its buffers, for a loop |
to_dot(*, all=False) |
the expression in Graphviz form; all=True draws the pruned nodes too |
nodes(*, want) |
how many nodes a call for want evaluates |
save(path), verify(path) |
write this equation; raise unless path holds it |
loaded |
property — whether this equation was read rather than built |
ddx.load(path) reads one, and ddx.equation(model, cache=path) builds or
reads as the file allows.
License
Boost Software License 1.0. The wheels bundle LLVM (Apache 2.0 with LLVM exception), zlib, zstd and pybind11; THIRD-PARTY-NOTICES.txt holds each licence and ships in the wheel.
Metadata
Release files for ddx-ad 1.3.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| ddx_ad-1.3.2-cp314-cp314-win_amd64.whl | CPython 3.14 | CPython 3.14 | Windows x86-64 | Details |
| ddx_ad-1.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl | CPython 3.14 | CPython 3.14 | Linux glibc 2.28+ x86-64, Linux glibc 2.27+ x86-64 | Details |
| ddx_ad-1.3.2-cp313-cp313-win_amd64.whl | CPython 3.13 | CPython 3.13 | Windows x86-64 | Details |
| ddx_ad-1.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl | CPython 3.13 | CPython 3.13 | Linux glibc 2.28+ x86-64, Linux glibc 2.27+ x86-64 | Details |
| ddx_ad-1.3.2-cp312-cp312-win_amd64.whl | CPython 3.12 | CPython 3.12 | Windows x86-64 | Details |
| ddx_ad-1.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl | CPython 3.12 | CPython 3.12 | Linux glibc 2.27+ x86-64, Linux glibc 2.28+ x86-64 | Details |
| ddx_ad-1.3.2-cp311-cp311-win_amd64.whl | CPython 3.11 | CPython 3.11 | Windows x86-64 | Details |
| ddx_ad-1.3.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl | CPython 3.11 | CPython 3.11 | Linux glibc 2.27+ x86-64, Linux glibc 2.28+ x86-64 | Details |
Total release size: 92.9 MB
Release files / ddx_ad-1.3.2-cp314-cp314-win_amd64.whl
| Download URL | ddx_ad-1.3.2-cp314-cp314-win_amd64.whl |
|---|---|
| Size | 804.5 kB |
| Tags | CPython 3.14 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
6375dcf445fd2446b00faad29d4845fed41416de69e8f7edfc9b498cd6595223
|
|
BLAKE2b-256 checksum How to use checksums |
2ca0036b79ae76135dfe3defb834a2031d5e16149d04c74d7e0118e63ab376e5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 11, 2026.
Transparency logRelease files / ddx_ad-1.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
| Download URL | ddx_ad-1.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 22.4 MB |
| Tags | CPython 3.14 Linux glibc 2.27+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
02242c4381fa48e3de2d7ffd1e3326ec7a13ade4f94dda3583c1a3c01afd7164
|
|
BLAKE2b-256 checksum How to use checksums |
c538d178161148307087bf56974965ef9bca749d444ec0e55c0358f58cd593c4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 11, 2026.
Transparency logRelease files / ddx_ad-1.3.2-cp313-cp313-win_amd64.whl
| Download URL | ddx_ad-1.3.2-cp313-cp313-win_amd64.whl |
|---|---|
| Size | 780.2 kB |
| Tags | CPython 3.13 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
6f2fac3bfbbdb5ff657a72fad24f1f3b637d27c3f6eeeda5c9e4e7ab25c75c44
|
|
BLAKE2b-256 checksum How to use checksums |
899dd002b83cbd100d0f887a312843ccaea5cfccdb1277795dc559251ba1b275
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 11, 2026.
Transparency logRelease files / ddx_ad-1.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
| Download URL | ddx_ad-1.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 22.4 MB |
| Tags | CPython 3.13 Linux glibc 2.27+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
ff0f4571f0099b0a42ba457b9637bc344a2c1abbe41caae1cc7a3736f633b237
|
|
BLAKE2b-256 checksum How to use checksums |
55f1b41d2f207a76f87bfd725dfe147820b0d40a593b71e579e209043ea4a5f6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 11, 2026.
Transparency logRelease files / ddx_ad-1.3.2-cp312-cp312-win_amd64.whl
| Download URL | ddx_ad-1.3.2-cp312-cp312-win_amd64.whl |
|---|---|
| Size | 780.2 kB |
| Tags | CPython 3.12 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
49a33f71d0b535bf894e30175828ee9845fb07547c383f6d614bd40ccdfc27c0
|
|
BLAKE2b-256 checksum How to use checksums |
1d43c0eab013083cc7a96db14865e8c8f946600e6061060d9ed580a21fa38699
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 11, 2026.
Transparency logRelease files / ddx_ad-1.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
| Download URL | ddx_ad-1.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 22.4 MB |
| Tags | CPython 3.12 Linux glibc 2.27+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
2e1e9d2ef7c9d9a592d3ac627b3f19e9905c2650de412bd6b32971df86b1d0f6
|
|
BLAKE2b-256 checksum How to use checksums |
0239fd93af1be93c9fc2e400221eabd9c6578103d34427f8da1a7256477bd294
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 11, 2026.
Transparency logRelease files / ddx_ad-1.3.2-cp311-cp311-win_amd64.whl
| Download URL | ddx_ad-1.3.2-cp311-cp311-win_amd64.whl |
|---|---|
| Size | 780.1 kB |
| Tags | CPython 3.11 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
5309d9de8c7338a21f518011f0301f1dc6caa6ef801629dfe93d789d80556da5
|
|
BLAKE2b-256 checksum How to use checksums |
6f1735fa27c31cdb2f392e980eaeb28c69824e21640fcc0d37e1e8c5a52467e2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 11, 2026.
Transparency logRelease files / ddx_ad-1.3.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
| Download URL | ddx_ad-1.3.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 22.4 MB |
| Tags | CPython 3.11 Linux glibc 2.27+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
1f824b788dd85d38e6a2bb084cf71319fa4562d877dfe5868e44b06ec006f510
|
|
BLAKE2b-256 checksum How to use checksums |
1fdd04e7bbb3a8b3ed1320b8a0c35c88f3c87d411191625d50b717e5bf771f96
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 11, 2026.
Transparency log