Python API reference

The Python reference describes the public objects and their arguments. Begin with Quickstart for a complete fit, or Advanced Topics for practical choices and interpretation. The sections below supply exact signatures once you know which operation you need.

Most workflows start with BezierSpace, Samples and fit. Use Points when cell identity must travel with coordinates, and use the returned FitResult.model for prediction. The common types live in torch_bsf.core and are also available from the package root.

For numerical methods, consult fitting and solvers for regression, or optimization for differentiable costs. Sampling, validation and active learning build on the same space and model. Plotting, scikit-learn, Lightning and MLflow integrations are optional; install the relevant extra before executing their examples.

geometry describes mutable topology construction, while a compiled BezierSpace records immutable geometry and coefficient layout. The archive interface in io preserves that layout with model coefficients. Private modules beginning with an underscore are implementation details.

core

BezierSpace defines the domain and global control layout. Points carries coordinates and cell IDs, Samples adds observed values and weights, and BezierModel adds output coefficients to a space. Geometry does not depend on output count. Use these types to make data alignment and model state explicit.

Points, Samples and BezierModel preserve floating tensor and NumPy array dtypes. Python float lists use torch.get_default_dtype() (normally float32). To retain double-precision coordinates, observations and coefficients, construct float64 tensors or NumPy arrays before passing them in; converting an already rounded float32 tensor cannot recover precision. Integer coordinates and coefficients become float64; on MPS, these integer inputs move to CPU for that promotion because MPS does not support float64. Integer observations remain integers. Samples weights are stored as CPU float64 tensors. Device inputs converted to CPU float64 move to CPU before changing dtype, preserving coordinate values and their autograd connection.

The constructor contracts and executable examples below use n_samples for the number of aligned rows, coordinate_width for the number of coordinate columns, and n_outputs for the number of response columns. See Points and Samples for shapes, defaults and validation. Their from_cells methods pack local rows of different widths; from_cells() shows the shared order of coordinates, observations and weights. These helpers convert nonempty coordinate batches and observations to CPU float64. forward() describes single-cell tensor inputs, explicit multi-cell Points and the prediction shape.

FixedControls stores CPU float64 values and reads non-tensor floating inputs directly as float64, so FixedControls({0: [0.2]}) preserves the Python float value independently of the default dtype. Existing tensor values are converted with their original precision. Complex fixed values are rejected.

Spaces, observations and differentiable models shared by every fitting backend.

class torch_bsf.core.BasisOperator(space: BezierSpace, points: Points, *, _groups: list[tuple[int, Tensor]] | None = None)[source]

Bases: object

Cell-local basis supporting products, transpose products and explicit export.

matmat(coefficients: Tensor) → Tensor[source]

Evaluate a coefficient matrix, accumulating only local contributions.

rmatmat(values: Tensor) → Tensor[source]

Apply the transpose, summing repeated shared coefficient columns.

to_dense() → Tensor[source]

Explicitly materialize the full design matrix, for a dense solve.

class torch_bsf.core.BezierModel(space: BezierSpace, coefficients: Any)[source]

Bases: Module

A vector-valued differentiable map on a compiled BezierSpace.

Parameters:
  • space – Geometry, degrees and coefficient ordering.

  • coefficients – Matrix of shape (space.n_controls, n_outputs). Copied on construction. Floating tensors and NumPy arrays retain their dtype. Python float lists use torch.get_default_dtype(); integer inputs become float64 (on CPU for integer MPS tensors). Pass float64 tensors or arrays for double precision.

Examples

>>> import torch
>>> from torch_bsf import BezierSpace, BezierModel
>>> model = BezierModel(BezierSpace.simplex(1, degree=1), torch.tensor([[1.], [3.]]))
>>> model(torch.tensor([[0.5, 0.5]])).item()
2.0
copy() → BezierModel[source]

Copy coefficients without mutating or duplicating immutable geometry.

forward(points: Points | Tensor) → Tensor[source]

Evaluate points in their original row order.

Parameters:

points (Points or torch.Tensor) – Barycentric coordinates. A raw tensor of shape (n_samples, space.width) is accepted only when the space has one addressable cell; all rows then belong to cell zero. Multi-cell spaces require Points with explicit IDs, including when every requested row belongs to the same cell. Active coordinates must be finite barycentric blocks; extra padding in mixed-cell Points is ignored.

Returns:

Predictions in input row order, on the coefficient dtype and device. Input coordinates are copied to that dtype and device. Evaluation is differentiable in coordinates and coefficients.

Return type:

torch.Tensor, shape (n_samples, n_outputs)

Raises:

ValueError – If raw coordinates are used with a multi-cell space, Points construction fails, IDs are outside the space, coordinate width is invalid, or active coordinates fail barycentric validation.

Examples

>>> import torch
>>> from torch_bsf import BezierModel, BezierSpace, Points, SimploidalSet
>>> model = BezierModel(BezierSpace.simplex(1, degree=1), [[1.0], [3.0]])
>>> tuple(model(torch.tensor([[0.5, 0.5], [0.25, 0.75]])).shape)
(2, 1)
>>> domain = SimploidalSet()
>>> edge = domain.add_simplex(1)
>>> space = BezierSpace.on_set(domain, degree=1)
>>> model = BezierModel(space, torch.ones(space.n_controls, 2))
>>> points = Points.from_cells(space, [(edge, [[0.5, 0.5]])])
>>> model(points).tolist()
[[1.0, 1.0]]
>>> model(torch.tensor([[0.5, 0.5]]))
Traceback (most recent call last):
    ...
ValueError: multi-cell models require explicit Points with cell IDs.
property n_outputs: int[source]

Dimension of each predicted value.

class torch_bsf.core.BezierSpace(cells: tuple[~torch_bsf.core.Cell, ...], n_controls: int, main_cells: tuple[int, ...], _edges: tuple[tuple[int, int], ...], _source_ids: ~collections.abc.Mapping[~torch_bsf.geometry.Simploid, int] = <factory>, _topology: tuple[~typing.Any, ...] = ())[source]

Bases: object

Immutable polynomial space, independent of learned values and output dimension.

Construct a simplex with simplex(), a product with simploid(), or compile a mutable topology builder with on_set(). Dimensions, degrees and control multi-indices accept Python and NumPy integers; booleans, strings and floats (including integer-valued floats) raise ValueError. Factor dimensions are positive; simplex() also accepts dimension zero. Single-cell degrees are non-negative; shared-space degrees are positive.

Examples

>>> from torch_bsf import BezierSpace
>>> space = BezierSpace.simplex(dimension=2, degree=3)
>>> space.n_controls
10
adjacency() → Tensor[source]

Return a copy of the undirected control-net edges.

basis(points: Points | Tensor) → BasisOperator[source]

Build a cell-local linear operator without a global dense allocation.

cell_id(cell: int | Simploid) → int[source]

Resolve an integer or an original builder handle to its frozen cell ID.

compatible(other: BezierSpace) → bool[source]

Whether coefficient ordering and all addressable cells agree.

control_index(index: Sequence[Any], *, cell: int | Simploid = 0) → int[source]

Resolve a cell-local multi-index to a globally shared coefficient row.

property n_cells: int[source]

Number of addressable non-degenerate cells, including faces.

classmethod on_set(domain: SimploidalSet, *, degree: int | Mapping[Simploid, int | Sequence[int]], default_degree: int | None = None) → BezierSpace[source]

Snapshot a simploidal set, propagating compatible degrees across shared faces.

The builder’s cell handles remain usable for addressing this snapshot; subsequent changes to the builder cannot change its basis or layout.

classmethod simplex(dimension: int, *, degree: int) → BezierSpace[source]

Create a single simplex, including a zero-dimensional constant.

classmethod simploid(dimensions: Sequence[int], *, degree: int | Sequence[int]) → BezierSpace[source]

Create a tensor product of simplices with one degree per factor.

validate(points: Points) → None[source]

Check IDs, finite coordinates and simplex membership of each factor.

property width: int[source]

Width used when packing mixed-cell coordinate batches.

with_degree(degree: int | Sequence[int]) → BezierSpace[source]

Recompile the same frozen domain at another degree for model selection.

class torch_bsf.core.Cell(dimensions: tuple[int, ...], degree: tuple[int, ...], controls: tuple[int, ...])[source]

Bases: object

A compiled cell’s factor dimensions, degrees and global coefficient rows.

property width: int[source]

Number of concatenated barycentric coordinates.

class torch_bsf.core.FixedControls(values: Mapping[int, Any])[source]

Bases: object

Explicit fixed values keyed by globally shared coefficient row.

Values are copied to CPU float64 tensors. Non-tensor floating inputs are read directly as float64, independently of PyTorch’s default dtype; tensor inputs retain their existing precision before conversion. Complex inputs are rejected.

Examples

>>> from torch_bsf import FixedControls
>>> FixedControls({0: [0.2]}).values[0].item()
0.2
classmethod from_cells(space: BezierSpace, values: Sequence[tuple[int | Simploid, Sequence[Any], Any]]) → FixedControls[source]

Resolve local references and reject contradictory shared values.

class torch_bsf.core.Points(parameters: Any, cell_ids: Any = None)[source]

Bases: object

Barycentric coordinate rows and their cell IDs, in prediction order.

Parameters:
  • parameters (array-like, shape (n_samples, coordinate_width)) – Real coordinate rows. For a simplex of dimension d, the width is d + 1; for a product, concatenate one barycentric block per factor. Mixed-cell rows may be padded on the right to space.width. Coordinates are copied on their input device, except integer MPS coordinates, which move to CPU for float64 promotion.

  • cell_ids (array-like of int, shape (n_samples,), optional) – One integer cell ID per row. Defaults to all zeros on the coordinate device. IDs are copied as torch.long on that device.

Raises:

ValueError – If coordinates are complex or not two-dimensional, or IDs are not a matching one-dimensional integer vector (booleans are rejected).

Notes

Floating tensors and NumPy arrays retain their dtype. Python float lists use torch.get_default_dtype(); integer coordinates become float64. Pass float64 tensors or arrays to preserve double-precision coordinates. Construction checks shapes and types only. Cell membership, finite active coordinates and barycentric blocks are checked by space.validate, basis construction or model evaluation. Inactive padding is ignored.

Examples

>>> from torch_bsf import BezierSpace, Points
>>> points = Points([[1.0, 0.0], [0.25, 0.75]])
>>> tuple(points.parameters.shape), points.cell_ids.tolist()
((2, 2), [0, 0])
>>> BezierSpace.simplex(1, degree=1).validate(points)
classmethod from_cells(space: BezierSpace, data: Sequence[tuple[int | Simploid, Any]]) → Points[source]

Pack ordered cell-local coordinates without manual padding.

Parameters:
  • space (BezierSpace) – Compiled space used to resolve cell references and validate rows.

  • data (sequence of (cell, parameters) pairs) – Each cell is an integer ID or an original builder handle. Its coordinates have shape (n_cell_samples, cell.width). Entries and rows within each entry retain their supplied order; repeated cell references are allowed.

Returns:

Coordinates of shape (n_samples, space.width), where n_samples is the sum of entry row counts. Nonempty input is converted to CPU float64 and zero-padded on the right. IDs are CPU torch.long. An empty sequence returns zero rows with coordinates in PyTorch’s default floating dtype on its default device.

Return type:

Points

Raises:

ValueError – If a cell reference is invalid, a coordinate batch has the wrong shape or local width, or its coordinates are not finite valid barycentric blocks for that cell.

Notes

Inputs first follow Points dtype inference, then convert to float64; this cannot recover precision already lost to float32. See Samples.from_cells() for a mixed-width packing example with aligned observations and weights.

Examples

>>> from torch_bsf import BezierSpace, Points
>>> space = BezierSpace.simplex(1, degree=1)
>>> points = Points.from_cells(space, [(0, [[0.25, 0.75], [1.0, 0.0]])])
>>> points.parameters.tolist(), points.cell_ids.tolist()
([[0.25, 0.75], [1.0, 0.0]], [0, 0])
>>> str(points.parameters.dtype), str(points.parameters.device)
('torch.float64', 'cpu')
take(indices: Tensor) → Points[source]

Select rows while preserving their cell identities.

to(*, device: device | str = 'cpu', dtype: dtype = torch.float64) → Points[source]

Copy coordinates to an explicit compute dtype and device.

class torch_bsf.core.QuadraticPenalty(ridge: float = 0.0, smoothness: float = 0.0)[source]

Bases: object

Weights of squared coefficients and adjacent coefficient differences.

class torch_bsf.core.Samples(parameters: Any, values: Any, *, cell_ids: Any = None, weights: Any = None)[source]

Bases: object

Points with finite observed outputs and optional nonnegative weights.

Parameters:
  • parameters (Points or array-like, shape (n_samples, coordinate_width)) – Existing points, retained as supplied, or coordinates used to build Points. Raw coordinates and cell IDs are copied by Points.

  • values (array-like, shape (n_samples, n_outputs) or (n_samples,)) – Real, finite observations in point row order. One-dimensional values become one output column. Both n_samples and n_outputs must be positive. Values are copied and retain their input device.

  • cell_ids (array-like of int, shape (n_samples,), optional) – IDs for raw coordinates; defaults to all zeros. Must be omitted when parameters is already a Points instance.

  • weights (array-like, shape (n_samples,), optional) – Observation weights in the same row order, copied to CPU float64. Defaults to all ones. Entries must be finite and nonnegative, and their sum must be finite and strictly positive. Individual zeros are allowed, but all observations must still be finite, including observations with zero weight.

Raises:

ValueError – If both Points and separate IDs are supplied, values are complex, nonfinite, empty or have a mismatched shape, or weights violate the shape, finiteness or positivity requirements. Raw coordinates also follow the construction checks of Points.

Notes

Coordinates follow Points. Observed values use torch.as_tensor dtype inference: tensors and NumPy arrays retain their dtype, Python float lists use torch.get_default_dtype(), and integer values remain integers. For double precision, pass float64 tensors or arrays. Weights are stored as CPU float64 tensors. As with direct Points construction, space membership is checked when the points are validated or used with a space, rather than here.

Examples

>>> from torch_bsf import Points, Samples
>>> points = Points([[1.0, 0.0], [0.0, 1.0]])
>>> samples = Samples(points, [2.0, 4.0], weights=[0.0, 1.0])
>>> tuple(samples.values.shape), samples.weights.tolist()
((2, 1), [0.0, 1.0])
>>> Samples(points, [2.0, 4.0], cell_ids=[0, 0])
Traceback (most recent call last):
    ...
ValueError: cell_ids are already specified by Points.
>>> Samples(points, [float("nan"), 4.0], weights=[0.0, 1.0])
Traceback (most recent call last):
    ...
ValueError: values must be finite with shape (n_samples, n_outputs), both positive.
classmethod from_cells(space: BezierSpace, data: Sequence[tuple[int | Simploid, Any, Any]], *, weights: Any = None) → Samples[source]

Pack ordered cell-local observations with different coordinate widths.

Parameters:
  • space (BezierSpace) – Compiled space used to resolve cell references and validate rows.

  • data (sequence of (cell, parameters, values) triples) – Nonempty sequence of cell IDs or original builder handles, coordinates of shape (n_cell_samples, cell.width), and corresponding finite observations of shape (n_cell_samples, n_outputs) or (n_cell_samples,). Each entry must contain at least one row and all entries must have the same n_outputs.

  • weights (array-like, shape (n_samples,), optional) – One flat vector for all concatenated rows, in entry order then row order within each entry. Defaults to ones; follows the finite, nonnegative, positive-sum contract of Samples.

Returns:

Coordinates of shape (n_samples, space.width) and observations of shape (n_samples, n_outputs), both on CPU in float64. Narrower coordinates have zero padding on the right. Cell IDs, values and weights follow the same concatenation order.

Return type:

Samples

Raises:
  • ValueError – If data is empty, cell references or local coordinates are invalid, an entry has invalid observations, or weights violate the Samples contract.

  • RuntimeError – If entries have inconsistent output widths, so their observed values cannot be concatenated.

Notes

Unlike direct construction, this helper converts coordinates and observations to CPU float64 after their usual dtype inference. Pass float64 inputs to avoid first rounding Python float lists to the default dtype. Outputs and weights are copied.

Examples

Pack a triangle row before two edge rows. The edge needs two active coordinates; its third column is padding. Weights follow this order, regardless of the numeric cell IDs.

>>> from torch_bsf import BezierSpace, Samples, SimploidalSet
>>> domain = SimploidalSet()
>>> edge, triangle = domain.add_simplex(1), domain.add_simplex(2)
>>> space = BezierSpace.on_set(domain, degree=1)
>>> samples = Samples.from_cells(space, [
...     (triangle, [[0.25, 0.25, 0.5]], [10.0]),
...     (edge, [[1.0, 0.0], [0.0, 1.0]], [20.0, 30.0]),
... ], weights=[2.0, 0.0, 3.0])
>>> samples.points.parameters.tolist()
[[0.25, 0.25, 0.5], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
>>> samples.points.cell_ids.tolist() == [space.cell_id(triangle), space.cell_id(edge), space.cell_id(edge)]
True
>>> tuple(samples.values.shape), samples.values[:, 0].tolist(), samples.weights.tolist()
((3, 1), [10.0, 20.0, 30.0], [2.0, 0.0, 3.0])
>>> str(samples.points.parameters.dtype), str(samples.values.dtype), str(samples.values.device)
('torch.float64', 'torch.float64', 'cpu')
take(indices: Tensor) → Samples[source]

Select observations, keeping coordinates, outputs and weights aligned.

geometry

Build an assembled domain with SimploidalSet and identify the faces that must share coefficients. Compile it into BezierSpace before fitting. Builder mutations do not alter an existing compiled space. See Single and assembled domains for chart orientation and continuity.

Topology builders for simplex products and their glued parameter domains.

Compile a builder with BezierSpace.on_set before fitting. A compiled space snapshots the topology; subsequent builder mutations cannot change its basis.

class torch_bsf.geometry.Simploid(simploid_type: Sequence[int])[source]

Bases: object

A simploid of a SimploidalSet.

Simploids are created by the construction methods of SimploidalSet, not directly. A non-degenerate simploid stores its faces explicitly. A degenerate simploid is stored in canonical form, as a non-degenerate base simploid together with a DegeneracyMap, so that two equal degenerate simploids are the same object.

Examples

>>> from torch_bsf.geometry import SimploidalSet
>>> S = SimploidalSet()
>>> prism = S.add_simploid((2, 1))
>>> prism.simploid_type, prism.dim
((2, 1), 3)
>>> prism.face(1, 0).simploid_type  # a triangle
(2,)
>>> prism.face(0, 0).simploid_type  # a square
(1, 1)
>>> e = prism.face(1, 0).face(0, 0)  # an edge
>>> e.degeneracy(0, 0).is_degenerate
True
>>> e.degeneracy(0, 0).face(0, 0) is e
True
property base: Simploid[source]

The non-degenerate simploid this one is degenerate from, or itself if it is not degenerate.

degeneracy(i: int, j: int) → Simploid[source]

The degenerate simploid \(s^{i+1}_j\) (j >= 0) or \(s^i_{-1}\) (j == -1).

Parameters:
  • i – The factor index: 0 <= i < length if j >= 0, 0 <= i <= length if j == -1.

  • j – The vertex of factor i to repeat, or -1 to insert a degenerate edge as the new factor i.

Return type:

The degenerate simploid.

property degeneracy_map: tuple[tuple[int, tuple[int, ...]], ...][source]

The degeneracy map from base to this simploid (the identity map if not degenerate).

property dim: int[source]

The dimension \(\sum_i a_i\).

face(i: int, j: int) → Simploid[source]

The face \(d^{i+1}_j\): removes vertex j of factor i.

Parameters:
  • i – The factor index, 0 <= i < length.

  • j – The vertex index, 0 <= j <= a_i.

Return type:

The face.

faces() → Iterator[tuple[tuple[int, int], Simploid]][source]

Iterates ((i, j), face(i, j)) over all faces.

property is_degenerate: bool[source]

Whether this simploid is degenerate.

property is_vertex: bool[source]

Whether this is a vertex, i.e., its type is ().

property length: int[source]

The length \(n\) of the type.

property simploid_type: tuple[int, ...][source]

The type \((a_1, \ldots, a_n)\).

vertices() → list[Simploid][source]

The distinct vertices of this simploid.

class torch_bsf.geometry.SimploidalSet[source]

Bases: object

A simploidal set [PL18].

A simploidal set is a set of simploids equipped with face and degeneracy operators satisfying the commutation relations of Definition 1 of the paper. Only non-degenerate simploids are stored; degenerate simploids are created on demand by Simploid.degeneracy() and by the faces of degenerate simploids.

Simploidal sets are built with the basic operations of the paper:

Examples

A tetrahedron and a cube connected by a prism (Figure 1(c) of the paper):

>>> from torch_bsf.geometry import SimploidalSet
>>> S = SimploidalSet()
>>> tetra = S.add_simplex(3)
>>> prism = S.add_simploid((2, 1))
>>> cube = S.add_simploid((1, 1, 1))
>>> _ = S.identify(tetra.face(0, 0), prism.face(1, 0))
>>> _ = S.identify(prism.face(0, 0), cube.face(0, 0))
>>> S.check()
>>> [s.simploid_type for s in S.main_simploids()]
[(3,), (2, 1), (1, 1, 1)]
>>> len(S.simploids(0))  # 4 + 6 + 8 vertices, minus 3 and 4 identified ones
11
add_simplex(dim: int) → Simploid[source]

Adds a complete simplex (a simploid of type \((\mathrm{dim})\)) with its boundary.

Face face(0, j) of a simplex \((v_0, \ldots, v_k)\) is \((v_0, \ldots, \hat v_j, \ldots, v_k)\).

Parameters:

dim – The dimension of the simplex.

Return type:

The simplex.

Examples

>>> from torch_bsf.geometry import SimploidalSet
>>> S = SimploidalSet()
>>> tri = S.add_simplex(2)
>>> len(S), len(tri.vertices())
(7, 3)
add_simploid(simploid_type: Sequence[int]) → Simploid[source]

Adds a complete simploid of the given type with its boundary.

It is built as the cartesian product of complete simplices.

Parameters:

simploid_type – The type \((a_1, \ldots, a_n)\).

Return type:

The simploid.

Examples

>>> from torch_bsf.geometry import SimploidalSet
>>> S = SimploidalSet()
>>> square = S.add_simploid((1, 1))
>>> len(S.simploids(0)), len(S.simploids(1)), len(S.simploids(2))
(4, 4, 1)
add_vertex() → Simploid[source]

Adds a new vertex.

Return type:

The vertex.

betti_numbers(field: Literal['Q', 'Z2'] = 'Q') → list[int][source]

The Betti numbers \(\beta_0, \ldots, \beta_{\mathrm{dim}}\) of the simploidal set.

They are the dimensions of the homology groups of the normalized chain complex (see boundary_matrix()), \(\beta_k = \dim C_k - \operatorname{rank} \partial_k - \operatorname{rank} \partial_{k+1}\). The ranks are computed exactly by integer Gaussian elimination, so the result does not depend on a floating-point tolerance.

Parameters:

field – The coefficient field: "Q" (rational numbers) or "Z2" (integers modulo 2). They differ only when the integral homology has torsion, e.g., for non-orientable spaces.

Return type:

The Betti numbers, one per dimension from 0 to dim (empty if the set is empty).

Raises:

ValueError – If field is neither "Q" nor "Z2".

Examples

A torus, a square whose opposite edges are identified:

>>> from torch_bsf.geometry import SimploidalSet
>>> S = SimploidalSet()
>>> square = S.add_simploid((1, 1))
>>> _ = S.identify(square.face(0, 0), square.face(0, 1))
>>> _ = S.identify(square.face(1, 0), square.face(1, 1))
>>> S.betti_numbers()
[1, 2, 1]
boundary_matrix(dim: int) → Tensor[source]

The matrix of the boundary operator \(\partial_{\mathrm{dim}}\) of the normalized chain complex.

Following Section 4.4 of Peltier and Lienhardt [PL18], the chain group \(C_k\) is spanned by the non-degenerate simploids of dimension \(k\), and the boundary of a simploid \(\sigma\) of type \((a_1, \ldots, a_n)\) is

\[\partial \sigma = \sum_{i=1}^n \sum_{j=0}^{a_i} (-1)^{a_1 + \cdots + a_{i-1} + j} d^i_j \sigma,\]

the Koszul sign rule for the product \(\Delta^{a_1} \times \cdots \times \Delta^{a_n}\), where the terms whose face \(d^i_j \sigma\) is degenerate are dropped.

Parameters:

dim – The dimension \(k \ge 0\) of the simploids whose boundary is taken.

Returns:

  • An integer (torch.long) matrix of shape (len(simploids(dim - 1)), len(simploids(dim)))

  • column c holds the boundary of simploids(dim)[c] in the basis simploids(dim - 1).

  • boundary_matrix(0) has no rows.

Raises:

ValueError – If dim is negative or a face does not belong to this set.

Examples

>>> from torch_bsf.geometry import SimploidalSet
>>> S = SimploidalSet()
>>> square = S.add_simploid((1, 1))
>>> [[key for key, f in square.faces() if f is e] for e in S.simploids(1)]
[[(0, 1)], [(0, 0)], [(1, 1)], [(1, 0)]]
>>> S.boundary_matrix(2)
tensor([[-1],
        [ 1],
        [ 1],
        [-1]])
>>> bool((S.boundary_matrix(1) @ S.boundary_matrix(2) == 0).all())
True
check() → None[source]

Checks the face commutation relations (II) of Definition 1 for every simploid.

Raises:

ValueError – If a relation does not hold or a face does not belong to this set.

cone(simploids: Sequence[Simploid] | None = None) → tuple[Simploid, dict[Simploid, Simploid]][source]

Cones simploids with a new vertex (Algorithm 8 of the paper).

The cone of a simploid of type \((a_1, \ldots, a_k)\) is a simploid of type \((a_1, \ldots, a_k, 1)\): an extrusion whose far end is degenerated into the new vertex v. For example, the cone of a square is a square-based pyramid of type \((1, 1, 1)\). The cone c of sigma satisfies c.face(k, 1) is sigma and c.face(k, 0) is the degeneracy of v with the type of sigma, where k = sigma.length.

Parameters:

simploids – The simploids to cone (their boundaries are coned too). All simploids if None.

Returns:

  • vertex – The new vertex.

  • cones – Maps each coned non-degenerate simploid to its cone.

Examples

>>> from torch_bsf.geometry import SimploidalSet
>>> S = SimploidalSet()
>>> square = S.add_simploid((1, 1))
>>> v, cones = S.cone([square])
>>> pyramid = cones[square]
>>> pyramid.simploid_type
(1, 1, 1)
>>> pyramid.face(2, 0).is_degenerate, pyramid.face(2, 0).base is v
(True, True)
degenerate(simploid: Simploid, face: Simploid, i: int, j: int) → Simploid[source]

Degenerates a simploid onto a degenerate simploid of one of its faces.

This identifies simploid with face.degeneracy(i, j); the two must have the same type, and their boundaries are identified accordingly.

Parameters:
Return type:

The resulting degenerate simploid.

Examples

Collapse an edge of a square into a vertex, which turns the square into a triangle-shaped simploid of type (1, 1):

>>> from torch_bsf.geometry import SimploidalSet
>>> S = SimploidalSet()
>>> square = S.add_simploid((1, 1))
>>> edge = square.face(0, 0)  # an edge of type (1,)
>>> d = S.degenerate(edge, edge.face(0, 0), 0, -1)
>>> d.is_degenerate, square.face(0, 0) is d
(True, True)
>>> len(S.simploids(0))
3
property dim: int[source]

The maximum dimension of the simploids (-1 if empty).

identify(a: Simploid, b: Simploid) → Simploid[source]

Identifies two simploids of the same type.

This is the higher-level identification of the paper: the boundaries of a and b are identified face by face (a.face(i, j) with b.face(i, j)), and the degenerate simploids induced by the merged simploids are identified too. If one of the identified simploids is degenerate, the result is degenerate (this is how degenerate() works).

References to simploids that are merged away become stale; use the returned simploid and the faces of the surviving simploids instead.

Parameters:
  • a – The simploids to identify.

  • b – The simploids to identify.

Return type:

The simploid resulting from the identification.

Raises:

ValueError – If the simploids (or faces to be identified) have different types, do not belong to this set, or if two distinct degenerate simploids would be identified.

Examples

Glue two triangles along an edge:

>>> from torch_bsf.geometry import SimploidalSet
>>> S = SimploidalSet()
>>> t1, t2 = S.add_simplex(2), S.add_simplex(2)
>>> e = S.identify(t1.face(0, 0), t2.face(0, 2))
>>> t1.face(0, 0) is e and t2.face(0, 2) is e
True
>>> len(S.simploids(0)), len(S.simploids(1))
(4, 5)
main_simploids() → list[Simploid][source]

The non-degenerate simploids that are not a face of another non-degenerate simploid.

product(other: SimploidalSet) → tuple[SimploidalSet, dict[tuple[Simploid, Simploid], Simploid]][source]

The cartesian product of two simploidal sets (Algorithm 7 of the paper).

The product of simploids of types \((a_1, \ldots, a_k)\) and \((b_1, \ldots, b_l)\) is a simploid of type \((a_1, \ldots, a_k, b_1, \ldots, b_l)\).

Parameters:

other – The second factor.

Returns:

  • product – A new simploidal set.

  • pairs – Maps each pair (sigma1, sigma2) of non-degenerate simploids to sigma1 x sigma2.

Examples

>>> from torch_bsf.geometry import SimploidalSet
>>> A, B = SimploidalSet(), SimploidalSet()
>>> tri, edge = A.add_simplex(2), B.add_simplex(1)
>>> P, pairs = A.product(B)
>>> pairs[(tri, edge)].simploid_type
(2, 1)
simploids(dim: int | None = None) → list[Simploid][source]

The non-degenerate simploids, optionally only those of a given dimension.

preprocessing

Explicit output transforms make scaling visible in training and inference. Fit statistics on training responses, transform exact fixed values in the same way, and invert predictions before reporting original-unit errors. See Output scaling for penalty and validation implications.

class torch_bsf.preprocessing.MinMaxScaler[source]

Bases: object

Min-max scaler that normalizes values to the [0, 1] range.

Floating statistics and returned values must be finite in the input dtype. Integral inputs retain integer statistics and use PyTorch’s default floating dtype for normalized values. An unrepresentable fitted range raises ValueError; rescale the original data units first.

mins[source]

Minimum values per feature dimension.

Type:

torch.Tensor

scales[source]

Scale factors (max - min) per feature dimension. Dimensions where max equals min are set to 1 to avoid division by zero.

Type:

torch.Tensor

fit(values: Tensor) → None[source]

Fit the scaler to the data.

Computes the per-feature minimum and scale (max - min) from values.

Parameters:

values (torch.Tensor) – Input tensor of shape (n_samples, n_features).

fit_transform(values: Tensor) → Tensor[source]

Fit the scaler to the data and return the normalized tensor.

Parameters:

values (torch.Tensor) – Input tensor of shape (n_samples, n_features).

Returns:

Normalized tensor of the same shape as values, with each feature scaled to the [0, 1] range.

Return type:

torch.Tensor

inverse_transform(values: Tensor) → Tensor[source]

Reverse the normalization applied by fit_transform().

Parameters:

values (torch.Tensor) – Normalized tensor of shape (n_samples, n_features) with the fitted feature count. Empty batches (zero samples) are accepted.

Returns:

Tensor rescaled back to the original value range.

Return type:

torch.Tensor

Raises:

ValueError – If the input is not two-dimensional, has a different feature count from the fitted data, or contains non-finite values or results.

transform(values: Tensor) → Tensor[source]

Normalize values with the fitted minimum and scale.

Parameters:

values (torch.Tensor) – Tensor of shape (n_samples, n_features) with the fitted feature count. Empty batches (zero samples) are accepted.

Returns:

Transformed tensor of the same shape as values.

Return type:

torch.Tensor

Raises:

ValueError – If the input is not two-dimensional, has a different feature count from the fitted data, or contains non-finite values or results.

class torch_bsf.preprocessing.NoneScaler[source]

Bases: object

Pass-through scaler that leaves values unchanged.

Useful as a no-op placeholder when normalization is not required, while still providing the same fit(), fit_transform(), and inverse_transform() interface as the other scalers.

fit(values: Tensor) → None[source]

No-op fit method included for API compatibility.

Parameters:

values (torch.Tensor) – Input tensor (ignored).

fit_transform(values: Tensor) → Tensor[source]

Return values unchanged.

Parameters:

values (torch.Tensor) – Input tensor of any shape.

Returns:

The same tensor as values without any modification.

Return type:

torch.Tensor

inverse_transform(values: Tensor) → Tensor[source]

Return values unchanged.

Parameters:

values (torch.Tensor) – Input tensor of any shape.

Returns:

The same tensor as values without any modification.

Return type:

torch.Tensor

transform(values: Tensor) → Tensor[source]

Return values unchanged.

Parameters:

values (torch.Tensor) – Input tensor of any shape.

Returns:

The same tensor as values without any modification.

Return type:

torch.Tensor

torch_bsf.preprocessing.NormalizeType[source]

"max" (MinMaxScaler), "std" (StdScaler), "quantile" (QuantileScaler) and "none" (NoneScaler).

Type:

The value normalization methods

alias of Literal[‘max’, ‘std’, ‘quantile’, ‘none’]

class torch_bsf.preprocessing.QuantileScaler[source]

Bases: object

Quantile-based scaler that normalizes values using percentile-based ranges.

Values are scaled so that the q-th percentile maps to 0 and the (1 - q)-th percentile maps to 1, without clipping values to this range. This makes the scaler robust to outliers compared to MinMaxScaler. Non-finite input, an unrepresentable fitted range, or an unrepresentable transformed result raises ValueError. Rescale original units first.

q[source]

The lower quantile fraction used as the effective minimum. Defaults to 0.05 (5th percentile), ignoring the bottom and top 5% of values.

Type:

float

mins[source]

Per-feature values at the q-th quantile, computed during fit().

Type:

torch.Tensor

scales[source]

Per-feature scale factors ((1-q)-quantile minus q-quantile). Dimensions where the scale is zero are set to 1 to avoid division by zero.

Type:

torch.Tensor

fit(values: Tensor) → None[source]

Fit the scaler to the data.

Computes per-feature quantile bounds from values.

Parameters:

values (torch.Tensor) – Input tensor of shape (n_samples, n_features).

fit_transform(values: Tensor) → Tensor[source]

Fit the scaler to the data and return the scaled tensor.

Parameters:

values (torch.Tensor) – Input tensor of shape (n_samples, n_features).

Returns:

Scaled tensor of the same shape as values. Values outside the fitted quantile range may exceed [0, 1].

Return type:

torch.Tensor

inverse_transform(values: Tensor) → Tensor[source]

Reverse the scaling applied by fit_transform().

Parameters:

values (torch.Tensor) – Scaled tensor of shape (n_samples, n_features) with the fitted feature count. Empty batches (zero samples) are accepted.

Returns:

Tensor rescaled back to the original value range.

Return type:

torch.Tensor

Raises:

ValueError – If the input is not two-dimensional, has a different feature count from the fitted data, or contains non-finite values or results.

transform(values: Tensor) → Tensor[source]

Scale values with the fitted quantile bounds.

Parameters:

values (torch.Tensor) – Tensor of shape (n_samples, n_features) with the fitted feature count. Empty batches (zero samples) are accepted.

Returns:

Transformed tensor of the same shape as values.

Return type:

torch.Tensor

Raises:

ValueError – If the input is not two-dimensional, has a different feature count from the fitted data, or contains non-finite values or results.

class torch_bsf.preprocessing.Scaler(*args, **kwargs)[source]

Bases: Protocol

Protocol for data scalers with a fit/transform interface.

All concrete scaler classes (MinMaxScaler, StdScaler, QuantileScaler, NoneScaler) satisfy this protocol via structural subtyping — no explicit inheritance is required.

fit(values: Tensor) → None[source]

Fit the scaler to values.

fit_transform(values: Tensor) → Tensor[source]

Fit the scaler to values and return the transformed tensor.

inverse_transform(values: Tensor) → Tensor[source]

Reverse the transformation applied by fit_transform().

transform(values: Tensor) → Tensor[source]

Transform values with the fitted statistics.

class torch_bsf.preprocessing.StdScaler[source]

Bases: object

Standard-score (z-score) scaler that normalizes values to zero mean and unit variance.

Statistics use a scaled fallback to avoid intermediate overflow. An unrepresentable standard deviation or result raises ValueError.

means[source]

Per-feature means computed during fit().

Type:

torch.Tensor

stds[source]

Per-feature standard deviations computed during fit(). Dimensions with zero standard deviation are set to 1 to avoid division by zero.

Type:

torch.Tensor

fit(values: Tensor) → None[source]

Fit the scaler to the data.

Computes the per-feature mean and standard deviation from values.

Parameters:

values (torch.Tensor) – Input tensor of shape (n_samples, n_features).

fit_transform(values: Tensor) → Tensor[source]

Fit the scaler to the data and return the standardized tensor.

Parameters:

values (torch.Tensor) – Input tensor of shape (n_samples, n_features).

Returns:

Standardized tensor of the same shape as values, with each feature having zero mean and unit standard deviation.

Return type:

torch.Tensor

inverse_transform(values: Tensor) → Tensor[source]

Reverse the standardization applied by fit_transform().

Parameters:

values (torch.Tensor) – Standardized tensor of shape (n_samples, n_features) with the fitted feature count. Empty batches (zero samples) are accepted.

Returns:

Tensor rescaled back to the original value range.

Return type:

torch.Tensor

Raises:

ValueError – If the input is not two-dimensional, has a different feature count from the fitted data, or contains non-finite values or results.

transform(values: Tensor) → Tensor[source]

Standardize values with the fitted mean and standard deviation.

Parameters:

values (torch.Tensor) – Tensor of shape (n_samples, n_features) with the fitted feature count. Empty batches (zero samples) are accepted.

Returns:

Transformed tensor of the same shape as values.

Return type:

torch.Tensor

Raises:

ValueError – If the input is not two-dimensional, has a different feature count from the fitted data, or contains non-finite values or results.

torch_bsf.preprocessing.make_scaler(normalize: Literal['max', 'std', 'quantile', 'none']) → Scaler[source]

The scaler of a normalization method.

Every scaler is an affine map of each feature, so it commutes with Bézier simploids, whose Bernstein basis sums to one: transforming the control points transforms the values of the model.

Parameters:

normalize – "max", "std", "quantile" or "none".

Return type:

A new, unfitted scaler.

Raises:

ValueError – If normalize is not one of the methods.

Examples

>>> import torch
>>> from torch_bsf.preprocessing import make_scaler
>>> scaler = make_scaler("max")
>>> scaler.fit_transform(torch.tensor([[1.0], [3.0]])).tolist()
[[0.0], [1.0]]

sampling

sample returns seeded parameter draws and grid returns deterministic lattices. Neither obtains response measurements. Product factors retain independent barycentric normalization, and assembled samples retain cell IDs. See Sampling and grids and Grid sampling. sample_skeleton splits rows among a simplex’s vertices, edges and larger faces, by default in the ratio that minimizes the asymptotic risk of inductive skeleton fitting; see Optimal skeleton sampling.

Domain-aware random and deterministic sampling, using local random generators.

Skeleton sampling and its optimal ratio follow the asymptotic risk analysis of inductive skeleton fitting by Tanaka et al. (AAAI 2020), extended to every dimension and degree.

torch_bsf.sampling.grid(space: BezierSpace, resolution: int = 10, *, cells: Sequence[int | Simploid] | None = None) → Points[source]

Create tensor-product barycentric grids on the selected cells.

resolution=0 gives a cell’s barycenter. Boundary points of different cells remain distinct parameter-domain points even when their images agree.

torch_bsf.sampling.optimal_skeleton_ratio(space: BezierSpace) → tuple[float, ...][source]

Skeleton sample fractions that minimize the asymptotic risk of inductive skeleton fitting.

Minimizing \(\sum_m a_m / N^{(m)}\) subject to \(\sum_m N^{(m)} = N\) gives \(N^{(m)} / N = \sqrt{a_m} / \sum_k \sqrt{a_k}\) and the minimum \((\sum_m \sqrt{a_m})^2 / N\) (Cauchy-Schwarz), where \(a_m\) are skeleton_risk_coefficients().

Parameters:

space – A single simplex of positive dimension and degree.

Returns:

Nonnegative fractions for the 0-, 1-, …, (M-1)-skeletons, summing to one.

Return type:

tuple of float

Raises:

Examples

>>> from torch_bsf import BezierSpace
>>> from torch_bsf.sampling import optimal_skeleton_ratio
>>> [round(r, 4) for r in optimal_skeleton_ratio(BezierSpace.simplex(1, degree=2))]
[0.4142, 0.5858]
torch_bsf.sampling.sample(space: BezierSpace, n: int, *, method: Literal['random', 'sobol'] = 'random', seed: int | None = None, cell_weights: Mapping[int | Simploid, float] | None = None) → Points[source]

Sample a space, uniformly within each selected cell’s simplex factors.

Cell probabilities default to equal weights on maximal cells, rather than an unspecified geometric volume. Sobol coordinates use exponential normalization of scrambled quasi-random uniforms. Cell selection remains categorical random sampling. Neither method resets global random state.

Examples

>>> from torch_bsf import BezierSpace
>>> from torch_bsf.sampling import sample
>>> sample(BezierSpace.simplex(2, degree=3), 5, seed=0).parameters.shape
torch.Size([5, 3])
torch_bsf.sampling.sample_skeleton(space: BezierSpace, n: int, *, ratio: Sequence[float] | Tensor | None = None, method: Literal['random', 'sobol'] = 'random', seed: int | None = None) → Points[source]

Stratified sample of a simplex’s skeletons for inductive skeleton fitting.

The n rows are split among the 0-skeleton (vertices), 1-skeleton (edges), … in proportion to ratio by largest-remainder rounding. Each skeleton’s count is split equally among its faces; when it does not divide, randomly chosen faces receive one extra row. Rows are uniform on their face, with exact zeros outside it, and are ordered by face size.

Parameters:
  • space – A single simplex of positive dimension and degree.

  • n – Number of rows.

  • ratio – Relative sizes of the 0-, 1-, …, (M-1)-skeletons of a simplex with M vertices: M finite nonnegative real entries with a positive sum. Defaults to optimal_skeleton_ratio(), which places no rows on skeletons without interior controls. Rows on such skeletons (faces with more than degree vertices) are not used by fit_skeleton_regression().

  • method – "random" or "sobol" coordinates within each face, as in sample().

  • seed – Seed of the local generator; global random state is not used.

Returns:

Barycentric rows of the single cell.

Return type:

Points

Raises:
  • TypeError – If space is not a BezierSpace.

  • ValueError – If space is not a single simplex of positive dimension and degree, n is not a nonnegative integer, method is unknown, or ratio is malformed. With the default ratio, also if the degree is too high for skeleton_risk_coefficients(); pass ratio then.

Examples

>>> from torch_bsf import BezierSpace
>>> from torch_bsf.sampling import sample_skeleton
>>> points = sample_skeleton(BezierSpace.simplex(2, degree=2), 100, seed=0)
>>> (points.parameters > 0).sum(1).bincount().tolist()
[0, 26, 74]
torch_bsf.sampling.skeleton_risk_coefficients(space: BezierSpace) → tuple[float, ...][source]

Asymptotic risk coefficients of inductive skeleton fitting on a simplex.

Inductive skeleton fitting estimates the controls interior to each face by least squares on that face’s observations, holding the controls of lower faces at their estimates (see torch_bsf.fitting.fit_skeleton_regression()). With \(N^{(m)}\) observations on the \(m\)-skeleton, split equally among its faces and drawn uniformly on each, outputs of dimension \(L\) and independent noise of variance \(\sigma^2\), the mean integrated squared error over the uniform simplex satisfies

\[R = \sigma^2 L \sum_{m} a_m / N^{(m)} + o(1 / N).\]

Coefficient m is \(a_m\). It is zero for skeletons whose faces have no interior controls (\(m \geq\) degree). This extends the numerical tables of Tanaka et al. (AAAI 2020) to every dimension and degree.

Parameters:

space – A single simplex of positive dimension and degree.

Returns:

\(a_0, \dots, a_{M-1}\) for a simplex with \(M\) vertices.

Return type:

tuple of float

Raises:
  • TypeError – If space is not a BezierSpace.

  • ValueError – If space is not a single simplex of positive dimension and degree, or if its Bernstein Gram matrices are too ill-conditioned to bound the float64 relative error by 1e-6. The largest accepted degree falls with the dimension: 14 for 2 vertices, 12 for 3, 11 for 4, 9 for 5, 8 for 6 and 7, and 7 for 8.

Notes

Time and memory grow with the square of the number of controls, \(\binom{D + M - 1}{D}\), so wide simplices are costly even at low degree.

Examples

>>> from torch_bsf import BezierSpace
>>> from torch_bsf.sampling import skeleton_risk_coefficients
>>> [round(a, 4) for a in skeleton_risk_coefficients(BezierSpace.simplex(2, degree=3))]
[0.1464, 3.5357, 1.0]

fitting

fit solves regression on known parameter-response pairs and returns FitResult. Fixed controls and quadratic penalties participate in the same solve. Evaluate result.model(points) after checking the numerical status; there is no initial guess for this linear least-squares operation. fit_skeleton_regression instead fits a simplex face by face from observations on its faces; see Optimal skeleton sampling.

Supervised fitting at known parameters: one entry point for every space and solver, and face-by-face simplex fitting.

torch_bsf.fitting.fit(space: BezierSpace, samples: Samples, *, solver: DenseLeastSquares | SparseLeastSquares = DenseLeastSquares(rcond=None, diagnostics='basic'), penalty: QuadraticPenalty = QuadraticPenalty(ridge=0.0, smoothness=0.0), fixed: FixedControls | None = None) → FitResult[source]

Fit a space to observations, returning an explicit numerical result.

Minimizes sum(w * squared_output_error) / sum(w) plus ridge * ||P||² and smoothness * ||D P||² in the original output units. Data MSE divides the data term by the output dimension. Fixed coefficients are exact values, not initial guesses. Neither the space nor observations are modified.

Parameters:
  • space – A compiled single-cell or glued-cell polynomial space.

  • samples – Coordinates, outputs and optional sample weights.

  • solver – Typed dense SVD or sparse LSMR configuration. Both compute on CPU float64 and return a float64 model; no implicit downcast is performed.

  • penalty – Quadratic weights, independent of the number of observations.

  • fixed – Explicit globally shared coefficient values.

Returns:

Model, metrics, diagnostics and termination state. This type never changes with diagnostic settings. Predict with result.model(...). Numerical failures return success=False and emit ConvergenceWarning; require_success() raises with the result attached. If no finite iterate exists, diagnostics.solution_available is false and the model contains placeholder free coefficients rather than a fit.

Return type:

FitResult

Examples

>>> import torch
>>> from torch_bsf import BezierSpace, Samples, fit
>>> t = torch.eye(2)
>>> result = fit(BezierSpace.simplex(1, degree=1), Samples(t, [[1.], [2.]]))
>>> result.success, result.model(t).tolist()
(True, [[1.0], [2.0]])
torch_bsf.fitting.fit_skeleton_regression(space: BezierSpace, samples: Samples, *, solver: DenseLeastSquares | SparseLeastSquares = DenseLeastSquares(rcond=None, diagnostics='basic')) → SkeletonRegressionResult[source]

Fit a simplex face by face from observations at known parameters.

This is the inductive skeleton fitting analyzed by Tanaka et al. (AAAI 2020). Vertices are fitted first, then edges, then larger faces. On each face, ordinary least squares on the observations lying on that face estimates the controls interior to it, with the controls on its boundary fixed at their estimates. Contrast fit(), which uses every observation for every control, and fit_skeleton(), which infers unknown parameters.

A row lies on the face spanned by its nonzero coordinates; zeros must be exact. Draw rows with sample_skeleton() and the optimal_skeleton_ratio() to minimize the asymptotic risk given by skeleton_risk_coefficients().

Parameters:
  • space – A single simplex of positive dimension and degree.

  • samples – Observations at known parameters. Every face with at most degree vertices needs at least one row of positive weight. Rows on larger faces, which have no interior controls, are ignored and counted in the result’s message. Weights apply within each face.

  • solver – Least-squares configuration used for every face.

Returns:

The assembled model and each face’s FitResult. A face whose rows do not identify its interior controls is not a failure; inspect that face’s diagnostics.data.rank. If a face’s solve fails, as reported by its FitResult (with a ConvergenceWarning), fitting stops there: success is False, faces ends with the failed face, and controls of later faces stay zero.

Return type:

SkeletonRegressionResult

Raises:
  • TypeError – If space or samples has the wrong type.

  • ValueError – If space is not a single simplex of positive dimension and degree, or a face with interior controls has no row of positive weight.

Examples

>>> import torch
>>> from torch_bsf import BezierSpace, Samples
>>> from torch_bsf.fitting import fit_skeleton_regression
>>> from torch_bsf.sampling import sample_skeleton
>>> space = BezierSpace.simplex(1, degree=2)
>>> points = sample_skeleton(space, 8, seed=0)
>>> t = points.parameters
>>> result = fit_skeleton_regression(space, Samples(points, (t[:, 1] ** 2)[:, None]))
>>> sorted(result.faces), torch.allclose(result.model(t), (t[:, 1] ** 2)[:, None])
([(0,), (0, 1), (1,)], True)

point_cloud

fit_point_cloud fits observations whose parameter coordinates are unknown, alternating inferred coordinates and controls. fit_skeleton fits faces inductively while preserving their boundaries. These nonlinear methods return explicit local convergence states. See Fitting from values alone for assumptions, initialization and examples.

Value-only Bézier simplex fitting (Kobayashi et al., AAAI 2019).

Algorithm 2 alternates constrained Newton parameter updates and linear least squares. Algorithm 3 fits faces in increasing support size, preserving the controls already estimated on their boundaries.

class torch_bsf.point_cloud.PointCloudFitResult(model: BezierModel, parameters: Tensor, objective_history: tuple[float, ...], success: bool, status: str, message: str, n_iterations: int, parameter_converged: bool, control_result: FitResult | None)[source]

Bases: object

A fitted surface and inferred barycentric rows in observation order.

objective_history contains the initial mean squared Euclidean distance and each accepted alternating update (no division by output dimension). Success describes local numerical convergence, not global optimality or identification of a unique parameterization. All tensors are CPU float64.

require_success() → PointCloudFitResult[source]

Raise with the termination message if convergence was not established.

class torch_bsf.point_cloud.SkeletonFitResult(model: BezierModel, faces: dict[tuple[int, ...], PointCloudFitResult], success: bool, status: str, message: str)[source]

Bases: object

Assembled model and local results keyed by sorted zero-based vertex IDs.

Each face result contains local barycentric coordinates for that face’s observations. Previously fitted boundary controls remain exactly fixed.

require_success() → SkeletonFitResult[source]

Raise if any face failed to converge.

torch_bsf.point_cloud.fit_point_cloud(space: BezierSpace, values: Any, *, initial: BezierModel | None = None, fixed: FixedControls | None = None, max_iter: int = 100, parameter_max_iter: int = 100, tolerance: float = 1e-05, parameter_tolerance: float = 1e-05) → PointCloudFitResult[source]

Fit values without supplied domain coordinates (paper Algorithm 2).

Minimize the mean squared Euclidean distance by alternating parameter and control updates. Parameters use analytic first/second derivatives with projected Newton steps, backtracking and a gradient fallback. Controls use CPU float64 SVD least squares, never normal equations. fixed specifies exact controls throughout, including before the first parameter update.

Automatic initialization uses the observed minimum of each output as a vertex and the affine grid between those vertices, as in the paper. It assumes minimization objectives and one output per vertex. Supply an initial model for arbitrary embeddings or a different orientation. No input model or observation is modified. Only single simplices are supported. Exhausting either iteration budget does not certify convergence.

tolerance bounds absolute change in root mean squared distance; parameter_tolerance bounds the projected gradient residual. The stopping rule uses the absolute change, correcting the signed-change expression printed in the paper. The objective uses the original units; scale observations explicitly when outputs have incompatible units.

torch_bsf.point_cloud.fit_skeleton(space: BezierSpace, values: Any, *, face_values: Mapping[tuple[int, ...], Any] | None = None, initial: BezierModel | None = None, max_iter: int = 100, parameter_max_iter: int = 100, tolerance: float = 1e-05, parameter_tolerance: float = 1e-05) → SkeletonFitResult[source]

Fit faces inductively while fixing their boundaries (paper Algorithm 3).

Faces are sorted tuples of zero-based vertex IDs. Only support sizes up to min(degree, number_of_vertices) are needed. By default each face’s samples are the nondominated points for its objective subset (all outputs are minimization objectives). This requires one output per vertex. face_values instead supplies every required face’s value-only samples, allowing arbitrary embeddings with initial. Full original output vectors must be retained on every face. No face memberships or coordinates are inferred from an arbitrary embedding.

Vertex controls are sample means. Higher faces use fit_point_cloud with all lower-face controls fixed exactly. Inspect faces for local stopping evidence; failure on a boundary stops assembly and returns the finite partial model with success=False.

results

Results separate a computed model from evidence about the computation. require_success raises on unsuccessful termination. Data diagnostics describe observation identifiability, whereas system diagnostics can also include penalty rows. Successful termination is distinct from held-out prediction accuracy.

Explicit numerical results: a computed model is not a convergence certificate.

exception torch_bsf.results.ConvergenceError(result: FitResult)[source]

Bases: RuntimeError

Failure requested by require_success(), retaining the numerical result.

exception torch_bsf.results.ConvergenceWarning[source]

Bases: UserWarning

A numerical method stopped without meeting its convergence criteria.

class torch_bsf.results.FitDiagnostics(solver: str, n_samples: int, n_free_controls: int, weight_sum: float, data: MatrixDiagnostics, system: MatrixDiagnostics, compute_dtype: str = 'float64', compute_device: str = 'cpu', model_dtype: str = 'float64', model_device: str = 'cpu', iterations: tuple[int | None, ...] = (), stop_codes: tuple[int | None, ...] = (), residual_norms: tuple[float | None, ...] = (), normal_residual_norms: tuple[float | None, ...] = (), condition_estimates: tuple[float | None, ...] = (), driver: str | None = None, output_success: tuple[bool, ...] = (), output_statuses: tuple[str, ...] = (), solution_available: bool = True)[source]

Bases: object

Compute provenance and observation/system diagnostics in original units.

driver identifies the executed backend (gelsd or lsmr), or is None if no solve was executed. output_success and output_statuses record termination for each output column, in prediction-column order. solution_available is false when a numerical failure left no finite iterate for at least one column; see FitResult for its placeholder. Residual norms and condition estimates from LSMR concern the augmented weighted, constrained system, not the unpenalized prediction MSE. Iterative fields use None for a column whose RHS overflowed and was not solved, or empty tuples if no iterative solve ran at all. Overall success can still be false after successful solves if objective evaluation or optional diagnostics fail; these failures do not erase solver termination.

class torch_bsf.results.FitResult(model: BezierModel, metrics: dict[str, float], diagnostics: FitDiagnostics, success: bool, status: str, message: str)[source]

Bases: object

A model, objective metrics, diagnostics and an explicit termination state.

Results are deliberately not callable. Predict with result.model(points). If diagnostics.solution_available is false, the model is only a finite placeholder preserving fixed controls, with free rows of unavailable output columns zero. It must not be used as a fitted solution. Finite unconverged LSMR iterates are retained.

require_success() → FitResult[source]

Return this result, or raise a ConvergenceError containing it.

warn_if_failed() → FitResult[source]

Expose a failed numerical solve without hiding its partial result.

class torch_bsf.results.MatrixDiagnostics(shape: tuple[int, int], rank: int | None = None, rank_threshold: float | None = None, singular_values: Tensor | None = None, condition_number: float | None = None, method: Literal['svd', 'estimate', 'not_computed', 'no_free_controls'] = 'not_computed')[source]

Bases: object

Information about one explicitly identified least-squares matrix.

None means not computed, never zero. method distinguishes a direct singular-value calculation from an iterative condition estimate. A rank below the number of free columns has infinite coefficient condition number.

class torch_bsf.results.SkeletonRegressionResult(model: BezierModel, faces: dict[tuple[int, ...], FitResult], success: bool, status: str, message: str)[source]

Bases: object

Assembled model and face least-squares results keyed by sorted zero-based vertex IDs.

Each face result solves for that face’s interior controls with every other control fixed. Faces are fitted in increasing size; a failed face stops assembly, leaving later faces absent and their controls zero.

require_success() → SkeletonRegressionResult[source]

Raise ConvergenceError with the failed face’s result if a face fit failed.

solvers

Choose a typed dense or sparse least-squares configuration. Both compute CPU float64 models; sparse solving uses the optional SciPy dependency. Tolerances and iteration limits concern the numerical solve rather than polynomial capacity. See Solvers, performance and optional execution.

Typed numerical solver configurations; importing this module does not load SciPy.

class torch_bsf.solvers.DenseLeastSquares(rcond: float | None = None, diagnostics: Literal['basic', 'full'] = 'basic')[source]

Bases: object

CPU float64 SVD solve of the augmented problem, without normal equations.

Parameters:
  • rcond (float | None) – Relative singular-value cutoff. None uses machine epsilon times the larger matrix dimension. The actual absolute threshold is recorded.

  • diagnostics (Literal['basic', 'full']) – basic reuses the system SVD; full additionally diagnoses the weighted observation matrix when regularization changes the system.

class torch_bsf.solvers.SparseLeastSquares(atol: float = 1e-08, btol: float = 1e-08, conlim: float = 100000000.0, max_iter: int | None = None)[source]

Bases: object

Optional SciPy LSMR on CPU float64, with explicit per-output stop codes.

Parameters:
  • atol – Backward-error stopping tolerances.

  • btol – Backward-error stopping tolerances.

  • conlim (float) – Estimated condition-number limit; zero disables this limit.

  • max_iter (int | None) – Maximum iterations per output; None uses the backend default. Condition or iteration limits are failures, not successful convergence.

model_selection

Validation operations compare predictions on withheld observations and retain fold results. Degree selection changes capacity and requires an explicit final refit when using the core selection API. Learn output preprocessing inside each fold to prevent validation leakage.

Validation and degree search return explicit evidence rather than bare models.

class torch_bsf.model_selection.CVResult(folds: tuple[FoldResult, ...], metrics: dict[str, float], success: bool)[source]

Bases: object

Validation evidence; never an implicitly refitted model or ensemble.

property models: tuple[BezierModel, ...][source]

Models available for explicitly constructing an Ensemble.

class torch_bsf.model_selection.DegreeCandidate(degree: int | tuple[int, ...], space: BezierSpace, validation: CVResult)[source]

Bases: object

A candidate degree and all its validation evidence.

class torch_bsf.model_selection.DegreeSearchResult(candidates: tuple[DegreeCandidate, ...], best_index: int | None, searched_all: bool = True)[source]

Bases: object

All evaluated candidates and a selection, without an implicit full-data fit.

class torch_bsf.model_selection.FoldResult(train_indices: Tensor, test_indices: Tensor, result: FitResult, metrics: dict[str, float], test_weight: float)[source]

Bases: object

A fitted training fold with its untouched held-out indices and metrics.

class torch_bsf.model_selection.KFold(n_splits: int = 5, shuffle: bool = True, seed: int | None = None)[source]

Bases: object

Local-generator k-fold splits; an impossible requested split count is an error.

split(n_samples: int) → tuple[tuple[Tensor, Tensor], ...][source]

Return disjoint train/test indices, covering every test sample once.

torch_bsf.model_selection.cross_validate(space: BezierSpace, samples: Samples, *, cv: KFold = KFold(n_splits=5, shuffle=True, seed=None), solver: DenseLeastSquares | SparseLeastSquares = DenseLeastSquares(rcond=None, diagnostics='basic'), penalty: QuadraticPenalty = QuadraticPenalty(ridge=0.0, smoothness=0.0), fixed: FixedControls | None = None) → CVResult[source]

Fit each training fold and retain split, solver and held-out evidence.

Every fold must have positive observation weight in both its train and test portions. Failed solves remain in the result and make the whole CV fail; they are never silently dropped from model selection. Non-finite held-out or aggregate metrics also make CV fail, retaining every fold for inspection. Zero-weight held-out rows are excluded from metric evaluation.

torch_bsf.model_selection.select_degree(space: BezierSpace, samples: Samples, *, degrees: Sequence[int | Sequence[int]], cv: KFold = KFold(n_splits=5, shuffle=True, seed=None), solver: DenseLeastSquares | SparseLeastSquares = DenseLeastSquares(rcond=None, diagnostics='basic'), penalty: QuadraticPenalty = QuadraticPenalty(ridge=0.0, smoothness=0.0)) → DegreeSearchResult[source]

Evaluate every specified degree on identical folds and record the result.

Invalid degrees raise; failed numerical solves remain visible and cannot win. Non-finite validation metrics likewise make candidates ineligible. If none is eligible, best_index is None and success is False. To refit, explicitly call fit(result.best_space, samples, ...).

ensemble

An ensemble combines explicitly selected compatible models for prediction. It is a deliberate modeling choice, not an automatic consequence of cross-validation. Compare its validation behavior with a single model refitted on all available training observations.

Explicit ensembling, independent of cross-validation and model selection.

class torch_bsf.ensemble.Ensemble(models: Sequence[BezierModel])[source]

Bases: Module

Mean predictor over copied models with the same space and output dimension.

forward(points: Points | Tensor) → Tensor[source]

Return the ensemble mean, without changing the member models.

Finite groups near sum overflow use an exact CPU reduction to retain cancelling residuals. The overflow-risk check synchronizes accelerators; only affected groups transfer to CPU. Ordinary groups remain vectorized on their device, and finite-input gradients follow the ordinary mean.

members(points: Points | Tensor) → Tensor[source]

Predict with every member, returning shape (n_models, n_points, n_outputs).

active_learning

Active learning selects additional parameter locations from a candidate pool. The application still obtains the responses and appends observations. Assess progress on an external validation set rather than only the adaptively selected training rows. See Explicit ensembles and active learning.

Acquisition functions with explicit, mathematically relevant inputs.

torch_bsf.active_learning.farthest_points(candidates: Points, observed: Points, n: int = 1, *, chunk_size: int = 1024) → Points[source]

Greedily cover parameter coordinates, comparing distances within cells.

Unobserved cells are selected first. No dummy ensemble or duplicated geometry/dimension arguments are required for this density acquisition. Equal distances select the earliest remaining candidate. chunk_size bounds both axes of each temporary distance matrix, using at most chunk_size ** 2 float64 distances. Smaller chunks reduce memory at the cost of more distance-kernel calls; arithmetic work is unchanged. Distances use direct coordinate differences to retain small separations between nearby points, independently of the candidate/observation count.

Parameters:
  • candidates (Points) – Candidate parameter coordinates with their cell IDs.

  • observed (Points) – Existing observations in the same coordinate layout.

  • n (int, default=1) – Number of distinct candidate rows to select.

  • chunk_size (int, default=1024) – Positive upper bound on each distance-matrix dimension.

Returns:

Selected candidates in greedy order, retaining input gradients.

Return type:

Points

Examples

>>> from torch_bsf import Points
>>> from torch_bsf.active_learning import farthest_points
>>> farthest_points(Points([[0.0], [1.0]]), Points([[0.0]]), chunk_size=1).parameters.tolist()
[[1.0]]
torch_bsf.active_learning.query_by_committee(ensemble: Ensemble, candidates: Points, n: int = 1) → Points[source]

Select candidates by mean population variance across committee outputs.

Ties preserve candidate order. Finite predictions are scored on CPU in float64 with exponent scaling to avoid variance overflow and underflow. This preserves relative output units and common-scalar ranking invariance, subject to rounding in the supplied predictions. Nearly tied nonzero scores use exact rational comparisons of those predictions, at additional CPU cost. Nonfinite predictions raise ValueError. Returned points retain their input gradients.

optimization

Use minimize when evaluating a differentiable cost instead of observing target responses. Supply an initial model, loss and typed method. Check objective and stopping evidence in the optimization result. Sampling and minibatch settings are explained in Objective minimization.

Objective minimization, separate from supervised least-squares regression.

class torch_bsf.optimization.Iteration(iteration: int, loss: float, gradient_norm: float)[source]

Bases: object

Pre-update minibatch loss and gradient norm at a zero-based iteration.

class torch_bsf.optimization.OptimizationResult(model: BezierModel, history: tuple[Iteration, ...], success: bool, status: str, message: str, objective: float, n_iterations: int, seed: int | None)[source]

Bases: object

An optimized copy, history and an honest stopping state.

Budget exhaustion and a user callback are not convergence certificates. Only all-fixed or a checked full-sample gradient tolerance yield success.

require_success() → OptimizationResult[source]

Raise if no convergence criterion was established.

class torch_bsf.optimization.PreconditionedSGD(max_iter: int = 100, step_size: float = 0.05, batch_size: int | None = None, preconditioner: Literal['gram', 'none'] = 'gram', step_decay: Literal['constant', 'sqrt'] = 'constant', gradient_tolerance: float | None = None)[source]

Bases: object

Domain-metric SGD with a fixed sample set and uniform random minibatches.

torch_bsf.optimization.gram_matrix(space: BezierSpace, *, cell_weights: Mapping[int | Simploid, float] | None = None) → Tensor[source]

Compute the exact Bernstein Gram matrix for the sampling distribution.

torch_bsf.optimization.linear_scalarization(objective: Callable[[Tensor], Tensor]) → Callable[[Tensor, Points], Tensor][source]

Turn differentiable objective values into a simplex-weighted loss.

torch_bsf.optimization.minimize(model: BezierModel, loss: Callable[[Tensor, Points], Tensor], *, method: PreconditionedSGD = PreconditionedSGD(max_iter=100, step_size=0.05, batch_size=None, preconditioner='gram', step_decay='constant', gradient_tolerance=None), points: Points | None = None, n_samples: int = 100, seed: int | None = None, cell_weights: Mapping[int | Simploid, float] | None = None, penalty: QuadraticPenalty = QuadraticPenalty(ridge=0.0, smoothness=0.0), fixed: FixedControls | None = None, callback: Callable[[Iteration, BezierModel], bool | None] | None = None) → OptimizationResult[source]

Minimize a differentiable loss on a copied model with domain-aware SGD.

Parameters:
  • model – Initial model; never mutated.

  • loss – loss(predicted_values, points) returning one loss per sample.

  • method – Typed preconditioned SGD settings, independent of Lightning. Float16 and bfloat16 initial models are copied and promoted to float32 on the same device for basis evaluation and gradients. Metric solves and update arithmetic use float64 before casting back to the model dtype. On MPS, float64 penalties and solves run on CPU with differentiable transfers. The returned model then has float32 coefficients.

  • points – Fixed samples. Otherwise draw n_samples with the stated cell weights.

  • n_samples – Sampling count, local random seed and explicit cell distribution.

  • seed – Sampling count, local random seed and explicit cell distribution.

  • cell_weights – Sampling count, local random seed and explicit cell distribution.

  • penalty – Quadratic penalties and exact fixed coefficient values.

  • fixed – Quadratic penalties and exact fixed coefficient values.

  • callback – Receives a pre-update Iteration and the current model. Returning True stops without claiming convergence.

Returns:

Copied model, pre-update history, final full-sample objective and stopping state.

Return type:

OptimizationResult

Raises:
  • ValueError – The loss does not return one finite value per sample.

  • ArithmeticError – The combined objective, gradient or coefficient update is non-finite, or the step-scaled metric matrix cannot be represented in float64. Zero-weight penalties are not evaluated. Active penalties accumulate in float64 and apply weights before the final product, while gradients retain the model dtype. Truly unrepresentable results raise on every stopping path.

interpolation

SimplicialMesh compiles user-supplied simplices into a cubic BezierSpace whose shared faces share controls, and locates points in it. hermite_interpolant returns a continuous piecewise cubic BezierModel through vertex data that, when clamped, stays within the data range of each simplex. macro_interpolant chooses how simplices are split (NoSplit, CloughTocher) and how smoothly the pieces join (C0, C1); on triangle meshes, it builds a C1 MacroInterpolant with the same range guarantee. lattice_mesh triangulates a simplex lattice conformingly, and refine bisects simplices while keeping the mesh conforming. See Shape-preserving interpolation on simplicial meshes for the construction and its error bound.

Shape-preserving cubic Bézier interpolation on simplicial complexes (“simplicial PCHIP”).

A SimplicialMesh glues user-supplied simplices of \(\mathbb{R}^d\) into one BezierSpace, so that simplices sharing a face share the controls on it. hermite_interpolant() fills that space with a cubic Bézier model that interpolates vertex data:

  • a vertex control is the datum \(f_i\);

  • the control on edge \(i \to j\) next to vertex \(i\) is \(f_i + g_i \cdot (x_j - x_i) / 3\), with a vertex gradient \(g_i\);

  • the control at the centre of face \((i, j, l)\) is the sum of the six edge controls of the face divided by four, minus \((f_i + f_j + f_l) / 6\), which reproduces quadratic data exactly.

Every control depends only on the data of the edge or face it lies on, so the interpolant is continuous across shared faces. With clamp=True every edge (face) control is clipped into the range of the data at the vertices of that edge (face). By the convex-hull property, the prediction on each simplex then stays within the range of its vertex data, which bounds the sup error by the modulus of continuity of the data at the mesh size. In one dimension this is the range limiter of piecewise cubic Hermite interpolation (PCHIP) [FC80].

class torch_bsf.interpolation.C0[source]

Bases: object

Continuous joins between neighbouring pieces (a continuity for macro_interpolant()).

class torch_bsf.interpolation.C1[source]

Bases: object

Continuously differentiable joins between neighbouring pieces (a continuity for macro_interpolant()).

class torch_bsf.interpolation.CloughTocher[source]

Bases: object

Split every triangle into three at its incenter: the Clough–Tocher split (a split for macro_interpolant()).

Two dimensions only. The incenter rather than the centroid is used because the segment joining the split points of two triangles sharing an edge then crosses that edge strictly inside it, which C1 relies on to keep every piece within the range of the data of its triangle. It is the incenter after each axis is divided by a power of two near the longest side of the mesh along it, which keeps that property (an affine map does) and makes the split, like the mesh, independent of the units of the axes. Where those powers of two are equal for both axes (e.g. on lattice_mesh()), it is the Euclidean incenter.

class torch_bsf.interpolation.MacroInterpolant(mesh: SimplicialMesh, pieces: SimplicialMesh, parent: Tensor, model: BezierModel, *, _transfer: Callable[[Points], Points])[source]

Bases: object

A piecewise cubic interpolant on a mesh, defined on pieces of its simplices (see macro_interpolant()).

mesh[source]

The mesh carrying the data.

Type:

torch_bsf.interpolation.SimplicialMesh

pieces[source]

The mesh of the pieces, on whose space the model is defined. It is mesh itself without a split.

Type:

torch_bsf.interpolation.SimplicialMesh

parent[source]

parent[k] is the simplex of mesh containing piece k.

Type:

torch.Tensor

model[source]

The float64 model. Evaluate it at locate(), or at transfer() of points of mesh.space.

Type:

torch_bsf.core.BezierModel

locate(x: Any, *, tol: float = 1e-08, batch_size: int = 1024) → Points[source]

Find the piece containing each point and its barycentric coordinates there.

The points are located in mesh by SimplicialMesh.locate(), and then in the pieces of their simplex by transfer().

Parameters:
Returns:

Barycentric coordinates and cell IDs in pieces.space, in the order of x.

Return type:

Points

Raises:

ValueError – If a point is invalid or outside the mesh (see SimplicialMesh.locate()).

transfer(points: Points) → Points[source]

Map points of mesh.space to the pieces containing them, preserving their order.

Parameters:

points – Barycentric coordinates in simplices of mesh and their cell IDs (from SimplicialMesh.cell_ids), e.g. from SimplicialMesh.locate().

Returns:

Barycentric coordinates and cell IDs in pieces.space.

Return type:

Points

Raises:

ValueError – If points are not valid points of mesh.space (see validate()), or a cell ID is not that of a simplex of mesh (e.g. a face, or a simplex replaced by refine()).

Examples

The interpolant of the first coordinate reproduces it, here at the centroids of the triangles:

>>> import torch
>>> from torch_bsf.core import Points
>>> from torch_bsf.interpolation import C1, CloughTocher, lattice_mesh, macro_interpolant
>>> mesh = lattice_mesh(2, 2)
>>> interpolant = macro_interpolant(mesh, mesh.vertices[:, 0], split=CloughTocher(), continuity=C1())
>>> centroids = Points(torch.full((mesh.n_simplices, 3), 1 / 3, dtype=torch.float64), mesh.cell_ids)
>>> values = interpolant.model(interpolant.transfer(centroids))[:, 0]
>>> torch.allclose(values, mesh.vertices[mesh.simplices, 0].mean(dim=1))
True
class torch_bsf.interpolation.NoSplit[source]

Bases: object

Interpolate on the simplices of the mesh as they are (a split for macro_interpolant()).

class torch_bsf.interpolation.SimplicialMesh(vertices: Any, simplices: Any, *, strict: bool = True)[source]

Bases: object

A conforming simplicial complex in \(\mathbb{R}^d\) compiled into a cubic BezierSpace.

Parameters:
  • vertices – Vertex coordinates of shape (n_vertices, d), d >= 1.

  • simplices – Vertex numbers of shape (n_simplices, d + 1). The simplices must be non-degenerate and meet face to face (conforming); the mesh is not generated here. A Delaunay triangulation of points in general position is conforming; one of lattice points with its flat simplices removed is not (use lattice_mesh() instead). A simplex counts as degenerate when its volume is at most 1e-12 times the product of its edge lengths both in the given coordinates and with each axis divided by the extent of the simplex along it (rounded down to a power of two), so that neither the units of the axes nor the scale of the coordinates matters.

  • strict – Whether a non-conforming mesh raises ValueError. If False, a warning is issued instead and unmatched_faces reports the defect.

vertices[source]

Float64 snapshot of the vertex coordinates, independent of the caller’s storage.

simplices[source]

Each simplex with its vertex numbers sorted in ascending order. The barycentric coordinates of cell k (as returned by locate()) refer to the vertices simplices[k] in this order.

space[source]

The compiled degree-3 space, in which simplices sharing a face share the controls on it.

cell_ids[source]

cell_ids[k] is the cell ID of simplex k in space.

Notes

Construction copies vertex and simplex inputs, including CPU float64 tensors and NumPy arrays. Treat the public vertices and simplices tensors as read-only: direct in-place edits are unsupported because geometric caches and the compiled space are not rebuilt. Construct a new mesh to change its geometry or topology. The vertex copy preserves autograd connections to tensor inputs.

Examples

Two triangles sharing the diagonal of the unit square:

>>> import torch
>>> from torch_bsf.interpolation import SimplicialMesh
>>> mesh = SimplicialMesh([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]], [[0, 1, 2], [3, 2, 1]])
>>> mesh.simplices.tolist()
[[0, 1, 2], [1, 2, 3]]
>>> points = mesh.locate(torch.tensor([[0.25, 0.25], [0.75, 0.75]]))
>>> points.cell_ids.tolist() == mesh.cell_ids.tolist()
True
>>> points.parameters[0].tolist()
[0.5, 0.25, 0.25]
barycentric(x: Any) → Tensor[source]

Barycentric coordinates of points with respect to every simplex.

Parameters:

x – Points of shape (n_points, d).

Returns:

Coordinates of shape (n_points, n_simplices, d + 1), in the vertex order of simplices. They sum to one and are all nonnegative exactly when the point lies in the simplex.

Return type:

torch.Tensor

Notes

The usual path uses the equilibrated float64 inverse. If its condition estimate indicates excessive factorization error, an absolute-product estimate of roundoff exceeds 1e-12 * (1 + abs(coordinate)), or the solve overflows, the affected point/simplex pairs are evaluated with exact rational arithmetic on the input floats and rounded once to float64. This slower path caches one exact inverse per affected simplex. It recovers coordinates lost by the solve, but cannot recover information already lost when the input points were rounded. Differentiable affected pairs additionally use cached SymPy expressions. Their derivatives are evaluated exactly at every order before rounding, independently for each point. Symbolic work and retained autograd state grow with dimension and derivative order; this path is intentionally slower. Forward-mode queries with any affected pair use symbolic expressions for the whole query so that forward_ad and torch.func.jvp retain tangents without reading dual tensor storage in Python.

property dimension: int[source]

Dimension d of the ambient space and of every simplex.

locate(x: Any, *, tol: float = 1e-08, batch_size: int = 1024) → Points[source]

Find the simplex containing each point and its barycentric coordinates there.

Every simplex is tested (brute force). A point on a shared face is assigned to one of the simplices containing it; the prediction does not depend on that choice because the interpolant is continuous.

Parameters:
  • x – Points of shape (n_points, d).

  • tol – Points whose barycentric coordinates are at least -tol in some simplex are accepted. Their coordinates are clipped to be nonnegative and renormalized, which moves points just outside the mesh onto its boundary.

  • batch_size – Number of points tested at a time, bounding temporary float64 storage by O(batch_size * n_simplices * (d + 1)). Exact arithmetic for cancellation-prone pairs additionally caches O(d**2) rational coefficients per affected simplex, whose integer sizes depend on the input exponents. Those pairs are processed sequentially without an array of rational coordinates. Differentiable queries additionally retain symbolic expression caches and autograd state.

Returns:

Barycentric coordinates and cell IDs in space, in the order of x.

Return type:

Points

Raises:

ValueError – If a point lies outside the mesh by more than tol.

property n_simplices: int[source]

Number of simplices.

property n_vertices: int[source]

Number of vertices.

neighbors() → list[list[int]][source]

The vertices sharing an edge with each vertex (its one-ring), in ascending order.

property unmatched_faces: int[source]

Number of faces that break conformity (0 for a conforming mesh).

A face of dimension d - 1 is counted if it belongs to more than two simplices, or if it belongs to one simplex and overlaps, in a set of positive \((d - 1)\)-volume, another such face in the same hyperplane (a hanging vertex, or a flat polygon split differently on its two sides). The overlap is tested over the whole faces, however thin it is, and faces count as in the same hyperplane only if they are so up to rounding. Rounding here is d units of rounding (machine epsilon) of the larger diameter plus the largest absolute coordinate of the two faces, which is how faces meeting at a common vertex or edge touch after rounding. In 1-D, where faces are points given as input, only equal points overlap. There is no other tolerance. Faces of the same simplex are never compared. Faces on the meshed domain are not counted, even when another part of the mesh lies close to them or touches them at a lower-dimensional face. Overlapping simplices that share no face are not detected. A mesh without boundary faces (every face in two or more simplices, which only overlapping simplices allow) has all its faces counted.

volumes() → Tensor[source]

Volumes of the simplices, of shape (n_simplices,).

torch_bsf.interpolation.estimate_gradients(mesh: SimplicialMesh, values: Any, *, max_rings: int = 4) → Tensor[source]

Estimate the gradient of vertex data at every vertex from its mesh neighbours.

At each vertex, a quadratic polynomial is fitted by least squares with weights \(1 / |H^{-1}(x_w - x_v)|^2\) to the data on the smallest \(k\)-ring neighbourhood (\(2 \le k \le\) max_rings) that contains at least two more points than the quadratic has unknowns. If no such neighbourhood exists, a linear polynomial is fitted on the one-ring instead. The quadratic fit reproduces the gradient of quadratic data exactly, and either fit reproduces that of linear data. Vertices that belong to no simplex get a zero gradient.

\(H\) is the diagonal matrix of the largest \(|x_w - x_v|\) along each axis over the neighbourhood, so that the estimate does not depend on the units of the axes: scaling an axis by \(c\) divides that component of the gradient by \(c\), even for axes of very different scales.

Parameters:
  • mesh – The mesh.

  • values – Vertex data of shape (n_vertices, n_outputs) or (n_vertices,).

  • max_rings – The largest neighbourhood to try.

Returns:

Float64 gradients of shape (n_vertices, d, n_outputs). Components beyond the float64 range are returned as signed infinity, without a warning. Finite vertex data can produce such gradients, for example on very short edges.

Return type:

torch.Tensor

Examples

>>> import torch
>>> from torch_bsf.interpolation import SimplicialMesh, estimate_gradients
>>> x = torch.linspace(0, 1, 6)[:, None]
>>> mesh = SimplicialMesh(x, [[k, k + 1] for k in range(5)])
>>> g = estimate_gradients(mesh, x.square())
>>> torch.allclose(g[:, 0, 0], 2 * x[:, 0].double())
True
torch_bsf.interpolation.hermite_interpolant(mesh: SimplicialMesh, values: Any, *, gradients: Any = None, clamp: bool = True) → BezierModel[source]

Interpolate vertex data by a continuous piecewise cubic Bézier model on a simplicial mesh.

The model reproduces the data at the vertices. Without clamping, it reproduces quadratic data exactly when the gradients do (for example, with enough neighbours for estimate_gradients()). With clamp=True, on every simplex and for every output, the prediction lies between the smallest and largest data at the vertices of that simplex; in one dimension, monotone data then give a monotone interpolant.

Parameters:
  • mesh – The mesh.

  • values – Vertex data of shape (n_vertices, n_outputs) or (n_vertices,).

  • gradients – Vertex gradients of shape (n_vertices, d, n_outputs). Estimated with estimate_gradients() if None.

  • clamp – Whether to clip edge and face controls into the range of their vertex data (shape preservation).

Returns:

A float64 model on mesh.space. Evaluate it at mesh.locate(x).

Return type:

BezierModel

Raises:

ValueError – If the data or supplied gradients are invalid, or the interpolant’s control values are non-finite because the data or estimated gradients cause float64 overflow. Clamping can clip infinite controls into the data range, but cannot repair NaN controls.

Examples

>>> import torch
>>> from torch_bsf.interpolation import SimplicialMesh, hermite_interpolant
>>> x = torch.tensor([[0.0], [1.0], [2.0], [3.0]])
>>> mesh = SimplicialMesh(x, [[0, 1], [1, 2], [2, 3]])
>>> model = hermite_interpolant(mesh, torch.tensor([0.0, 0.0, 1.0, 1.0]))
>>> y = model(mesh.locate(torch.linspace(0, 3, 31)[:, None]))
>>> bool((y.diff(dim=0) >= 0).all() and y.min() >= 0 and y.max() <= 1)
True
torch_bsf.interpolation.lattice_mesh(dim: int, k: int) → SimplicialMesh[source]

The Kuhn (Freudenthal) triangulation of the degree-k lattice of the standard dim-simplex.

The vertices are the lattice points \(a / k\) with \(a \in \mathbb{N}^{d + 1}\) and \(|a| = k\), in the order of grid() with resolution=k. Their coordinates are the first dim barycentric coordinates (the last one is one minus their sum). The k ** dim simplices have equal volume, have lattice points as vertices and meet face to face. They are not congruent for dim >= 3, because the map from the staircase coordinates below to these coordinates is not an isometry. In the staircase coordinates \(z_j = a_0 + \dots + a_j\) (\(j < d\)) the lattice is \(\{0 \le z_0 \le \dots \le z_{d-1} \le k\}\), and the simplices are the Kuhn simplices \((c, c + e_{\pi(1)}, c + e_{\pi(1)} + e_{\pi(2)}, \dots)\) of unit cubes whose vertices all lie in it. The mesh records these paths, so that refine() with rule="newest_vertex" can bisect each simplex across the diagonal of its cube.

Lattice points are co-spherical, so their Delaunay triangulation is not unique; qhull returns flat simplices, and removing them leaves a mesh that is not conforming. Use this mesh instead.

Parameters:
  • dim – Dimension of the simplex, at least one.

  • k – Lattice degree (number of intervals per edge), at least one.

Returns:

A conforming mesh with comb(k + dim, dim) vertices and k ** dim simplices.

Return type:

SimplicialMesh

Examples

>>> from torch_bsf.interpolation import lattice_mesh
>>> mesh = lattice_mesh(2, 3)
>>> mesh.n_vertices, mesh.n_simplices, mesh.unmatched_faces
(10, 9, 0)
>>> float(mesh.volumes().sum())
0.5
torch_bsf.interpolation.macro_interpolant(mesh: SimplicialMesh, values: Any, *, split: NoSplit | CloughTocher = NoSplit(), continuity: C0 | C1 = C0(), gradients: Any = None, clamp: bool = True) → MacroInterpolant[source]

Interpolate vertex data by a piecewise cubic Bézier model on pieces of the simplices of a mesh.

split says how each simplex is divided into pieces, and continuity how smoothly neighbouring pieces join. The supported constructions are

  • NoSplit() with C0() (any dimension): hermite_interpolant();

  • CloughTocher() with C1() (two dimensions): the Clough–Tocher macro-element, continuously differentiable, on three cubic pieces per triangle.

Both interpolate the data and reproduce quadratic data without clamping when the gradients do. With clamp=True, on every simplex of mesh and for every output, the prediction lies between the smallest and largest data at its vertices. For the C1 construction, the gradients are first limited for this, each projected onto the gradients keeping the controls next to its vertex in the range of every triangle containing them, which keeps the interpolant C1. In the spirit of [FC80], this is a two-dimensional range-restricted C1 interpolant. The set of such gradients can shrink abruptly as the mesh moves. For example, take a vertex whose datum is the largest datum of the triangle on one side of each of two of its edges and the smallest of the triangle on the other side. Its gradient must be orthogonal to both edges: while they are parallel, it is free along their common normal; once they are not, it must vanish. The clamped C1 interpolant therefore need not depend continuously on the vertex coordinates.

The construction is not differentiated with respect to the mesh: like every BezierModel, the model copies its coefficients without their autograd history, as for hermite_interpolant(), and the split points are computed from the vertices without it too. Derivatives with respect to the vertices therefore pass only through SimplicialMesh.locate().

Parameters:
  • mesh – The mesh.

  • values – Vertex data of shape (n_vertices, n_outputs) or (n_vertices,). For the C1 construction, an output with data beyond about 2**1000 (or, without clamp, controls beyond it) is processed in units of a power of two up to 2**24, so that its sums stay finite; its subnormal data are then interpolated only up to an absolute error below 2**-1050.

  • split – How each simplex is divided into pieces: NoSplit or CloughTocher.

  • continuity – How smoothly neighbouring pieces join: C0 or C1.

  • gradients – Vertex gradients of shape (n_vertices, d, n_outputs). Estimated with estimate_gradients() if None. With clamp and the C1 construction, a gradient whose controls exceed about 2**1000 (near float64 overflow) is divided by a power of two before it is limited.

  • clamp – Whether to keep the prediction on every simplex within the range of its vertex data.

Returns:

The interpolant: call it at points, or evaluate its model at its locate().

Return type:

MacroInterpolant

Raises:
  • TypeError – If split or continuity is not one of the classes above.

  • ValueError – If the construction is not supported, the mesh is not two-dimensional for CloughTocher or has a triangle whose split point rounds onto one of its sides, the data or supplied gradients are invalid, or the interpolant is not finite in float64 (see hermite_interpolant()).

Examples

>>> import torch
>>> from torch_bsf.interpolation import C1, CloughTocher, lattice_mesh, macro_interpolant
>>> mesh = lattice_mesh(2, 3)
>>> x = mesh.vertices
>>> interpolant = macro_interpolant(mesh, x[:, 0] * x[:, 1], split=CloughTocher(), continuity=C1())
>>> interpolant.pieces.n_simplices == 3 * mesh.n_simplices
True
>>> y = interpolant(x)
>>> torch.allclose(y[:, 0], x[:, 0] * x[:, 1])
True
torch_bsf.interpolation.refine(mesh: SimplicialMesh, simplices: Sequence[int], *, rule: Literal['longest_edge', 'newest_vertex'] = 'longest_edge') → SimplicialMesh[source]

Bisect simplices at the midpoint of their refinement edge, keeping the mesh conforming.

An edge is bisected in every simplex that contains it, which keeps the mesh conforming. Before an edge is bisected, every simplex containing it whose refinement edge is a different one is refined first, recursively, so that cells are always cut across their own refinement edge. Refining one simplex may therefore add several vertices. The refinement edge depends on rule:

  • "longest_edge" (Rivara): the longest edge, measured in the vertex coordinates. Ties between edges of equal length are broken by their vertex numbers, consistently over the whole mesh.

  • "newest_vertex" (Maubach [Mau95]): every simplex is an ordered vertex tuple \((x_0, \dots, x_d)\) with a tag \(k \in \{1, \dots, d\}\), its refinement edge is \(x_0 x_k\), and the children \((x_0, \dots, x_{k-1}, z, x_{k+1}, \dots, x_d)\) and \((x_1, \dots, x_k, z, x_{k+1}, \dots, x_d)\) get the tag \(k - 1\) (\(d\) after 1). It starts from a mesh of lattice_mesh(), whose simplices are the Kuhn paths with tag \(d\), and the refined mesh carries the ordered simplices and tags for the next call. It cuts a Kuhn simplex across the diagonal of its cube, so dim rounds of refining every simplex of lattice_mesh(dim, k) give a triangulation of the degree-2k lattice, and the simplices take only finitely many shapes up to similarity [Mau95]. Longest-edge bisection of a Kuhn mesh does neither: the longest edge of a Kuhn simplex is not the cube diagonal in the coordinates of lattice_mesh(), so rounds of refinement do not return to a lattice mesh and closure adds vertices that were not asked for.

Parameters:
  • mesh – A conforming mesh. For rule="newest_vertex", a mesh returned by lattice_mesh() or by this function with rule="newest_vertex".

  • simplices – Integer indices into mesh.simplices of the simplices to bisect.

  • rule – "longest_edge" (default) or "newest_vertex".

Returns:

The refined mesh. It keeps the vertices of mesh with their numbers and appends the new vertices (edge midpoints) in the order they were created. Its simplices are sorted.

Return type:

SimplicialMesh

Raises:

ValueError – If rule is not one of the strings "longest_edge" and "newest_vertex", if an index is not one of a simplex, or if rule="newest_vertex" is used on a mesh that carries no newest-vertex state (e.g. one built directly with SimplicialMesh).

Examples

>>> from torch_bsf.interpolation import lattice_mesh, refine
>>> coarse = lattice_mesh(2, 2)
>>> fine = refine(coarse, [0])
>>> fine.n_vertices - coarse.n_vertices, fine.n_simplices - coarse.n_simplices, fine.unmatched_faces
(1, 2, 0)

Two rounds of newest-vertex bisection of every triangle double the lattice degree:

>>> mesh = lattice_mesh(2, 2)
>>> for _ in range(2):
...     mesh = refine(mesh, range(mesh.n_simplices), rule="newest_vertex")
>>> mesh.n_vertices, mesh.n_simplices, mesh.unmatched_faces
(15, 16, 0)
>>> sorted(set((4 * mesh.vertices).flatten().tolist()))
[0.0, 1.0, 2.0, 3.0, 4.0]
torch_bsf.interpolation.refinement_indicator(mesh: SimplicialMesh, values: Any, *, gradients: Any = None, clamp: bool = True, split: NoSplit | CloughTocher = NoSplit(), continuity: C0 | C1 = C0()) → Tensor[source]

Score simplices for refinement by the cubic correction to linear interpolation.

The score of a simplex is its volume times the mean over outputs of the absolute difference between the cubic interpolant (macro_interpolant(), by default hermite_interpolant()) and linear interpolation at its centroid. It is large where the data bend within a large simplex. Refine the highest-scoring simplices first, for example with refine(), and evaluate the response at the new vertices.

Parameters:
  • mesh – The mesh.

  • values – Vertex data of shape (n_vertices, n_outputs) or (n_vertices,).

  • gradients – Vertex gradients, as in hermite_interpolant().

  • clamp – Whether the cubic interpolant is clamped, as in hermite_interpolant().

  • split – The construction of the cubic interpolant, as in macro_interpolant().

  • continuity – The construction of the cubic interpolant, as in macro_interpolant().

Returns:

Nonnegative float64 scores of shape (n_simplices,), one per simplex of mesh. Finite centroid predictions and extreme vertex data use exact reductions near the float64 limits, applying the volume before final rounding. A score beyond float64 is positive infinity.

Return type:

torch.Tensor

Raises:
  • TypeError – If split or continuity is not one of the classes of macro_interpolant().

  • ValueError – If macro_interpolant() cannot construct a finite interpolant, including when the vertex data or gradients cause its controls to overflow float64, or if a centroid prediction is non-finite.

Examples

>>> import torch
>>> from torch_bsf.interpolation import SimplicialMesh, refinement_indicator
>>> x = torch.arange(6.0)[:, None]
>>> mesh = SimplicialMesh(x, [[k, k + 1] for k in range(5)])
>>> [round(s, 6) for s in refinement_indicator(mesh, x.square()).tolist()]
[0.25, 0.25, 0.25, 0.25, 0.25]

splitting

Subdivision reparameterizes a model on smaller cells while preserving its represented function. Keep the child-to-parent coordinate maps when comparing values. Refitting child models is a separate operation that can change the function; see Exact local subdivision.

Exact local subdivision with explicit maps back to the parent domain.

class torch_bsf.splitting.EdgeSplit(i: int, j: int, fraction: float = 0.5, factor: int = 0)[source]

Bases: object

Split one factor edge at a fraction strictly between zero and one.

class torch_bsf.splitting.LongestEdge(fraction: float = 0.5)[source]

Bases: object

Select the longest chord of the selected cell’s control-net vertices.

Scaled float64 length comparisons support finite extreme endpoints and subnormal distances. Uniform output scaling preserves selection except for rounding near ties or lost input precision. Non-finite corner values raise ValueError. Equal lengths use factor/vertex lexicographic order.

class torch_bsf.splitting.ParameterMap(parent_cell: int, dimensions: tuple[int, ...], split: EdgeSplit, part: Literal['A', 'B'])[source]

Bases: object

An affine child-to-parent map and its partial inverse.

from_parent(points: Points) → tuple[Points, Tensor][source]

Return child coordinates and a mask selecting parent rows in this piece.

to_parent(points: Points | Tensor) → Points[source]

Map child barycentric points to their parent cell.

class torch_bsf.splitting.ResidualSplit(samples: Samples, resolution: int = 10)[source]

Bases: object

Minimize the sum of unweighted child MSEs over a split-fraction grid.

Exact subdivision preserves the represented function. This policy balances residual grouping; it does not refit the children or guarantee error reduction.

class torch_bsf.splitting.SpaceSubdivision(model: BezierModel, parents: tuple[int, ...], split: EdgeSplit, _refinement: Refinement, _original: BezierSpace, _nodes: tuple[Simploid, ...])[source]

Bases: object

An exact conforming subdivision of a whole space and the map of points onto it.

model[source]

The subdivided model, representing the same function on a refined space.

Type:

torch_bsf.core.BezierModel

parents[source]

For every cell of the refined space, the cell of the original space whose relative interior contains it (0 for a single-cell space, whose faces are not cells).

Type:

tuple[int, …]

split[source]

The split of the selected cell chosen by the policy.

Type:

torch_bsf.splitting.EdgeSplit

transfer(points: Points) → Points[source]

Map points of the original space to the refined space, preserving their order.

A point on a boundary shared by several pieces goes to one of them; every such choice evaluates to the same value, since the subdivision is conforming.

class torch_bsf.splitting.SubdivisionResult(models: tuple[BezierModel, BezierModel], maps: tuple[ParameterMap, ParameterMap])[source]

Bases: object

Two exact local models and their parent parameter maps.

This does not remesh neighboring cells of an assembled domain.

torch_bsf.splitting.subdivide(model: BezierModel, *, cell: int = 0, policy: EdgeSplit | LongestEdge | ResidualSplit = LongestEdge(fraction=0.5)) → SubdivisionResult[source]

Split a selected cell exactly, retaining its output dtype and device.

torch_bsf.splitting.subdivide_space(model: BezierModel, *, cell: int = 0, policy: EdgeSplit | LongestEdge | ResidualSplit = LongestEdge(fraction=0.5)) → SpaceSubdivision[source]

Split a cell exactly and propagate the split so that the whole space stays conforming.

The selected factor edge is split at fraction, and so is every factor edge of the space parallel to it through shared faces: the edges of the same factor in neighboring cells across shared faces and, through a degenerate face, the edge of its base it maps onto. Each affected cell is bisected along all of its marked edges, so a shared face is subdivided in the same way from every side and the result is again a simploidal Bézier space: closed under splitting, including spaces with degenerate cells, for any fraction in \((0, 1)\). The coefficients are computed by de Casteljau subdivision, so the refined model represents the same function.

Parameters:
  • model – The model to subdivide.

  • cell – The cell whose edge selects the split.

  • policy – How to choose the edge of cell, as in subdivide().

Return type:

The refined model with the parent of every refined cell and a map of points onto the refined space.

Examples

Split one triangle of a square made of two triangles; the edge shared by both triangles is split, so the neighbor is split too:

>>> import torch
>>> from torch_bsf import BezierModel, BezierSpace, Points
>>> from torch_bsf.geometry import SimploidalSet
>>> from torch_bsf.splitting import EdgeSplit, subdivide_space
>>> S = SimploidalSet()
>>> a, b = S.add_simplex(2), S.add_simplex(2)
>>> _ = S.identify(a.face(0, 0), b.face(0, 0))
>>> space = BezierSpace.on_set(S, degree=2)
>>> model = BezierModel(space, torch.randn(space.n_controls, 1, dtype=torch.float64))
>>> result = subdivide_space(model, cell=space.cell_id(a), policy=EdgeSplit(1, 2, 0.3))
>>> len(result.model.space.main_cells)
4
>>> points = Points(torch.tensor([[0.2, 0.3, 0.5]], dtype=torch.float64), [space.cell_id(b)])
>>> torch.allclose(model(points), result.model(result.transfer(points)))
True

io

save_model and load_model preserve compiled geometry and coefficients in a versioned archive independent of filename extension. Control-point export is a data export rather than a replacement for topology-aware persistence. Store application parameter transforms alongside the archive.

Versioned model persistence for both single-cell and glued-cell spaces.

torch_bsf.io.export_control_points(path: str | Path, model: BezierModel) → None[source]

Export coefficient rows as CSV data, explicitly without pretending to save geometry.

torch_bsf.io.load_model(path: str | Path) → BezierModel[source]

Restore a validated model without importing a training framework.

Original mutable builder handles are not persisted. Loaded cells are addressed by the same stable integer IDs recorded in the model schema.

torch_bsf.io.save_model(path: str | Path, model: BezierModel) → None[source]

Save an explicit schema and non-pickled coefficient array atomically.

The format is a ZIP containing model.json and coefficients.npy, independent of the filename extension and Python class import paths.

Parameters:
  • path – Destination file; its parent directory must exist.

  • model – Any unified Bézier model, including a glued multi-cell space.

plotting

plot_model displays selected output coordinates and optional control nets. Rendering density changes the evaluated mesh, not the fitted coefficients. Higher-dimensional cells are shown through two-dimensional faces; inspect interior residuals separately.

A single plotting entry point for single- and multiple-cell models.

torch_bsf.plotting.plot_model(model: BezierModel, *, resolution: int = 20, coordinates: Sequence[int] | None = None, ax: Any = None, show_controls: bool = True, **kwargs: Any) → Any[source]

Draw maximal cells and their control nets on shared Matplotlib axes.

Select two or three output coordinates for higher-dimensional models. Domains above dimension two are shown through their two-dimensional faces. Returns the axes. Matplotlib is imported only when plotting.

sklearn

BezierRegressor adapts the common fit to NumPy arrays and estimator workflows. Features must encode valid barycentric blocks. Generic feature standardization generally violates those constraints. See Scikit-learn integration for degree searches and output shapes.

Scikit-learn integration using the same numerical problem and results.

class torch_bsf.sklearn.BezierRegressor(degree: int | Sequence[int] = 3, *, dimensions: Sequence[int] | None = None, solver: DenseLeastSquares | SparseLeastSquares = DenseLeastSquares(rcond=None, diagnostics='basic'), penalty: QuadraticPenalty = QuadraticPenalty(ridge=0.0, smoothness=0.0), fixed: FixedControls | None = None)[source]

Bases: RegressorMixin, BaseEstimator

Fit a single simplex or product using explicit numerical settings.

Parameters:
  • degree – Polynomial degree, or one degree per simplex factor.

  • dimensions – Factor dimensions; inferred as a single simplex from X when omitted.

  • solver – The same typed settings accepted by torch_bsf.fit().

  • penalty – The same typed settings accepted by torch_bsf.fit().

  • fixed – The same typed settings accepted by torch_bsf.fit().

result_, model_, diagnostics_

Complete fit evidence, fitted model, and numerical diagnostics.

fit(X: Any, y: Any, sample_weight: Any = None) → BezierRegressor[source]

Fit weighted observations and expose their complete FitResult.

predict(X: Any) → NDArray[Any][source]

Predict with the fitted model; preserve the target’s original rank.

integrations.lightning

The optional Lightning adapter’s regression_module trains the common model on supervised samples through a Lightning Trainer. See Solvers, performance and optional execution for a complete single-device Trainer example and its training settings. Use the core fit for a direct linear least-squares solve. Objective-only optimization uses torch_bsf.minimize() with plain PyTorch and does not require Lightning; see Objective minimization.

Opt-in Lightning execution for the common weighted regression objective.

torch_bsf.integrations.lightning.regression_module(model: BezierModel, samples: Samples, *, learning_rate: float = 0.01, penalty: QuadraticPenalty = QuadraticPenalty(ridge=0.0, smoothness=0.0), fixed: FixedControls | None = None) → Any[source]

Create a LightningModule from an explicit initial model and observations.

The caller owns Trainer settings and a one-item DataLoader (e.g. [0]). Each step uses the complete normalized weighted objective, so batch and device partitioning cannot silently change regularization. This adapter supports one device; use core least squares for deterministic regression. Its bezier_model() method returns an independent CPU float64 model.

integrations.mlflow

The optional MLflow integration records experiments without making the numerical core depend on a tracking service. Retain the parameter schema, units and reference-problem definition with recorded metrics so that artifacts can be interpreted outside their original run.

Explicit MLflow recording, independent of core numerical execution.

torch_bsf.integrations.mlflow.log_result(result: FitResult, *, tracking_uri: str | None = None) → str[source]

Record metrics, diagnostics and the versioned model artifact in a run.

Reuse an active run; otherwise start and finish one. Requires the mlflow extra. Returns the run ID. No autologging or pickled Python models are used. Unavailable placeholder models are never written; failure metrics and diagnostics are still recorded with bsf.solution_available=False.