Changelog¶
0.4.0 (2026-09-14)¶
Debuggability, backend unification, memory, and a reproducible writeup.
tangent.explain now returns a dead_inputs list and a data-flow graph, so
the Tangent 2 paper notebook (which uses it) needs this release.
Added in 0.4.0¶
- Public custom-gradient API.
tangent.register_adjoint/tangent.register_tangentregister a reverse-/forward-mode template rule for a library op you do not own (e.g.numpy.hypot), alongside the existing@tangent.custom_vjpfor your own functions and black boxes. Documented indocs/custom_gradients.md. - Typed public API (PEP 561). Ships a
tangent/py.typedmarker and inline types, sodf = tangent.grad(f)is a typed callable to mypy/pyright; for a single-argument function the gradient carries the input's type. explaindata-flow graph + static dead-input detection. For straight-line functionstangent.explainprints the primal's data-flow graph and returnsdead_inputs— arguments that provably never affect the output — plus adataflowentry. Conservative: control flow yields no graph rather than a wrong claim.- Opt-in tape-liveness optimization (
optimizations={'tape_liveness': True}). Stores only the shape of primals the adjoint reads for shape (aTapedShapecarrier), cutting peak gradient memory ~90% on array loops with an identical gradient, proven safe by reaching-definition analysis.
Changed in 0.4.0¶
- Unified backend op catalog (
tangent/op_catalog.py). The six array backends register their shared elementwise, binary and reduction rules through oneregister/register_reductionsentry, so no backend can silently drift. - Coarsening → backend kernel handoff. Straight-line coarsening now emits the
symbolic VJP in the primal's own backend for JAX and PyTorch, so pairing it
with
compile='jax'/torch.compilefuses the segment into one kernel (TensorFlow / Keras still fall back). insert_grad_ofis now documented as a first-class gradient-surgery API in the API reference, with a gradient-surgery notebook.- Honest benchmarks. A cross-framework benchmark harness
(
benchmarks/vs_frameworks.py, jax / torch.func / autograd / finite differences on MLP / conv / scalar) and a single canonical building-simulation benchmark page; the reconciled optimization impact is ~2.6× from tape-aware DCE (the symbolic passes add nothing on that array-dominated workload).
Fixed in 0.4.0¶
- Generated primal/tangent functions no longer carry the source method's
decorators, so differentiating a
@staticmethodworks on Python 3.9 (a module-levelstaticmethodobject is not callable before 3.10).
Docs and reproducibility¶
- A reproducible Tangent 2 writeup in
paper/(runnable notebook +paper/reproduce.py), a Colab notebook gallery landing page, and case studies differentiating messy NumPy simulators (LIBOR Market Model greeks, projectile-with-drag ODE, SIR epidemic calibration). - Developer tooling:
make check/ pre-commit hooks for the ruff CI gates.
0.3.0 (2026-09-11)¶
Everything below shipped since the 0.2.0 release: differentiable ODEs and shape checking, backend-compiled adjoints, a persistent disk cache, custom gradient rules, while-loop and annotated checkpointing, SciPy support, a notebook/REPL front door, and IR-invariant verification.
Added in 0.3.0¶
- IR invariant verification (
tangent/verify.py): the frontend is a sequence of source-to-source passes, each assuming the previous ones' output shape - "works until two passes disagree." Each pass's output contract is now a checkable invariant (ANF form afteranf, resolvedfuncannotations afterresolve_calls, no nested defs afterclass_desugar, no lambdas afterlambda_desugar). WithTANGENT_VERIFY_IR=1(orrun_passes(..., verify=True)) the pass manager checks the relevant invariant after each pass and raisesIRInvariantErrornaming the pass and offending node, so malformed IR fails at its source rather than three passes later. Off by default; the whole corpus is verified to satisfy its invariants (tests/test_ir_invariants.py, and the full 76k suite passes with verification on). This is the safe, shippable realization of "one explicit IR with a documented invariant"; a wholesale single-rewrite core and interprocedural activity analysis remain future work. - SciPy gradients (
tangent/scipy_extensions.py, optional): adjoints and forward-mode tangents forscipy.special- the error-function family (erf, erfc), the gamma family (gammaln, gamma, digamma/psi), the logistic pair (expit, logit), xlogy, and the logsumexp reduction (adjoint routes through the softmax) - plusscipy.linalg.solve(vector and matrix RHS) andscipy.linalg.inv. Scientific and statistical code written against SciPy now differentiates like NumPy code. All FD-verified; loaded silently when SciPy is present. - Works in notebooks, the REPL, and dynamic code (
@tangent.function): source-to-source AD needs the function's source, whichinspect.getsourcecannot retrieve in the plain REPL, inexec-defined functions, or after the source file moves.@tangent.functioncaptures the source at definition time;tangent.grad(f, source=...)and@tangent.function(source=...)accept it explicitly. Captured source survives even deletion of the defining file. The "source unavailable" error now points at these fixes. - Compile-time shape checking (
tangent.check_shapes): abstractly evaluates a function onShapedArrayinputs (shape + dtype, no data), applying NumPy's real broadcasting and linear-algebra rules, and raises aShapeErrorpointing at the offending source line on a rank/broadcast/ matmul/reshape mismatch - catching the class of bug finite differences only reveal at run time. Rides NumPy's dispatch protocols; an unmodeled op yields an unknown shape rather than a guess, so there are no false positives. - Differentiable ODE integration (
tangent.odeint): integratedy/dt = func(y, t, *args)and differentiate the solution w.r.t. the initial state and parameters via the continuous adjoint method (Chen et al. 2018) - memory constant in the number of integration steps. The adjoint dynamics are vector-Jacobian products of the user's ownfunc, generated bytangent.vjp(the "define the dynamics in NumPy, get the adjoint solve for free" story). Fixed-step RK4, reverse-mode first order; forward mode and higher order raise cleanly. Seeexamples/neural_ode.py. - Checkpointing for while-loops and data-dependent iteration: online
(Stumm-Walther) checkpointing handles loops whose trip count is unknown at
compile time (convergence loops, fixed-point solvers). Loop-carried-state
snapshots are kept under a fixed budget and geometrically thinned as the
loop runs, so peak tape memory stays bounded by
2 * budgetsnapshots for any number of iterations (97%+ reduction measured on a 3500-iteration solver), with gradients identical to the fully-taped path. Engaged bywith tangent.checkpoint():orgrad(..., checkpoint=True);breakinside the loop is supported. A latent bug was fixed en route: FixGrad could zero-initialize the adjoint's seed parameter when its only use was buried in nested loops, which now never happens (a parameter is always defined). - Compile the adjoint, don't just print it:
tangent.grad(f, compile='jax'|'torch'|'tinygrad')lowers the generated gradient into a backend compiler while keeping the readable source attached (df.__tangent_source__). Measured: the jax-lowered Tangent adjoint of an MLP loss matchesjax.grad+jiteval time exactly (0.075 vs 0.077 ms). The runtime type-dispatch registries now resolve subclasses (jit tracers, FakeTensors) by isinstance with caching, which is what makes lowering possible. - Persistent disk cache: generated gradient source is cached across
processes (
~/.cache/tangent-ad,TANGENT_CACHE_DIR/TANGENT_DISK_CACHEto override), keyed by function source + options + a fingerprint of Tangent's own sources; ~13x faster coldgrad()measured. Load verifies every referenced global resolves and falls back to full recompilation on any problem. tangent.explain(f, x)prints the primal, the generated adjoint, the gradient, a finite-difference cross-check, and flags arguments that do not affect the output;tangent.source_map(df)maps generated-gradient lines back to the primal statements (and user-file line numbers) they differentiate.- Public custom-gradient API:
@tangent.custom_vjpwithfunc.defvjp(bwd)/func.defjvp(jvp)registers user reverse/forward rules (JAX-shaped; Tangent re-provides primals so there is no residual plumbing), andtangent.stop_gradient(x)freezes a value in both modes. with tangent.checkpoint():annotates specific loops for segment checkpointing - no global flag, no length threshold, and runtime loop bounds are allowed (96%+ peak-tape reduction measured, gradients bit-identical). Implemented as an early desugar pass because the dataflow analyses treatwithas opaque.- Dynamic
np.concatenate/np.stack: lists built by.append()in loops now differentiate through concat/stack (the adjoint returns a list of per-element gradients that flows through the list machinery). Also fixes a latent inliner bug where a dynamically built list could be replaced by its initial[]literal. python -m tangent doctordiagnoses the classic installation traps: the abandoned 2017tangentpackage colliding withtangent-ad, broken optional backends, and a smoke gradient.- Framework benchmarks (
benchmarks/vs_frameworks.py): tangent vsjax.gradvstorch.autogradon a tensor-heavy MLP and a 1000-step data-dependent scalar recurrence, compile and eval time reported separately; results in the Performance guide (tangent is 6x faster than eager torch on the recurrence at 1% of JAX's unroll-compile cost).
0.2.0 (2026-09-11)¶
First version of this fork to be numbered ahead of Google's final PyPI release (0.1.9, December 2017), so that installs from this repository upgrade cleanly over the abandoned upstream package.
Highlights since upstream 0.1.9¶
- New backends: JAX, TensorFlow 2.x (eager), PyTorch, Keras 3
(backend-agnostic
keras.ops), and tinygrad (method-basedTensor.*API, including conv2d/max_pool2d/avg_pool2d/layernorm/batchnorm). - Higher-order differentiation: second and third derivatives, Hessian-vector products, forward-over-reverse.
- Language coverage: classes and inheritance, closures, lambdas,
comprehensions,
enumerate/zip, f-strings, chained and augmented assignment (including subscript targets), early returns, tuple unpacking, the@operator, pytree (container) arguments — tracked indocs/features/PYTHON_FEATURE_SUPPORT.mdand enforced bytests/test_feature_matrix.py. - Gradient checkpointing:
grad(f, checkpoint=True)performs segment (√n) recomputation on eligible counted loops — O(√n) peak tape memory with identical gradients (96.8% peak reduction measured). Seedocs/checkpointing_user_guide.mdand the "Real gradient checkpointing" entry below. - Optimizations: dead-code elimination, common-subexpression elimination, algebraic simplification (SymPy), and a straight-line coarsening prototype.
- Tooling: gradient-flow and computation-graph visualization, gradient-function caching, clear rejection errors for non-differentiable syntax, a finite-difference oracle for backend adjoints.
- Modernization: Python 3.9–3.13, gast 0.6/0.7, NumPy 2.x, pyproject-based packaging, GitHub Actions CI, 75k+ parameterized tests.
Added in 0.2.0¶
- Differentiable list building:
xs.append(v)andv = xs.pop()are now differentiated (previously.append()was silently treated as non-differentiable, returning zero gradients). They are desugared into rebindings through three new primitives (tangent.list_append/list_last/list_init) whose adjoints are written in terms of each other, so forward mode and second/third derivatives work, including appends inside dynamic-length loops. In-place mutations that cannot be expressed as a rebinding (extend/insert/remove/sort/reverse, or append through an attribute/subscript) are rejected with a clear error instead of silently dropping gradients. - Distribution, docs site, and release pipeline: the package is now
distributed on PyPI as
tangent-ad(the import name staystangent; PyPI'stangentis the abandoned 2017 upstream release - uninstall it before installing this). A documentation site built with Material for MkDocs (landing page, getting-started guides, user guide, generated API reference, benchmarks) deploys to GitHub Pages on every push, and pushing avX.Y.Ztag builds, verifies (twine + clean-venv wheel smoke test + tag/version match), publishes to PyPI via Trusted Publishing, and creates a GitHub Release. See RELEASING.md for the one-time PyPI/Pages setup. - Derivative coverage tail: registered adjoints (and forward-mode rules
where mechanical) for
np.sort(permutation-routing, closed under higher order),np.argsort(non-differentiable, zero tangent),np.cumprod,np.pad(constant mode),np.take(flat and 1-D-indices-with-axis forms, with repeated-index accumulation), two-operand explicit-outputnp.einsum(the gradient equation is rearranged at run time; ellipsis, implicit output, and diagonal/trace forms raise a cleanNotImplementedError),np.linalg.choleskyandnp.linalg.eigvalsh, plus a forward-mode rule for TF'srsqrt(previously pinned as forward-unimplemented). All verified against the finite-difference oracle. - Compile-time scalability: functions a few hundred statements long used
to crash with
RecursionErrorbefore they could be differentiated (the CFG dataflow walk recursed once per statement); the walk is now an iterative worklist, and a 400-statement chain compiles fine. The optimizer's fixpoints report changes directly instead of serializing the entire AST withgast.dumptwice per iteration, dead-code elimination peels cascading dead chains in a single dataflow analysis instead of one analysis per layer, and the post-advanced-DCE cleanup round only runs when advanced DCE changed something. Compilation of a 150-statement function dropped ~33%, and the full test suite runs ~25% faster. - Real gradient checkpointing:
tangent.grad(f, checkpoint=True)now implements segment (√n) recomputation — eligible loops run untaped with a snapshot of the loop-carried state every ceil(√n) iterations, and the backward pass replays one segment at a time. Peak tape memory drops from O(n) to O(√n) with gradients identical to the fully-taped path (measured: 49.2 MB → 1.6 MB, 96.8%, on a 2000-iteration loop with 1000-float state;benchmarks/checkpointing_memory.py). The previous implementation stored only the loop target selectively (~0% measured reduction on state-carrying loops) and was restricted to zero-basedrange(n); any constant-boundrange(start, stop, step)is now eligible. First-order reverse mode only. - Mode consistency: every corpus construct now behaves the same in every
differentiation mode. The last "first-order-only" gaps are closed - multi-
output functions (
return 2*a, a; polar transforms) and subscript-scatter loops now pass forward mode and reverse-over-reverse too - and the audited harness exclusion list contains only by-design entries. Builtin casts and comparisons gained symmetric rules in both modes:float()(identity),int()(zero derivative a.e.), and forward-mode twins forabs/min/max. Forward mode also seeds loop-counter tangents, fixing a NameError on anyx * iarithmetic inside loops. - Clean errors restored for unregistered ops: NumPy 2.x wraps most public
functions in
_ArrayFunctionDispatcher, which the unimplemented-op scan did not recognize - the unimplemented sets were nearly empty, so calls likenp.sort(x)recursed into NumPy's own source and crashed with an opaqueAttributeErrorinstead of raisingReverseNotImplementedError/ForwardNotImplementedError. The scan now recognizes dispatchers, and forward mode's no-source fallback raises the standard clean error instead of a bareValueError. breakandcontinue: both now differentiate exactly (historicallybreakmiscomputed gradients via tape replay, then both were rejected). A newloop_exit_desugarpass lowers them into guard flags before differentiation:continuebecomes a per-iteration skip flag guarding the rest of the body;breakadds a loop-level flag that awhilefolds into its condition and aforuses to skip remaining iterations. Works with data-dependent exits, nested loops, forward mode, both motions, and second order. Loops with anelseclause remain rejected (while/elsenow rejected explicitly, likefor/else).returninside loops: lowered into an assignment plus a returning flag plusbreak(which the break lowering then handles), with the exit propagated past each enclosing loop and the final return lifted by the single-exit transform. Previously rejected.- Iteration over computed sequences:
for v in x * 2.0,for v in np.flip(x), andfor v in [x, x * 2](a literal of active values) now differentiate - the iterable expression is hoisted into a named intermediate and indexed, the same rewrite long applied to named sequences. Previously these loops were neither rewritten nor rejected and gradients through the loop variable were silently dropped. Tuple-unpacking targets (for a, b in pairs) work; set literals of active values are rejected (no stable order). - List comprehensions over runtime iterables:
[f(v) for v in xs]with a runtimexsis now lowered into an indexed loop built ontangent.list_append(previously rejected; before that, silently wrong). Runtimeiffilters and nested comprehensions are supported; multiple generators and tuple targets are still rejected cleanly. - Container (pytree) return values in reverse mode: functions returning
dicts, lists, or nested containers of arrays can now be differentiated
(previously a
KeyErrorcrash). The default seed expands to a matching pytree of ones (the gradient of the sum of all leaves), a caller-supplied seed of the same structure is used as the cotangent, and second derivatives through container arguments work. A structurally mismatched seed raises a clearValueErrorinstead of being silently replaced. - New NumPy derivatives (reverse and forward mode, validated against a
finite-difference oracle): the ufunc spellings
np.add,np.subtract,np.divide,np.negative,np.power,np.float_power;np.arctan2,np.hypot,np.logaddexp,np.arcsinh,np.arccosh,np.arctanh,np.exp2,np.cbrt,np.fmax,np.fmin; the shape opsnp.cumsum,np.flip,np.ravel,np.swapaxes,np.moveaxis,np.tile,np.repeat,np.roll; andnp.linalg.solveplusnp.linalg.norm(default 2-norm/Frobenius). - Backend parity: the
@matmul operator now differentiates for PyTorch (and Keras); softmax/log_softmax adjoints for PyTorch, Keras, JAX and TensorFlow;concatenate/stackdifferentiate across NumPy, JAX, TensorFlow, PyTorch and Keras (previously JAX-only); elementwise gaps closed across backends (e.g. torchexpm1/rsqrt, jaxreciprocal/log1p/expm1/exp2, keras log/exp family,tf.math.reciprocal— whose old TF-1-only spelling registered nothing on TF 2.x). Unary elementwise rules are now generated from one shared backend-neutral table, so every table op has both reverse- and forward-mode rules on every backend. - Forward mode catches up: the extended NumPy and TensorFlow modules previously registered zero forward-mode tangents; they now cover their op catalogs (abs, clip, where, min/max/prod, trig, linalg.inv, trace, softmax, concat/stack, ...).
tangent.backend_status()reports each optional backend as'available','not installed', or'broken: <error>'.tangent.passes: a documented pass registry for the desugaring pipeline withregister_pass(..., before=/after=)andunregister_passas an extension hook for inserting custom frontend passes.- Property-based tests (hypothesis) drive a corpus sample against the finite-difference oracle; CI now also runs the tinygrad-backed tests, ruff lint + format checks, and a coverage floor.
Changed in 0.2.0¶
import tangentis silent. Missing optional backends (jax, torch, tensorflow, keras, tinygrad) no longer emit a wall ofUserWarnings and stdout banners; they are logged at DEBUG level on the'tangent'logger. AUserWarningis kept only when a backend package is installed but its extensions fail to load (a broken install is actionable). Usetangent.backend_status()to see what loaded.- Gradient-cache keys now include compilation settings.
grad(f)followed bygrad(f, optimizations={...})(or a differentcheckpoint_config/grad_config) previously returned the same cached function; such calls now compile and cache separately, so previously colliding call sites will each trigger their own (correct) compilation. Cache hits are also much cheaper: source hashes are memoized instead of re-reading the source file on every lookup. - Checkpointing consolidated to what demonstrably works:
grad(f, checkpoint=True)(zero-based constantrange(n)loops) and the manualcheckpointed_loopforward-pass helper.grad_with_checkpointingnow raisesNotImplementedErrorat wrapper-creation time with an actionable message.checkpoint=Truenow composes withoptimized=True(optimization is no longer force-disabled). - List/set/dict comprehensions over dynamic iterables are rejected with a
clear
TangentParseErrorat transform time. The old.append()-loop fallback crashed in return position and silently produced zero gradients in assignment position. Comprehensions over compile-time-constant iterables are still unrolled and differentiate correctly. numpy/tfconcatenate/stackover a dynamically built list raiseNotImplementedErrorinstead of silently returning zero gradients.- The codebase is uniformly formatted with
ruff format(4-space indent, 100 columns), enforced in CI; the reformat commit is listed in.git-blame-ignore-revs. - Docs pruned: ~15 stale point-in-time progress reports (phase "COMPLETE" writeups, outdated test-status snapshots) were deleted or folded into the living feature guides; remaining status claims match measured reality.
Fixed in 0.2.0¶
tangent.autodiff(f, optimizations=...),checkpoint_config=...andgrad_config=...raisedTypeError: the caching wrapper re-declared a narrower parameter list than the realautodiff. The full signature is restored.- Wrong or crashing higher-order derivatives with
optimized=True: dead-code elimination trusted per-op-id tape annotations that are not unique in differentiated gradient code, so it could delete the wrong tape push (second derivatives read stale primal values, or tripped the stack op-id assertion). DCE now computes its own balanced push/pop pairing and never removes an ambiguous pair; the two long-standing expected failures in reverse-over-reverse are eliminated. - Multi-output functions with
check_dims=Falsecrashed (or could get a wrong default seed): the output arity was inferred by scanning generated code for the shape-check assert. It is now recorded as an annotation during the reverse-mode transformation. grad(..., checkpoint=True)could produce wrong gradients forfor i in range(start, stop)loops withstart != 0: the checkpointed adjoint reconstructs the loop target as the zero-based iteration index. Such loops now fall back to the standard (non-checkpointed) templates.- Broadcasting operands of
np.multiplyandnp.maximumreceived wrongly-shaped gradients (missing unbroadcast reduction). ValueErrorfor mismatched forward-mode derivative arguments was raised incorrectly (aTypeErrormasked the real message).- The stack variable in the subscript-assignment adjoint template was bound via
a typo'd keyword (
_stack_), working only because the default name matched. - Package metadata now identifies this fork (maintainer, repository URLs) and
exposes
tangent.__version__.
Removed in 0.2.0¶
- An unreachable, unfinished automatic-checkpointing pipeline
(
tangent/checkpointing/,tangent/analysis/,tangent/preprocessing/), the placeholdercheckpointed_backward(its default path returned the incoming gradient unchanged), andcheckpointed_grad(its "simplified backward" returned the single-step gradient at the final state, not the loop gradient). - The vendored Python 2
funcsigsbackport, in favor ofinspect.signature.