Automatic differentiation bindings¶
These C and Python interfaces call HMath.Autodiff. They retain its checked
input domains, graph node budget, accumulation rules and derivative conventions.
Scalar expressions, compiled graphs, gradient states and symbolic tensors are
immutable Haskell values with independently owned references.
Include hmath_autodiff.h in C or C++; use from hmath import autodiff as ad in
Python. Numerical reverse-graph evaluation uses the original binary64 implementation.
Dual, primal, tangent and forward support arbitrary nesting over Double,
Float, Integer and Rational leaves. Float arithmetic stays Float; exact
leaves never pass through floating-point intermediates. Other generic leaf
instances remain pending and are not claimed by this page.
Ownership and errors¶
Each returned C handle owns one reference. hmath_ad_retain(value) adds a
reference; hmath_ad_release(value) removes one. Both accept NULL. The caller
holds an input reference throughout a call, and never races final release with
use. Every live handle reserves the Haskell runtime: final shutdown returns
HMATH_BUSY until those references are released. Expressions/graphs retain their
Haskell dependencies independently; releasing a source expression does not
invalidate a compiled graph or derivative.
Python objects provide idempotent close() and with blocks. Their destructor
is a fallback. A closed object rejects use with ValueError. Operations pin
input references during calls, including reentrant callbacks. Sharing immutable
objects across threads is supported; caller output buffers must be independent.
C arrays are borrowed for the call. Outputs must not overlap inputs or each
other. Output capacity is measured in elements, not bytes. Insufficient capacity
returns HMATH_BOUNDS_ERROR; outputs remain untouched on failure. A NULL
context disables cancellation; otherwise cancellation is checked before entry.
An in-flight Haskell operation is not asynchronously interrupted.
Original Left MathError values become HMATH_MATH_ERROR in C and HMathError
with status == 11 in Python. The error message retains the original constructor
and payload. Pure Haskell exceptions become status 6. Invalid native pointers,
wrong handle kinds, buffer capacities and allocation failures are binding errors.
No extra finite check is added to unchecked Dual arithmetic or the raw
GradientState constructor.
Scalar expressions and graphs¶
| Python | C | Contract |
|---|---|---|
variable(index) |
hmath_ad_variable |
Declare a zero-based graph input. The original compileGraph checks index validity, including negative indices. |
constant(value) |
hmath_ad_constant |
Embed a Double; non-finite literals are rejected during graph compilation, not construction. |
compile_graph(input_count, expression) |
hmath_ad_compile_graph |
Compile a reusable graph with exactly that many inputs. The C parameter named index is this input count. Original maximum is one million scalar nodes. |
evaluate_graph(graph, values) / graph.evaluate(values) |
hmath_ad_evaluate_graph |
Re-evaluate with exactly the declared number of finite inputs. |
differentiate(index, expression) |
hmath_ad_differentiate |
Construct a symbolic derivative. Opaque custom rules retain their original higher-order rejection. |
detach(expression) |
hmath_ad_unary with HMATH_AD_DETACH |
Preserve the primal while dropping reverse edges. |
stop_gradient(expression) |
hmath_ad_unary with HMATH_AD_STOP_GRADIENT |
Original stopGradient alias. |
Python Expr implements +, -, *, /, **, unary - and abs through
original Haskell instances. Scalar literals are converted to native constants.
C uses hmath_ad_binary and hmath_ad_unary with the enums in the header.
Python unary helpers are negate, absolute, sign, reciprocal, exp, log,
sqrt, sin, cos, tan, asin, acos, atan, sinh, cosh, tanh,
asinh, acosh and atanh. These also operate on Dual values where the
corresponding Haskell instance exists. Dual comparisons use the original Eq/Ord
instances: equality includes both fields, while ordering compares primal fields
only, recursively. This intentionally preserves their distinction when two
Dual values have equal primals but different tangents.
from hmath import autodiff as ad
with ad.variable(0) as x:
with ad.compile_graph(1, x * x + 1) as graph:
assert graph.evaluate([3]) == 10
assert graph.evaluate([4]) == 17
See the original expression and compilation contracts.
Reverse derivatives and products¶
Python builders are pure functions receiving a list of Expr inputs. They return
one expression for scalar objectives or a sequence for vector objectives. An
existing expression, or sequence of expressions, may be passed instead of a
builder. The C interface receives those explicit expression handles. A builder's
external Python side effects and evaluation count are not part of its mathematical
contract; build expressions only from the supplied inputs and deterministic rules.
| Python signature | C function | Returned result / C flat output layout |
|---|---|---|
value_and_gradient(function, values) |
hmath_ad_value_and_gradient |
(value, gradient) / [value, gradient...], capacity n+1. Repeated input leaves accumulate. |
gradient(function, values) |
hmath_ad_gradient |
Fresh length-n list; no ambient accumulation. |
hessian(function, values) |
hmath_ad_hessian |
n rows of n symbolic second derivatives / row-major n*n. |
jacobian(function, values) |
hmath_ad_jacobian |
m output-by-input rows / row-major m*n; empty outputs preserve shape. |
jvp(function, values, direction) |
hmath_ad_jvp |
Jacobian times input direction, length m; original direction length/finite checks. |
vjp(function, values, direction) |
hmath_ad_vjp |
Output cotangent times Jacobian, length n; direction length is m. |
hvp(function, values, direction) |
hmath_ad_hvp |
Hessian times input direction, length n. |
gradient_check(step, function, values) |
hmath_ad_gradient_check |
Maximum absolute discrepancy from central finite differences; positive finite step is required. |
complex_gradient(function, value) |
hmath_ad_complex_gradient |
Complex Wirtinger derivative; the builder receives [real, imaginary]. C input/output are two real coordinates. The original convention divides real-coordinate derivatives by two. |
differentiable_step(rate, function, expressions) |
hmath_ad_differentiable_step |
Updated symbolic expressions preserving outer derivatives. C returns a rank-one ADTensor; retrieve its owned expression references. |
Here n is input count and m is output expression count. Native result products
are checked for representable sizes before allocating/writing memory. The original
MathError values cover invalid lengths, non-finite values, invalid tolerances,
invalid graph indices and graph-budget exhaustion.
from hmath import autodiff as ad
def objective(xs):
x, y = xs
return x * x + x * y
assert ad.value_and_gradient(objective, [3, 4]) == (21, [10, 3])
assert ad.hessian(objective, [3, 4]) == [[2, 1], [1, 0]]
assert ad.hvp(objective, [3, 4], [2, -1]) == [3, 2]
See reverse differentiation, Jacobian, Hessian, and differentiable steps.
Dual forward mode¶
Dual(value, tangent, dtype=None) uses the original Dual numeric instances.
The default leaf type is Double; nested values inherit the component type.
Supported explicit choices are:
| Python dtype | Original Haskell leaf | C constructor | Available classes |
|---|---|---|---|
"float64" |
Double |
hmath_ad_dual_create |
Num, Fractional, Floating, Eq, Ord |
"float32" |
Float |
hmath_ad_dual_create_f32 |
Num, Fractional, Floating, Eq, Ord |
"integer" |
Integer |
hmath_ad_dual_create_integer |
Num, Eq, Ord |
"rational" |
Rational |
hmath_ad_dual_create_rational |
Num, Fractional, Eq, Ord |
"complex64", "complex128" |
Complex Float, Complex Double |
shared Value export + hmath_ad_dual_nested |
Num, Fractional, Floating, Eq |
"int8" … "int64", "intp" |
Signed bounded integers / native Int | shared Value export + hmath_ad_dual_nested |
Num, Eq, Ord |
"uint8" … "uint64", "uintp" |
Unsigned bounded integers / native Word | shared Value export + hmath_ad_dual_nested |
Num, Eq, Ord |
| Typed NumericValue prototype | Original Numeric.Dual towers or Quaternion Float/Double | shared Value export + hmath_ad_dual_nested |
Original leaf dictionaries only |
Unsupported class operations reject the request, such as Integer division or
Rational exp; they never silently convert to Double. dual.dtype reports the
leaf type and dual.depth reports nesting depth. Components may themselves be
Dual values of the same scalar type/depth.
Numeric literals are embedded at that depth/type with zero tangents, following the
original Num instance. Mixing already constructed incompatible depths is a
TypeError, corresponding to incompatible Haskell types.
primal(dual) / dual.primal and tangent(dual) / dual.tangent extract their
respective components. C constructs a dual with hmath_ad_dual_create and copies
[primal, tangent] through hmath_ad_values_copy for flat Dual values. For
nested values, hmath_ad_dual_nested constructs the next level and
hmath_ad_dual_parts returns two independently owned component handles.
hmath_ad_dual_depth reports nesting depth; scalar component handles have depth
zero. Flat Double/Float leaves support their compatible floating copy view.
Python unwraps standard scalar leaves to floats, complex values, exact integers
or fractions
and returns Dual objects for nested components. Integer/Rational constructors use
signed decimal strings, with separate rational numerator/denominator strings.
The compatible Integer/Rational component accessor is hmath_ad_scalar_text;
free both returned
strings with hmath_ad_string_free. Python exposes those leaves as arbitrary
precision int and fractions.Fraction, including beyond Python's decimal
conversion digit cap. Integer/Rational and other typed coefficient handles have
no implicit Double copy view. For every coefficient type,
hmath_ad_value_import converts a depth-zero component handle to a shared typed
Value; query/extract it with the shared Value API. Conversely, create a typed
shared coefficient Value, export it with hmath_ad_value_export, then pass
matching component handles to hmath_ad_dual_nested. Both conversions preserve
original scalar types and exact payloads.
A boxed hmath.numeric_types.NumericValue can select the complete original
coefficient type, including nested Numeric.Dual and Quaternion components.
Dual.element_type returns an independently owned scalar prototype; it retains
the entire coefficient witness. dtype reports primitive names or
"numeric.dual" / "numeric.quaternion" for those structured leaves. The
prototype, rather than that diagnostic string, selects a structured type.
Original Numeric.Dual has no Ord dictionary; Quaternion has Num but no
Fractional/Floating dictionary. The binding rejects those unavailable operations
without assigning new instances. AD.Dual may be nested over any supported leaf;
its Eq and available Ord continue to use the original instances. Bounded
arithmetic wraps according to the original Haskell type; constructor input
conversion rejects out-of-range Python integers.
forward(function, value, dtype=None) seeds a native dual with tangent one, applies the
builder using original Dual arithmetic, and returns (primal, derivative).
C callers compose the same sequence using the dual constructor, native operators
and component extraction. Unchecked non-finite behavior is retained.
forward_jvp(function, values, direction) returns (primals, tangents) using the
original checked vector API. Input length and finite checks precede builder
execution. C callers can first invoke hmath_ad_forward_jvp_result with zero
outputs to perform those checks, then build their dual outputs with the input
values/direction, and invoke it again to extract checked output components.
The C result layout is [primals..., tangents...] with capacity 2*m.
from hmath import autodiff as ad
assert ad.forward(lambda x: x * x * x, 3) == (27, 27)
assert ad.forward(lambda x: x+1, 16777216, dtype="float32") == (16777216, 1)
assert ad.forward(lambda x: x*x, 10**100, dtype="integer") == (10**200, 2*10**100)
with ad.Dual(3, 1) as seed:
value, derivative = ad.forward(lambda x: x*x*x, seed)
with value, derivative:
assert (derivative.primal, derivative.tangent) == (27, 18)
assert ad.forward_jvp(lambda xs: [xs[0] * xs[1]], [2, 3], [1, 2]) == ([6], [7])
See forward mode.
Explicit gradient state¶
| Python | C | Meaning |
|---|---|---|
GradientState(values) |
hmath_ad_gradient_state_create |
Original raw constructor; stores the supplied list without extra validation. |
zero_gradients(count) |
hmath_ad_zero_gradients |
Create zeros; negative count retains the original InvalidArgument. |
state.accumulated_gradient |
hmath_ad_values_copy |
Copy the retained gradient list. |
accumulate_gradients(state, values) |
hmath_ad_accumulate_gradients |
Return a new state with a checked, same-length finite sum. Original state is unchanged. |
reset_gradients(state) |
hmath_ad_reset_gradients |
New zeros of the original length. |
gradient_hook(function, state) |
hmath_ad_gradient_hook_result |
Apply a checked transformation; changed length and non-finite output are rejected. In C retrieve state values, apply the caller's hook, then pass the transformed values to the result adapter. |
clip_gradient(limit, values) |
hmath_ad_clip_gradient |
Clip Euclidean norm without overflowing the norm calculation. C returns a gradient-list handle to copy/release. Positive finite limit is required. |
Symbolic tensors¶
ADTensor(shape, values) / ad_tensor(shape, values) call the original checked
adTensor; values are expressions in logical row-major order. C uses
hmath_ad_tensor_create. Shape product must match expression count, with original
negative-dimension, broadcast and contraction failures preserved.
| Python | C | Meaning |
|---|---|---|
ad_shape(tensor) / tensor.shape |
hmath_ad_shape_copy |
Dimensions; Python returns a tuple, C uses ptrdiff_t array. |
ad_values(tensor) / tensor.values |
hmath_ad_tensor_values |
Logical expression list. Every returned C expression reference is owned. |
ad_broadcast(shape, tensor) |
hmath_ad_broadcast |
Original checked broadcasting; reverse contributions sum over repeats. |
ad_add(left, right) / left + right |
hmath_ad_tensor_add |
Broadcasted symbolic addition. |
ad_multiply(left, right) / left * right |
hmath_ad_tensor_multiply |
Broadcasted symbolic elementwise product. |
ad_matmul(left, right) / left @ right |
hmath_ad_matmul |
Original symbolic batched matrix multiplication. |
ad_contract(left_axes, right_axes, left, right) |
hmath_ad_contract |
Contraction preserving uncontracted axis order. |
ad_sum(tensor) |
hmath_ad_sum |
Symbolic scalar sum. |
hmath_ad_metadata reports value count and rank before allocating getter buffers.
These tensors retain the original scalar-expression expansion; this binding does
not claim a new tensor-kernel AD engine or change the existing graph budget.
See original tensor rules.
Custom rules and checkpoints¶
custom_operation(rule, expressions) calls original customOperation. The rule
receives numerical values and returns (value, partial_derivatives). The original
reverse evaluator checks derivative arity and finite values. Value-only compiled
graph evaluation retains its original behavior, including not validating unused
partial-derivative arity. Python materializes the returned derivative sequence
when invoking the foreign callback.
custom_unary(function, derivative, expression) calls original customUnary
with separate native callbacks. Evaluating only the graph value does not call the
derivative. Custom rules retain their first-order limitation: symbolic higher
orders return the original InvalidArgument.
checkpoint(builder, expressions) stores the original graph-building recipe.
The recipe is replayed by Haskell during evaluation/backpropagation. Its inputs
are local symbolic expressions, not concrete floating-point values. Builder
results may contain nested custom rules or checkpoints.
Python keeps callbacks and their referenced data alive transitively through
arithmetic, tensor, derivative and compiled-graph handles, including expressions
created by checkpoint replay. Python exceptions are caught at the callback
boundary and rethrown after native cleanup. An existing HMathError retains its
status and message. Callback-created expressions are retained conservatively
until all owning checkpoint/derived handles are released.
The additive C entries hmath_ad_custom_operation_checked,
hmath_ad_custom_unary_checked and hmath_ad_checkpoint_checked use the
hmath_callback_error wire from hmath_callback.h. This preserves all 21
original MathError constructors and complete message payloads, including NUL,
Unicode and long strings. Returning status11 from a custom rule supplies an
actual Left MathError to the original evaluator. Positioned expression errors
(status12) and interchange errors (status13) retain their structured fields.
Python selects these entries when available; raise hmath.errors.MathError,
hmath.expression.ExprError or an interchange error from a callback to preserve
that domain value. Unexpected Python exceptions rethrow after native cleanup.
C callback function pointers, userdata and error payload storage must remain live until all derived handles are released. Callbacks are synchronous and may reenter HMath; return an error status rather than throwing across C. Error payload memory must remain valid until the invoking native call returns. The legacy string-only callback entries remain ABI compatible; use the checked entries for lossless structured failures. Every callback-created Expr returned from checkpoint replay is independently retained for its derived graph's life.
C custom-rule callbacks receive writable partial capacity equal to argument count and separately report actual partial count. Never write beyond capacity. A checkpoint builder receives borrowed expression handles and returns one owned expression reference; retain an input before returning that same input. The header contains the complete callback signatures.
from hmath import autodiff as ad
assert ad.value_and_gradient(
lambda xs: ad.custom_unary(lambda x: x**3, lambda x: 3*x*x, xs[0]),
[2],
) == (8, [12])
assert ad.value_and_gradient(
lambda xs: ad.checkpoint(lambda ys: ys[0]*ys[0], xs),
[2],
) == (4, [4])
See custom rules and checkpoints.
Shared object interchange¶
hmath_ad_value_import converts an owned legacy Expr/ADTensor/Dual handle into
an independently owned shared Value, and hmath_ad_value_export converts back.
No expression is reparsed or reconstructed mathematically. Python performs this
conversion automatically for neural and generic APIs, retaining callback owners
through the converted value and every derived result. Value287 is original Expr,
288 is original ADTensor and289 is original Autodiff.Dual. Numeric.Dual remains
a distinct original type; these owners are never silently exchanged.