# aadc — AADC for Python (agent guide)

Adjoint algorithmic differentiation. Record a computation once, replay it many
times with exact derivatives — vectorised over AVX lanes, optionally JIT-
compiled.

This file ships INSIDE the wheel. `aadc.llms_txt()` returns it, so it always
describes the version actually imported. Check `aadc.__version__` (wheel) and
`aadc.__engine_version__` (C++ engine) — they are different facts and neither
implies the other.

## Install

    pip install aadc

Linux x86-64/aarch64, macOS arm64, Windows x86-64; CPython 3.10–3.14.
Unlicensed use runs in Community Edition (non-commercial) and prints one line
to stderr at import. `aadc.license_status()` reports the state; a licence is
supplied out-of-band via `$AADC_NG_LICENSE`, `./aadc-ng.lic` or
`~/.aadc-ng/license`. `aadc.license(key)` is a 1.x API that no longer exists
and raises NotImplementedError saying so.

## The core loop

    import aadc

    f = aadc.Functions()
    f.start_recording()
    x  = aadc.idouble(2.0)
    ax = x.mark_as_input()
    y  = x * x + 3.0
    ay = y.mark_as_output()
    f.stop_recording()

    ws = f.create_workspace()
    ws.set_val(ax, 5.0)
    ws.forward()                  # ws.val(ay) -> 28.0
    ws.set_diff(ay, 1.0)
    ws.reverse()
    ws.diff(ax)                   # -> 10.0   (dy/dx = 2x at x=5)

Four rules that explain most confusion:

1. `mark_as_input()` / `mark_as_output()` return HANDLES (`Argument`,
   `Result`). Replay is addressed through those, never through the Python
   variable `x` — after `stop_recording()`, `x` is a stale value.
2. The recorded graph is fixed. Changing an input and replaying is free;
   changing the *structure* means recording again.
3. Reverse mode needs a seed: `ws.set_diff(<output>, 1.0)` before
   `ws.reverse()`, then read `ws.diff(<input>)`. Seeding nothing gives zeros —
   that is the commonest "my gradients are zero" cause. Adjoints do NOT
   accumulate across independently seeded passes on this engine: a fresh
   `set_diff()` wipes the stale plane (measured). `reset_diff()` is for
   clearing state deliberately.
4. `Kernel` is an alias of `Functions`. Same class, two names.

`record_kernel` is the same thing with less ceremony:

    with aadc.record_kernel() as kernel:
        x  = aadc.idouble(2.0)
        ax = x.mark_as_input()
        ay = (x * x + 3.0).mark_as_output()

## The trap: branching on an active value

`if x > 0:` needs a plain `bool`, so the comparison COLLAPSES — the direction
it took while recording is frozen into the tape. Replay with a different input
still follows the recorded branch, producing a plausible wrong number and a
wrong derivative.

    y = aadc.iif(x > strike, x - strike, aadc.idouble(0.0))   # correct
    if x > strike: ...                                        # frozen

This is REPORTED, not silent:

    aadc.recording_passive_warnings()   # during/after recording
    kernel.passive_warnings()           # per kernel
    kernel.num_passive_warnings()

Treat a non-zero count as a finding to explain, not noise. Related helpers:
`aadc.where_arr` (array form), `aadc.masked_assign`, `aadc.smart_assign`,
`aadc.branching_function` + `aadc.aadc_early_return` for whole functions,
`aadc.lower_bound` for searching a sorted knot array (it returns the
INSERTION index -- count of knots < x -- so the containing interval is
one less).

## Arrays

`aadc.array(...)` gives an `AADCArray`: a NumPy-compatible array of active
values. NumPy ufuncs work on it; `mark_as_input()` / `mark_as_output()` apply
elementwise and return arrays of handles.

    import numpy as np
    a  = aadc.array(np.array([1.0, 2.0, 3.0]))
    aa = a.mark_as_input()
    s  = np.sum(a * a)

Use `aadc.math` (not `math`) for scalar transcendentals on active values;
`np.exp` and friends already dispatch correctly on `idouble` and `AADCArray`,
and so does `np.where` on active arrays.

`isnan()`, `isfinite()` and `isinf()` all RECORD (scalar and array): they
return an `ibool` / mask that re-evaluates per replay, and cost no passive
conversion. Collapsing one with `if` does — use `aadc.iif(x.isnan(), a, b)`.
`isinf()` needs aadc-ng >= 2.15.0; older engines had no IsInf opcode and
returned a frozen, *uncounted* bool.

Two things do NOT record: `x.floor()/ceil()/trunc()` return plain floats and
raise the passive-warning count, and `x.to_int()` returns a plain Python `int`.

## Speed: compile, then batch

    kernel.compile()              # JIT the tape (falls back with a warning)
    kernel.last_forward_used_compiled()   # True iff a compiled segment ran

`aadc.evaluate(...)` replays a kernel over many input rows, across threads and
AVX lanes, returning values and derivatives together. This is where AADC pays
for itself: per-row cost after recording is a fraction of re-running the
original Python. `aadc.ThreadPool` sizes the workers; one `Workspace` per
thread — a Workspace is NOT shareable.

## Source tracing: find the code that computed a number

Record with `trace=True` and the tape carries file/line/function for every
operation:

    f.start_recording(trace=True)
    ...
    f.stop_recording()
    print(f.trace_report(outputs={ay: "npv"}))
    f.trace_toc()                 # per-function index, [begin, end) op ranges
    f.trace_range(begin, end)     # operations in a range

For an agent this is the difference between guessing which code ran and
knowing. Attribution requires the code to have been COMPILED with the
instrumenting toolchain (`aadcpp -faadc-debug`): Python-level arithmetic and
the engine are always attributed; third-party C++ (e.g. QuantLib) only if you
installed an instrumented build. A report with 0% attribution looks exactly
like a healthy one — check the counters.

## Verifying a derivative (do this before trusting one)

    ws.set_val(ax, 5.0); ws.forward(); base = ws.val(ay)
    h = 1e-6
    ws.set_val(ax, 5.0 + h); ws.forward(); up = ws.val(ay)
    fd = (up - base) / h                  # ~10.0, compare with ws.diff(ax)

If AAD and finite differences disagree by more than FD noise, suspect a frozen
branch first (check the passive-warning count), then a missing
`mark_as_diff()`, then a genuine bug — in that order.

## Error messages you will actually hit

* `ImportError: AADC_BACKEND=...` — the 1.x engine was removed. Unset the
  variable, or `pip install "aadc<2"` for the old line.
* Zero gradients everywhere — no `set_diff` seed, or the value was never
  connected to an input (it was a plain float, not an `idouble`), or it passed
  through something passive: `float()`, `.val()`, `.to_int()`, `floor()`.
* Two copies of `libaadc-ng` in one process — values can be right and
  gradients silently wrong. Never vendor the library into a second wheel; all
  AADC packages must resolve to one image.
* `dir(aadc)` is the declared API (`aadc.__all__`). Names outside it exist but
  are not interface.

## Map of the API

    idouble ibool iint          active scalars
    Functions / Kernel          recording + replay driver
    Workspace                   one replay's values and adjoints (per thread)
    Argument Result             input/output handles
    array fromiter ArrayNG      NumPy-compatible active arrays
    record_kernel               record a block
    record                      record a function -- NOTE its Jacobian is
                                FINITE-DIFFERENCE, not adjoint
    iif where_arr               branchless selection
    masked_assign(target, value) branch-mask assign; returns the new value
    evaluate                    batched replay (evaluate_sums raises
                                NotImplementedError on this engine)
    optimize least_squares      solvers over recorded kernels
    root_scalar                 scalar root find on the tape
    aadc_assert                 precondition recorded on the tape
    trace_report trace_toc      source attribution (record with trace=True)

Full documentation: https://github.com/matlogica — see the `aadc-docs`
Python book. Contact: info@matlogica.com
