Skip to main content

PyPI Python 3.11+ License

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.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for ddx-ad 1.3.3
File
ddx_ad-1.3.3-cp314-cp314-win_amd64.whl CPython 3.14 CPython 3.14 Windows x86-64 Details
ddx_ad-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl CPython 3.14 CPython 3.14 Linux glibc 2.27+ x86-64, Linux glibc 2.28+ x86-64 Details
ddx_ad-1.3.3-cp313-cp313-win_amd64.whl CPython 3.13 CPython 3.13 Windows x86-64 Details
ddx_ad-1.3.3-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.3-cp312-cp312-win_amd64.whl CPython 3.12 CPython 3.12 Windows x86-64 Details
ddx_ad-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl CPython 3.12 CPython 3.12 Linux glibc 2.28+ x86-64, Linux glibc 2.27+ x86-64 Details
ddx_ad-1.3.3-cp311-cp311-win_amd64.whl CPython 3.11 CPython 3.11 Windows x86-64 Details
ddx_ad-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl CPython 3.11 CPython 3.11 Linux glibc 2.28+ x86-64, Linux glibc 2.27+ x86-64 Details

Total release size: 92.9 MB

Release files / ddx_ad-1.3.3-cp314-cp314-win_amd64.whl

Download URL ddx_ad-1.3.3-cp314-cp314-win_amd64.whl
Size 804.5 kB
Tags CPython 3.14 Windows x86-64
SHA-256 checksum
How to use checksums
ffafd1f9034aaa3774a96c2080f2cd6fd250e52a3a3a65c3d59dcbe31e6a3e8b
BLAKE2b-256 checksum
How to use checksums
e92ca8cf575d928b3c251fd82c2accacfe4bacb9daf2495dbfc81e874ef01d6c
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 14, 2026.

Transparency log

Release files / ddx_ad-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl

Download URL ddx_ad-1.3.3-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
2dc1a958478b80d9042b05421b414022280814f4984ff255565f05b742fc6867
BLAKE2b-256 checksum
How to use checksums
689e8e99b4fbf44ecfdade26ab8a94280384204d3a69cbf0824d8864a3e7e297
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 14, 2026.

Transparency log

Release files / ddx_ad-1.3.3-cp313-cp313-win_amd64.whl

Download URL ddx_ad-1.3.3-cp313-cp313-win_amd64.whl
Size 780.2 kB
Tags CPython 3.13 Windows x86-64
SHA-256 checksum
How to use checksums
35f09ead88ed2eb1bfafbe4239356e0cd0d37866697362fdb2f90f2d21e2b381
BLAKE2b-256 checksum
How to use checksums
6b56956dfddae9027a1b165eee9680b73850b9dd3aaaa4486612de70e6d904f7
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 14, 2026.

Transparency log

Release files / ddx_ad-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl

Download URL ddx_ad-1.3.3-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
2905181da60934de1ddf1d2a3d16744df14fb175494fc0dbabf8e71bb1c810f0
BLAKE2b-256 checksum
How to use checksums
e3d784767bb276cd6bfe9f134bf7d160576f5a0b81afd04a1ed0b5c1878ead46
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 14, 2026.

Transparency log

Release files / ddx_ad-1.3.3-cp312-cp312-win_amd64.whl

Download URL ddx_ad-1.3.3-cp312-cp312-win_amd64.whl
Size 780.2 kB
Tags CPython 3.12 Windows x86-64
SHA-256 checksum
How to use checksums
a7b684a549a78a85714c5aa0bb29844669df410d8e159c254d4adf349d97ab73
BLAKE2b-256 checksum
How to use checksums
27056b79b2c304438af30d2eca2ed6080ce90efb8137db83fa4921c1d5457fe7
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 14, 2026.

Transparency log

Release files / ddx_ad-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl

Download URL ddx_ad-1.3.3-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
69dbdaeccce407715359469e00765892c81f9abddc76cb39ee2ef93f8672ecb7
BLAKE2b-256 checksum
How to use checksums
a1202cef8d1bde27b8dd08189c778e060bb7140d804ce3cda03ed600b60de154
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 14, 2026.

Transparency log

Release files / ddx_ad-1.3.3-cp311-cp311-win_amd64.whl

Download URL ddx_ad-1.3.3-cp311-cp311-win_amd64.whl
Size 780.1 kB
Tags CPython 3.11 Windows x86-64
SHA-256 checksum
How to use checksums
117bbc7a32b7a548e8a23bc21181317dcef44a1f8906be2b0659bda4be6d8a1f
BLAKE2b-256 checksum
How to use checksums
48dbe5055d8a93ea4c777802be6fa7fc7e066d30dc2ebb0d05c9e2949530c9c3
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 14, 2026.

Transparency log

Release files / ddx_ad-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl

Download URL ddx_ad-1.3.3-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
48b1096814822c7bdaa63fce10de61a6a991ccbe2a79fefe8f2efa5966decdcd
BLAKE2b-256 checksum
How to use checksums
365798319b6d246f00c7a3a07134606e07d4aac0cfb27a773b42e22695d6288d
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 14, 2026.

Transparency log

Release history Release notifications | RSS feed

1.3.6

8 release files

1.3.5

8 release files

1.3.4

8 release files

This release

1.3.3 This release

8 release files

1.3.2

8 release files

1.3.1

8 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page