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

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.6
File
ddx_ad-1.3.6-cp314-cp314-win_amd64.whl CPython 3.14 CPython 3.14 Windows x86-64 Details
ddx_ad-1.3.6-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.6-cp313-cp313-win_amd64.whl CPython 3.13 CPython 3.13 Windows x86-64 Details
ddx_ad-1.3.6-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.6-cp312-cp312-win_amd64.whl CPython 3.12 CPython 3.12 Windows x86-64 Details
ddx_ad-1.3.6-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.6-cp311-cp311-win_amd64.whl CPython 3.11 CPython 3.11 Windows x86-64 Details
ddx_ad-1.3.6-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.6-cp314-cp314-win_amd64.whl

Download URL ddx_ad-1.3.6-cp314-cp314-win_amd64.whl
Size 804.6 kB
Tags CPython 3.14 Windows x86-64
SHA-256 checksum
How to use checksums
e9789f63c0ec323ebb9f4ccad35f521e7b9034f2b004c0e6ffd43aca057dc431
BLAKE2b-256 checksum
How to use checksums
d7845c0f040e110a2f3661ee7ad7e5f82f8c595ce1ce5e3841cdfcd48b05d254
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 Oct 5, 2026.

Transparency log

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

Download URL ddx_ad-1.3.6-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
cfe9dddcafe49de9c7aa96bda19c892c33a261948b56c00b1f29d02b8eee6809
BLAKE2b-256 checksum
How to use checksums
f797eca3ce8acfc34a4439166ca9e34d48fbe201123afce1f5563cc160e350ce
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 Oct 5, 2026.

Transparency log

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

Download URL ddx_ad-1.3.6-cp313-cp313-win_amd64.whl
Size 780.2 kB
Tags CPython 3.13 Windows x86-64
SHA-256 checksum
How to use checksums
bf5948bcf5b388122856a5ed545af23738f745c31bc285bcf7b855cef5bc3c34
BLAKE2b-256 checksum
How to use checksums
2a82ae8389f790d103d0690fec2608025f6073322ee09c05d87592fcb4cfe86c
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 Oct 5, 2026.

Transparency log

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

Download URL ddx_ad-1.3.6-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
39b23091b71c10cc68b903a944d6bcf4bd02ec68e58ac363d95809870ee6d8ee
BLAKE2b-256 checksum
How to use checksums
f8c22bbb9fbb45e5e59f363c997114006e141af7fe26682915df14b18a69ea35
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 Oct 5, 2026.

Transparency log

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

Download URL ddx_ad-1.3.6-cp312-cp312-win_amd64.whl
Size 780.2 kB
Tags CPython 3.12 Windows x86-64
SHA-256 checksum
How to use checksums
90132780bd2e47afa3f3aff18ad6ad302f956afcd507e29f746fc13b47c6347e
BLAKE2b-256 checksum
How to use checksums
52eac24903667f64be3f06491e3cd941978c2ed948215a1b255a7e9b3a4c2c3f
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 Oct 5, 2026.

Transparency log

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

Download URL ddx_ad-1.3.6-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
718e00983a9a4f34a66bd50dbbba6a4f5ffeddad1965a4cb75347d4d27a0c52b
BLAKE2b-256 checksum
How to use checksums
b5ba3498c24625a5c0cb22d11000ec48f4110ae2515cb5a944a1dd199223a765
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 Oct 5, 2026.

Transparency log

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

Download URL ddx_ad-1.3.6-cp311-cp311-win_amd64.whl
Size 780.1 kB
Tags CPython 3.11 Windows x86-64
SHA-256 checksum
How to use checksums
1fe2a3a6f57cd9479c3508036e538b3eb6e93ba7e471999c8db22043c98b26ee
BLAKE2b-256 checksum
How to use checksums
0fcdf638a26ce5ba0fc6c2bf560d8b4c3266462b169ee947a9dbccf1bb855778
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 Oct 5, 2026.

Transparency log

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

Download URL ddx_ad-1.3.6-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
ef74022d1d770c844eb5a9acb0d3fe70c34c44fb82db56b252329c2dc9b97cf2
BLAKE2b-256 checksum
How to use checksums
26b1f3266c6e142986b6333878fe1f1ec2bf34499b37b89761d395d972efc616
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 Oct 5, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.3.6 This release

8 release files

1.3.5

8 release files

1.3.4

8 release files

1.3.3

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