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) - full autograd support — differentiable with respect to control points and rational weights
- full GPU support with
dtypeanddevicecorrectness - midpoint uniform knot refinement for curves and surfaces (rational and non-rational)
- gaussian and mean curvature for 3D B-spline surfaces
- zero runtime dependencies beyond PyTorch itself
- optional dependency on
OCPfor STEP file export of 3D surfaces
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_basic_surface_3d.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.pyexample_curvatures.pyexport_step.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.
Surface curvatures
K, H = bspline_surface_curvatures(uv, points, *, degree, periodic, clamped)
K, H = bspline_rational_surface_curvatures(uv, points, weights, *, degree, periodic, clamped)
K, H = bspline_surface_grid_curvatures(u, v, points, *, degree, periodic, clamped)
K, H = bspline_rational_surface_grid_curvatures(u, v, points, weights, *, degree, periodic, clamped)
Gaussian (K) and mean (H) curvatures of a B-spline surface in R³ at the
sampled (u, v) points, computed from the first and second fundamental forms.
Restricted to 3D control points (points.shape[-1] == 3). Each degree
component must be >= 2 so that second-order parametric derivatives exist.
STEP export
from torchnodo.ocp_export import bspline_surface_to_ocp_geom, bspline_surface_to_step
surface = bspline_surface_to_ocp_geom(points, *, degree, periodic, clamped)
bspline_surface_to_step(filepath, points, *, degree, periodic, clamped)
Convert a 3D B-spline surface to OCP's Geom_BSplineSurface or write it
directly to a STEP file (ISO 10303). Restricted to 3D control points
(points.shape[-1] == 3), non-rational only.
bspline_surface_to_ocp_geomreturns anOCP.Geom.Geom_BSplineSurface, which can be composed with the rest of the OpenCascade Python API (trimming, sewing, boolean operations, custom STEP writers, etc.).bspline_surface_to_stepis a thin convenience wrapper that wraps the surface as a face and writes the resulting shape to a.stepfile.
These entry points live in the torchnodo.ocp_export submodule because they
depend on OCP (Python bindings for
OpenCascade), which is not a runtime dependency of torchnodo. Install
it with the ocp extra:
pip install torchnodo[ocp]
Periodic surfaces are exported by first unwrapping the pole grid via
unwrap_bspline_surface into the equivalent
non-periodic representation. The resulting STEP face is geometrically
identical but is not tagged as periodic at the STEP level.
See examples/export_step.py for a runnable demo.
License
MIT License.
Project details
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.3.0.tar.gz.
File metadata
- Download URL: torchnodo-1.3.0.tar.gz
- Upload date:
- Size: 16.5 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 |
068de65167c3a4b390b29070a2418ec333af23c0682efa7497e70961e71703a2
|
|
| MD5 |
b9f0901a7daadda275451236b9488c2a
|
|
| BLAKE2b-256 |
89838fb34720737df538cd8e72f70673e493866433f6d7951e5f1f94a36d36cc
|
File details
Details for the file torchnodo-1.3.0-py3-none-any.whl.
File metadata
- Download URL: torchnodo-1.3.0-py3-none-any.whl
- Upload date:
- Size: 24.6 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 |
71c301aac279e8f1472667f43f65a3983202dae399bc64f5c753666d778b784c
|
|
| MD5 |
94d1b7c299e21fae260004481a0a4d52
|
|
| BLAKE2b-256 |
7242a3118e8ceeeb0ac1a8cf8a3d311a3fdccf6ae8c4700689190b7dd790ed97
|