Skip to content

Rewrite tax from the ground up with a slice-streamed ET architecture - #15

Closed
andreapasquale94 wants to merge 12 commits into
mainfrom
claude/tax-rewrite-ground-up-tfsL6
Closed

Rewrite tax from the ground up with a slice-streamed ET architecture#15
andreapasquale94 wants to merge 12 commits into
mainfrom
claude/tax-rewrite-ground-up-tfsL6

Conversation

@andreapasquale94

Copy link
Copy Markdown
Owner

Discards the previous module layout (ADS, ODE, separate kernel/op
hierarchies, expression flattening, Doxygen, MkDocs) and replaces it
with a foundation built around two architectural pillars:

  • Dual sizing. TruncatedTaylorExpansionT<T,N,M> (static-extent
    Eigen::Matrix) and DynamicTaylorExpansion (runtime-sized
    Eigen::VectorX) share a single TaxExpression concept and feed the
    same coefficient kernels. Mixed static/dynamic expressions are
    rejected at compile time via SameKindExpression.

  • Slice-streamed ET. View-like nodes (Add/Sub/Neg/ScalarMul/
    ScalarAdd) allocate nothing; their degreeSlice(d) returns a custom
    ParentSliceView holding stable refs to the parent. Buffered nodes
    (Mul/Div/Square/Sqrt/Exp/Log/SinCos/SinhCosh) own coeffs_ buffers
    and fill them monotonically through advanceTo. The operator<<=
    driver writes directly into the destination so the root allocates
    nothing either.

Coverage: +, -, *, /, scalar variants, sin, cos, tan, sinh, cosh,
tanh, exp, log, sqrt, square, cube. 47 GoogleTests across 5
executables pass under both Release and ASan+UBSan.

Out of scope for this commit (will land on top of the new
foundation): ADS, ODE integrator, Eigen vector/matrix adapters,
benchmarks, DACE comparison, Doxygen / MkDocs site, Python bindings.

claude added 12 commits May 9, 2026 21:37
Discards the previous module layout (ADS, ODE, separate kernel/op
hierarchies, expression flattening, Doxygen, MkDocs) and replaces it
with a foundation built around two architectural pillars:

- Dual sizing.  TruncatedTaylorExpansionT<T,N,M> (static-extent
  Eigen::Matrix) and DynamicTaylorExpansion<T> (runtime-sized
  Eigen::VectorX) share a single TaxExpression concept and feed the
  same coefficient kernels.  Mixed static/dynamic expressions are
  rejected at compile time via SameKindExpression.

- Slice-streamed ET.  View-like nodes (Add/Sub/Neg/ScalarMul/
  ScalarAdd) allocate nothing; their degreeSlice(d) returns a custom
  ParentSliceView holding stable refs to the parent.  Buffered nodes
  (Mul/Div/Square/Sqrt/Exp/Log/SinCos/SinhCosh) own coeffs_ buffers
  and fill them monotonically through advanceTo.  The operator<<=
  driver writes directly into the destination so the root allocates
  nothing either.

Coverage: +, -, *, /, scalar variants, sin, cos, tan, sinh, cosh,
tanh, exp, log, sqrt, square, cube.  47 GoogleTests across 5
executables pass under both Release and ASan+UBSan.

Out of scope for this commit (will land on top of the new
foundation): ADS, ODE integrator, Eigen vector/matrix adapters,
benchmarks, DACE comparison, Doxygen / MkDocs site, Python bindings.
`coeff` / `derivative` now accept the multi-index in three forms:
  - std::span<const std::size_t> (the canonical interface)
  - const std::array<std::size_t, Vars>& (lets callers write
    `result.coeff({1, 0})` directly)
  - template <std::size_t... Alpha> form (`result.coeff<1, 0>()`)
    where the flat index and factorial are computed at compile time
    and the runtime cost collapses to a single Eigen access.

`eval` likewise gains an std::array<T, Vars> overload so the brief's
`result.eval({0.1, 0.05})` example compiles directly.
- All public ET / storage methods previously named `degreeSlice(d)` are
  now simply `slice(d)`.  Mechanical rename across kernels, view-like
  and buffered ETs, both storage types, the `<<=` driver, and concepts.

- New `python/` directory builds a `tax` Python package via nanobind
  (`pip install nanobind`, then configure with `-DTAX_BUILD_PYTHON=ON`).
  Per the architectural brief, only `DynamicTaylorExpansion<double>`
  is exposed (as `tax.DynTE`); the static-extent C++ path stays C++-only,
  with no `std::variant` over an (Order, Vars) grid and no JIT.

  Bindings cover:
    - DynTE: zero / one / constant / variable / variables factories,
      order / nvars properties, value, coeff, derivative, eval,
      coeffs_norm_inf / _1 / _2, repr.
    - Operators: +, -, *, /, unary -, with both DynTE-DynTE and
      DynTE-scalar variants.  Each evaluates the underlying ET into
      a fresh DynTE because Python cannot meaningfully own lazy ET
      temporaries across statements.
    - Math: sin, cos, tan, sinh, cosh, tanh, exp, log, sqrt, square,
      cube.

- pytest suite under `python/tests` (13 cases, including the brief's
  `u * sin(v) + u * v` example) is registered as a CTest test when
  `pytest` is on PATH.

- New `.github/workflows/python.yml` builds the bindings on Ubuntu
  with Python 3.11, runs both the C++ and Python test suites.
Adds a 14-page MkDocs site under docs/ themed with a custom Anthropic-
inspired palette (warm cream + coral accent in light mode, warm
near-black + same coral in dark mode).  Builds clean under
`mkdocs build --strict`.

Pages:
  - Home: hero + feature grid + at-a-glance comparison
  - Getting started: install, CMake options, hello-Taylor walkthrough
  - Concepts/index, slice-streaming, static-vs-dynamic
  - Guide/index, arithmetic, math-functions, multivariate, derivatives
  - API reference: every public symbol grouped by component
  - Python: nanobind module reference + eager-evaluation rationale
  - Architecture: end-to-end <<= flow + DA recurrences in MathJax
  - Changelog

Theming highlights:
  - Two custom palettes (`tax-light`, `tax-dark`) mapping Material's
    own CSS variables to a warm cream/coral palette
  - Inter for body, JetBrains Mono for code
  - Subtle blurred-glass header, thin nav rules, larger H1 weight
  - Custom .tax-hero and .tax-features components on the landing page

Adds .github/workflows/docs.yml that runs `mkdocs build --strict` on
every push and PR, and on pushes to main publishes the result to
GitHub Pages via actions/deploy-pages.
`tax.DynTE.zero(...)`, `tax.DynTE.one(...)`, `tax.DynTE.constant(...)`,
`tax.DynTE.variable(...)`, `tax.DynTE.variables(...)` are removed.
Construction now goes through module-level utility functions:

    tax.zero(order, nvars)
    tax.one(order, nvars)
    tax.constant(value, order, nvars)
    tax.variable(value, order, nvars, var_idx)
    tax.variables([x0, x1, ...], order)

`tax.DynTE` itself is no longer directly constructible from Python —
it is a return type only.  `isinstance(x, tax.DynTE)` still works, and
the class continues to expose the same accessors (`order`, `nvars`,
`value`, `coeff`, `derivative`, `eval`, norms) and operator
overloads.

This is more idiomatic Pythonic / functional surface area: the
namespace reads as `tax.variable(...)` / `tax.sin(...)` rather than
mixed-style `tax.DynTE.variable(...)` / `tax.sin(...)`.

Tests grow from 13 to 14 cases (adds a check that
`tax.DynTE()` raises TypeError).  Docs (python.md, index.md,
changelog.md, CLAUDE.md) updated accordingly.
`actions/setup-python@v5` with `cache: pip` requires either
`requirements.txt` or `pyproject.toml` to derive the cache key.  The
docs workflow had `cache: pip` set without one, causing every run to
fail with:

    No file in /home/runner/work/tax/tax matched to
    [**/requirements.txt or **/pyproject.toml]

Adds `docs/requirements.txt` listing the MkDocs Material build deps,
points the workflow's `cache-dependency-path` at it, and switches the
install step to `pip install -r docs/requirements.txt`.

mkdocs.yml gets an `exclude_docs` rule so the requirements file does
not ship into the rendered site under /requirements.txt.
Replaces the parallel `TruncatedTaylorExpansionT<T, Order, Vars>` and
`DynamicTaylorExpansion<T>` classes with a single template
`TaylorExpansionT<T, int Order, int Vars>` modelled on
`Eigen::Matrix<T, Rows, Cols>`.  The size template parameters are
signed `int` so that `Eigen::Dynamic` (= -1) doubles as the
runtime-size sentinel:

    Order, Vars >= 0          -> static path (compile-time monomial count,
                                  Eigen::Matrix<T, kSize, 1> on the stack)
    Order = Vars = Dynamic    -> dynamic path (runtime order_/nvars_,
                                  Eigen::VectorX<T> heap)

Mixed dynamism (one static, one dynamic) is rejected with a
static_assert.  Empty-base optimisation collapses the ConstexprShape
helper to zero size in the static case so `sizeof(TE<3>) ==
sizeof(Eigen::Matrix<double, 4, 1>)` (asserted in
tests/test_storage.cpp).

Aliases stay the same:
  TE<N>            = TaylorExpansionT<double, N, 1>
  TEn<N, M>        = TaylorExpansionT<double, N, M>
  DynTE<T = double> = TaylorExpansionT<T, Eigen::Dynamic, Eigen::Dynamic>

Also renames the trait constants to mirror Eigen's
`RowsAtCompileTime` / `IsRowMajor` convention:

  kOrder   -> OrderAtCompileTime   (int, possibly Eigen::Dynamic)
  kVars    -> VarsAtCompileTime    (int, possibly Eigen::Dynamic)
  kStatic  -> IsStatic             (bool)
                IsDynamic = !IsStatic (new convenience)

Files moved:
  static_tte.hpp + dynamic_tte.hpp -> storage/tte.hpp

Other touch-points:
  * concepts.hpp / expr/base.hpp / view_nodes.hpp / buffered_nodes.hpp /
    ops/arithmetic.hpp / ops/assign.hpp: rename trait constants and
    propagate int kOrder/kVars through ET nodes.
  * python/src/tax_module.cpp: use the DynTE alias and wrap factory
    bindings in lambdas (a bare `&DynTE::zero` is now ambiguous because
    the unified template carries both static-only and dynamic-only
    overloads).
  * Docs: rewrote concepts/static-vs-dynamic; updated index, api,
    architecture, python, changelog, README, CLAUDE.md.

Verification:
  * 47 C++ tests pass under Release and ASan + UBSan.
  * 14 Python tests pass.
  * `mkdocs build --strict` clean.
New free functions in `namespace tax`:

  Inverse trig & hyperbolic:
    asin, acos, atan, asinh, acosh, atanh, atan2(y, x)

  Roots & powers:
    cbrt, pow<N>(x) (compile-time integer exponent),
    pow(x, p)      (runtime real exponent),
    hypot(x, y), hypot(x, y, z)

  Exp / log:
    log10

  Special:
    erf

  Paired sin/cos and sinh/cosh:
    sincos(x)   returns SinCosPair<E>   with .sin() / .cos()
    sinhcosh(x) returns SinhCoshPair<E> with .sinh() / .cosh()

  All paired forms share a single buffered node so the second `<<=`
  is just a buffer copy.

New kernels:

  * `eulerAuxRecurrenceComputeDegree` (kernels/inverse_trig.hpp)
    handles all six inverse trig / inverse hyperbolic functions in
    one shape: G(u) * E[F] = sign * E[u].  The buffered ET node
    `InverseFunctionExpr<E, FunKind, GMode, Sign>` parameterises the
    G mode (1 + u^2, 1 - u^2, sqrt(1 - u^2), sqrt(1 + u^2),
    sqrt(u^2 - 1)) and the sign.

  * `atan2ComputeDegree` (same header) for atan2 with the
    (x*E[y] - y*E[x]) RHS.

  * `cbrtComputeDegree`, `powRealComputeDegree`
    (extension to kernels/elementary.hpp).

  * Erf is implemented inline inside `ErfExpr` (no dedicated kernel
    file): maintains H = exp(-u^2) and runs
    `d * F_d = (2/sqrt(pi)) * sum H_a * |b| * u_b`.

New ET nodes (all in `tax::expr`):

    InverseFunctionExpr<E, FunKind, GMode, Sign>
    AtanExpr, AtanhExpr, AsinExpr, AcosExpr, AsinhExpr, AcoshExpr
    Atan2Expr<Y, X>
    CbrtExpr<E>
    PowRealExpr<E>
    ErfExpr<E>
    SinCosNodeExpr<E> + SinCosPairView<Node, ReturnSin>
    SinhCoshNodeExpr<E> + SinhCoshPairView<Node, ReturnSinh>

Python bindings exposed for every new function (asin, acos, atan,
asinh, acosh, atanh, atan2, log10, cbrt, pow, hypot 2/3-arg, erf,
sincos, sinhcosh).  `sincos` / `sinhcosh` return Python `(s, c)`
tuples since Python can't carry a long-lived owner across statements;
under the hood the second `realise(...)` reads from already-populated
buffers thanks to the C++ pair owner.

Tests: 31 C++ math cases (was 13) and 24 Python cases (was 14).
All pass under Release, ASan + UBSan, and pytest.  `mkdocs build
--strict` clean.

Docs: rewrote docs/guide/math-functions.md, expanded the operator
and ET-node tables in docs/api.md, updated changelog.
The erf kernel materialised (u^2)_k into a temporary buffer typed
`Coeffs`, which on the static path is `Eigen::Matrix<T, kSize, 1>`
with `kSize == monomialCount(N, M)`.  Calling `Coeffs::Zero(slice_size)`
with `slice_size != kSize` triggers Eigen's runtime assertion
`v == T(Value)` in Debug builds.  Tests passed in Release where the
assertion is compiled out.

Switch the temporary to an explicitly dynamic
`Eigen::Matrix<Scalar, Eigen::Dynamic, 1>` so the slice-sized
allocation is always valid.
The streaming-assignment operator `<<=` is dropped in favour of an
`Expr<Derived>::eval()` member that materialises any tax expression
into a fresh `TaylorExpansionT` of matching shape:

    auto result = (u * tax::sin(v) + u * v).eval();
    // or
    TEn<3, 2> result = (u * tax::sin(v) + u * v).eval();

For storage operands the surface is unchanged; for ETs, `.eval()` is
now the canonical way to produce a concrete value.  Same streaming
sweep underneath — `eval()` walks degrees 0..order(), advances the
ET, and copies each slice into the destination's coefficient buffer.
No intermediate `TaylorExpansionT` is allocated; only the returned
value.

Implementation:
  * `Expr<Derived>::eval()` declared in expr/base.hpp.
  * Defined out-of-line in storage/tte.hpp (after `TaylorExpansionT`
    is complete) so the body can construct the result type.
  * `operator<<=` removed from `TaylorExpansionT`.
  * `include/tax/ops/assign.hpp` deleted; the umbrella header drops
    its include.
  * Python `realise()` helper simplified to a one-liner that calls
    `.eval()`.

Tests, examples, and docs updated:
  * 47 C++ tests pass under Release / ASan+UBSan / Debug.
  * 24 Python tests pass.
  * `mkdocs build --strict` clean.
to describe the current design only

Storage's polynomial-evaluator method is renamed:

    tte.eval(dx)  ->  tte.at(dx)

`.eval()` (no args) is reserved for the ET-base materialise method
that turns a lazy expression into a fresh `TaylorExpansionT`.  The
two members are no longer overloaded on the same name:

    auto result = (u * tax::sin(v) + u * v).eval();   // materialise
    result.at({0.1, 0.05});                           // evaluate at a point

The rename touches the static and dynamic factory paths, the Python
binding (now exposed as `DynTE.at(dx)` instead of `DynTE.eval(dx)`),
and the tests / docs that called the old name.

Documentation pass: comments and prose now describe the current
design directly instead of contrasting it with rejected alternatives.
Removed "naive ... we instead", "rather than", "Wrapped in lambdas
because the unified template ..." style framing in:

  - include/tax/expr/view_nodes.hpp file-level comment
  - include/tax/storage/tte.hpp at(dx) docstring
  - python/src/tax_module.cpp factory-block comment
  - docs/architecture.md slice-view section
  - docs/concepts/slice-streaming.md "allocation footprint" section
  - docs/index.md, docs/getting-started.md, docs/guide/arithmetic.md,
    docs/guide/math-functions.md, README.md, CLAUDE.md

Sample snippets adjusted to the natural `auto result = expr.eval();`
form throughout (was `Type result; result = expr.eval();`).

Verification: 47 C++ tests pass under Release / Debug / ASan+UBSan,
24 Python tests pass, `mkdocs build --strict` clean.
Adds the plumbing for producing a Python wheel of the tax bindings on
every push.

  * pyproject.toml — declares scikit-build-core as the build backend,
    pins nanobind, and configures cibuildwheel to produce
    manylinux_2_28 wheels for CPython 3.10, 3.11, 3.12, 3.13.  musllinux
    skipped (the C++23 toolchain story is weaker there).  GCC 13 is
    pulled in via gcc-toolset-13 inside the manylinux container.

  * python/CMakeLists.txt — adds `install(TARGETS _tax LIBRARY
    DESTINATION tax)` so scikit-build-core lands the compiled
    `_tax.so` next to the `tax/__init__.py` wrapper inside the wheel.

  * Top-level CMakeLists.txt — adds an Eigen FetchContent fallback
    used when neither find_package(Eigen3) nor a system include dir
    succeeds.  This is what lets the wheel build inside images that
    don't ship Eigen.

  * .github/workflows/wheels.yml — runs cibuildwheel on Linux and
    uploads the resulting `*.whl` files as a `tax-wheels-*`
    artifact.  A separate job builds and uploads an sdist.  No PyPI
    publishing; wheels are downloadable from the Actions UI / API
    only.

  * .gitignore — ignore dist/, wheelhouse/, *.whl, *.tar.gz.

  * docs/python.md — adds an "Install" section pointing readers at
    the workflow artifact URL plus the `python -m build --wheel`
    flow for local wheel builds.  changelog.md gains an entry.

Verified locally: `python -m build --wheel` produces
`tax-0.2.0-cp311-cp311-linux_x86_64.whl`; `pip install` of that
wheel followed by `import tax` works correctly.
@andreapasquale94
andreapasquale94 deleted the claude/tax-rewrite-ground-up-tfsL6 branch May 23, 2026 08:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants