Metadata-Version: 2.4
Name: torchnodo
Version: 1.1.0
Summary: Uniform (Rational) B-Splines in PyTorch
Author: Victor Poughon
Author-email: Victor Poughon <victor.poughon@gmail.com>
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.11
Project-URL: homepage, https://codeberg.org/victorpoughon/torchnodo
Project-URL: repository, https://codeberg.org/victorpoughon/torchnodo
Description-Content-Type: text/markdown

# torchnodo

![PyPI - Version](https://img.shields.io/pypi/v/torchnodo)
![PyPI - License](https://img.shields.io/pypi/l/torchnodo)

**torchnodo** is an implementation of uniform (rational) B-splines in PyTorch.
It provides a small, purely functional API for evaluating B-spline curves and
surfaces and their parametric derivatives, with full autograd and GPU support.

<video autoplay="autoplay" src="/attachments/a4a0db4a-85aa-4cad-bd91-8388a91e5cbb" title="curve_2d_animation" controls loop></video>

<hr>

<video autoplay="autoplay" src="/attachments/ff229de3-8f09-4c7c-be72-0e8e656873c6" title="surface_1d_animation" controls loop></video>

## Features

* control points for curves and surfaces of arbitrary dimension (not limited to 2D/3D)
* arbitrary B-spline polynomial degree `P`
* analytical parametric differentiation or any order `D ≤ P`
* periodic and non-periodic support, with clamped and unclamped knot vectors
* rational variants (weighted control points) for both curves and surfaces
* optimized surface evaluation on a regular `U × V` grid (`bspline_surface_grid`) and on scattered `(u, v)` samples (`bspline_surface`)
* midpoint uniform knot refinement for curves and surfaces (rational and non-rational)
* full autograd support — differentiable with respect to control points and rational weights
* full GPU support with `dtype` and `device` correctness
* zero runtime dependencies beyond PyTorch itself

Out of scope:

- non-uniform knots (not a NURBS implementation in the general sense)
- degree elevation
- explicit surface-of-revolution, swept surface, or other higher-level constructors
- B-splines volumes or higher order manifolds

## Examples

### A 2D B-Spline curve

```python
import matplotlib.pyplot as plt
import torch

from torchnodo import bspline_curve

# Evaluate a 2D curve of degree 3 with 5 random control points
control_points = torch.rand(5, 2)
curve = bspline_curve(
    u=torch.linspace(0, 1, 200),
    points=control_points,
    degree=3,
    order=0,
    periodic=False,
    clamped=True,
)

# Plot the curve value (0-th order derivative) and its control polygon
plt.plot(curve[0, :, 0], curve[0, :, 1])
plt.plot(control_points[:, 0], control_points[:, 1], "o--", alpha=0.4)
plt.show()
```

![basic_curve](./doc/basic_curve.png)

### A 1D B-Spline surface

```python
import matplotlib.pyplot as plt
import torch

from torchnodo import bspline_surface_grid

# Evaluate a 1D surface of degree (3, 2) with 5x4 random control points
u = torch.linspace(0, 1, 60)
v = torch.linspace(0, 1, 60)
surface = bspline_surface_grid(
    u,
    v,
    points=torch.rand(5, 4, 1),
    degree=(3, 2),
    order=(0, 0),
    periodic=(False, False),
    clamped=(True, True),
)

z = surface[0, 0, :, :, 0]

U, V = torch.meshgrid(u, v, indexing="ij")

# Plot the surface over its U x V parametric grid
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.plot_surface(U, V, z, cmap="cividis")
ax.set(xlabel="U", ylabel="V", zlabel="Z")
plt.show()
```

![basic_surface](./doc/basic_surface.png)

### Browse all examples

All runnable examples live in the [`examples/`](examples/) directory:

* [`example_basic_curve.py`](examples/example_basic_curve.py)
* [`example_basic_surface.py`](examples/example_basic_surface.py)
* [`example_basis_functions.py`](examples/example_basis_functions.py)
* [`example_fit_curve.py`](examples/example_fit_curve.py)
* [`example_fit_surface.py`](examples/example_fit_surface.py)
* [`example_refine_curve.py`](examples/example_refine_curve.py)
* [`example_refine_surface.py`](examples/example_refine_surface.py)
* [`example_curve_2d_animation.py`](examples/example_curve_2d_animation.py)
* [`example_surface_1d_animation.py`](examples/example_surface_1d_animation.py)
* [`example_surface_3d_animation.py`](examples/example_surface_3d_animation.py)

## Installation

Install with `pip`:

```bash
pip install torchnodo
```

Or, in a `uv` project:

```bash
uv add torchnodo
```

> ⚠️ **PyTorch is not declared as a dependency.** torchnodo requires PyTorch at
runtime, but the `pyproject.toml` of **torchnodo** intentionally does not list
`torch` so that you can install the variant of PyTorch you want (CPU-only, CUDA,
ROCm, etc.) without interference.

## Design choices

* **Purely functional API**

There are no classes, no state, and no mutation. Every public entry point is a
free function that takes control points and configuration and returns tensors.
This keeps the API composable with `torch.nn.Module`, autograd, `torch.compile`,
and functional transforms without wrapping.

* **Almost loop-free code**

B-spline evaluation is expressed in terms of tensor operations and runs a
batched de Boor-style recursion. The only Python-level loops are over B-spline
degree `P` and parametric derivative order `D`, both of which are *static* —
they are fixed at call time and typically small (≤ 5). There is no Python-level
loop over parameter values or control points.

* **Arbitrary control-point dimension**

The trailing `C` axis of `points` tensors is a pure "batch of coordinates" and
is never inspected. Typical uses are 2D or 3D control points for curves and 3D
control points for surfaces, but any `C ≥ 1` is supported.

* **Joint evaluation of values and parametric derivatives**

The "value" of a function is really its zero-th order derivative. So when
evaluating a spline, request the number of parametric derivatives you need with
the `order=` argument. The API returns a tensor of shape:

> ( order of parametric derivation, parametric samples, dimension of control points )

which in practice translates to:

| Function               | Output tensor shape                     |
| ---------------------- | --------------------------------------- |
| `bspline_curve`        | `(order+1, U, C)`                       |
| `bspline_surface_grid` | `(order[0] + 1, order[1] + 1, U, V, C)` |
| `bspline_surface`      | `(order[0] + 1, order[1] + 1, UV, C)`   |

* **Uniform knots only**

Knots vectors are either uniform clamped or uniform unclamped. They are never
stored as tensors and remain implicit in the code.

* **Normal vs grid surface**

Two surface evaluators are provided:

1. `bspline_surface_grid(u, v, points, ...)` evaluates on the full Cartesian
  product `u × v`. This is the fast path for rendering, plotting, or any dense
  grid use case: basis functions in `u` and `v` are computed independently and
  combined with a single `einsum`.
2. `bspline_surface(uv, points, ...)` evaluates on arbitrary scattered `(u, v)`
  pairs. Use it when surface samples are not on a grid.

### Nomenclature

* **degree** (`P`, `Q`): the polynomial degree of the B-spline.
* **order** (`D`, `E`): the *parametric* derivative order. Unrelated to spline
  order in some textbooks (which use "order" to mean `degree + 1`).

## API

### Evaluation

```python
curve = bspline_curve(u, points, *, degree, order, periodic, clamped)
curve = bspline_rational_curve(u, points, weights, *, degree, order, periodic, clamped)

surface = bspline_surface(uv, points, *, degree, order, periodic, clamped)
surface = bspline_rational_surface(uv, points, weights, *, degree, order, periodic, clamped)

surface = bspline_surface_grid(u, v, points, *, degree, order, periodic, clamped)
surface = bspline_rational_surface_grid(u, v, points, weights, *, degree, order, periodic, clamped)
```

**Common arguments:**

* `u` / `v` / `uv`: parameter values in `[0, 1]`. For curves, `u` is shape
  `(U,)`. For scattered surface evaluation, `uv` is shape `(UV, 2)`. For grid
  surface evaluation, `u` and `v` are independent 1D tensors.
* `points`: control points.
  * curves: shape `(K, C)`
  * surfaces: shape `(K, L, C)`
* `weights` (rational variants only): positive weights with shape `(K,)` for
  curves and `(K, L)` for surfaces.
* `degree`: polynomial degree. For curves, an `int`. For surfaces, a `tuple[int,
  int]` of `(P, Q)`.
* `order`: highest parametric derivative order to compute. For curves, an `int`
  in `[0, P]`. For surfaces, a `tuple[int, int]` with each component in `[0, P]`
  / `[0, Q]`.
* `periodic`: whether the curve/surface is periodic. For surfaces, a
  `tuple[bool, bool]` — the two parametric axes are independent, so surfaces can
  be periodic in `u` only, `v` only, both (torus-like), or neither.
* `clamped`: whether the knot vector is clamped (boundary knots repeated `P`
  times so the curve passes through the first and last control point) or
  unclamped (uniformly extended on both sides). For surfaces, a `tuple[bool,
  bool]`.

**Typical `periodic` / `clamped` combinations**

| curve type          | periodic | clamped |
| ------------------- | -------- | ------- |
| open, interpolating | `False`  | `True`  |
| open, "floating"    | `False`  | `False` |
| closed loop         | `True`   | `False` |

`periodic=True, clamped=True` works but is not a very natural configuration.

### Control points refinement

```python
points = refine_curve_points(points, degree, *, periodic, clamped)
points, weights = refine_rational_curve_points(points, weights, degree, *, periodic, clamped)

points = refine_surface_points(points, degree, *, periodic, clamped)
points, weights = refine_rational_surface_points(points, weights, degree, *, periodic, clamped)
```

Each refinement call inserts one knot at the midpoint of every inner knot span,
along every parametric axis. The returned control points define a curve/surface
that is *geometrically identical* to the original; only the control polygon /
control grid densifies.

Midpoint refinement requires an unclamped knot vector.

![curve_refinement](./doc/curve_refinement.png)

<hr>

![surface_refinement](./doc/surface_refinement.png)

## License

MIT License.
