Uniform (Rational) B-Splines in PyTorch
Project description
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.
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 × Vgrid (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
dtypeanddevicecorrectness - 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
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()
A 1D B-Spline surface
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()
Browse all examples
All runnable examples live in the examples/ directory:
example_basic_curve.pyexample_basic_surface.pyexample_basis_functions.pyexample_fit_curve.pyexample_fit_surface.pyexample_refine_curve.pyexample_refine_surface.pyexample_curve_2d_animation.pyexample_surface_1d_animation.pyexample_surface_3d_animation.py
Installation
Install with pip:
pip install torchnodo
Or, in a uv project:
uv add torchnodo
⚠️ PyTorch is not declared as a dependency. torchnodo requires PyTorch at runtime, but the
pyproject.tomlof torchnodo intentionally does not listtorchso 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:
bspline_surface_grid(u, v, points, ...)evaluates on the full Cartesian productu × v. This is the fast path for rendering, plotting, or any dense grid use case: basis functions inuandvare computed independently and combined with a singleeinsum.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 meandegree + 1).
API
Evaluation
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,uis shape(U,). For scattered surface evaluation,uvis shape(UV, 2). For grid surface evaluation,uandvare independent 1D tensors.points: control points.- curves: shape
(K, C) - surfaces: shape
(K, L, C)
- curves: shape
weights(rational variants only): positive weights with shape(K,)for curves and(K, L)for surfaces.degree: polynomial degree. For curves, anint. For surfaces, atuple[int, int]of(P, Q).order: highest parametric derivative order to compute. For curves, anintin[0, P]. For surfaces, atuple[int, int]with each component in[0, P]/[0, Q].periodic: whether the curve/surface is periodic. For surfaces, atuple[bool, bool]— the two parametric axes are independent, so surfaces can be periodic inuonly,vonly, both (torus-like), or neither.clamped: whether the knot vector is clamped (boundary knots repeatedPtimes so the curve passes through the first and last control point) or unclamped (uniformly extended on both sides). For surfaces, atuple[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
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.
License
MIT License.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file torchnodo-1.1.0.tar.gz.
File metadata
- Download URL: torchnodo-1.1.0.tar.gz
- Upload date:
- Size: 13.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Fedora Linux","version":"43","id":"","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f8c05c6458c66708c427dd59a99894775696e5868edc4e38417ed6f033f0ab10
|
|
| MD5 |
bbb98d92730afed013290b602c398cb8
|
|
| BLAKE2b-256 |
681e807a6dc344ea92586cfab906d2511c62d196bd61658c4173759b78be4d4d
|
File details
Details for the file torchnodo-1.1.0-py3-none-any.whl.
File metadata
- Download URL: torchnodo-1.1.0-py3-none-any.whl
- Upload date:
- Size: 20.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Fedora Linux","version":"43","id":"","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1f1d2c9bd9f3d34aa83e460f7f369653bb5054f9453c9cd33de065447485f4f2
|
|
| MD5 |
98d3d7f93d9a82e45f01aa24c13f20df
|
|
| BLAKE2b-256 |
496c9b8835b718e4c45a3002ffc51ce8af0474f822efbe4b2c4438fe9b7850b4
|