Rewrite tax from the ground up with a slice-streamed ET architecture - #15
Closed
andreapasquale94 wants to merge 12 commits into
Closed
Rewrite tax from the ground up with a slice-streamed ET architecture#15andreapasquale94 wants to merge 12 commits into
andreapasquale94 wants to merge 12 commits into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.