torch_bsf package

Submodules

torch_bsf.active_learning module

torch_bsf.active_learning.suggest_next_points(models: Sequence[Module] | ModuleList, n_suggestions: int = 1, n_candidates: int = 1000, method: Literal['qbc', 'density'] = 'qbc', params: Tensor | None = None, n_params: int | None = None, simploid_type: Sequence[int] | None = None, seed: int | None = None) → Tensor[source]

Suggest points on the simplex or simploid where new data should be sampled.

The candidates are drawn uniformly from the simploid of type simploid_type (see torch_bsf.simploidal.sample_simploid()), or from the simplex of dimension n_params - 1 for models without a type.

Parameters:
  • models (Sequence[nn.Module]) – An ensemble of models (e.g., from k-fold cross-validation). Each model must be callable with a tensor of shape (n_candidates, n_params) and return predictions. Accepts any Sequence of Module instances, including ModuleList.

  • n_suggestions (int, default=1) – The number of points to suggest.

  • n_candidates (int, default=1000) – The number of candidate points to evaluate.

  • method (Literal["qbc", "density"], default="qbc") – The method to use: - “qbc”: Query-By-Committee. Suggests points where models disagree most. - “density”: Suggests points that are furthest from existing training points.

  • params (torch.Tensor, optional) – The existing training parameters. Required for method=”density”.

  • n_params (int, optional) – The number of simplex parameters (input dimension). When omitted, the value is inferred from models[0].n_params if that attribute exists. When provided and models[0] exposes an n_params attribute, the two values must agree; a ValueError is raised on mismatch.

  • simploid_type (Sequence[int], optional) – The type of the simploid to sample. When omitted, it is taken from models[0].simploid_type if that attribute exists (e.g., for BezierSimploid models), and the candidates are drawn from a simplex otherwise. All models with a simploid_type must agree with it.

  • seed (int, optional) – Random seed for drawing the candidates, which makes the suggestions reproducible.

Returns:

The suggested points in shape (n_suggestions, n_params).

Return type:

torch.Tensor

Examples

>>> import torch_bsf
>>> from torch_bsf.active_learning import suggest_next_points
>>> from torch_bsf.simploidal import sample_simploid
>>> t = sample_simploid((1, 1), 30, seed=0)
>>> models = torch_bsf.fit_kfold(t, t[:, :1] * t[:, 2:3], degree=2, simploid_type=(1, 1), seed=0)
>>> suggest_next_points(models, n_suggestions=3).shape
torch.Size([3, 4])

torch_bsf.plotting module

torch_bsf.plotting.plot_bezier_simplex(model: BezierSimploid, num: int = 100, ax: Any = None, show_control_points: bool = True, *, max_control_points: int = 500, max_pairwise_points: int = 2000, **kwargs: Any) → Any[source]

Plots a Bézier simplex.

For a Bézier simploid of any type, see plot_bezier_simploid().

Parameters:
  • model (BezierSimploid) – The Bézier simplex to plot: a Bézier simploid of a single factor (type (n_params - 1,)), e.g. as returned by torch_bsf.fit().

  • num (int) – The number of grid points for each edge. For model.n_params >= 4 this value is used only to decide whether to use a full meshgrid or random sampling: if the combinatorial meshgrid size comb(num + n_params - 1, n_params - 1) exceeds max_pairwise_points, exactly max_pairwise_points uniformly random simplex samples are drawn instead and num no longer controls the sample count.

  • ax (matplotlib.axes.Axes or None) – The matplotlib axes to plot on. If None, a new figure is created. Ignored when model.n_params >= 4; for pairwise plots a new figure is created only when model.n_values > 0 (when model.n_values == 0 an empty (0, 0) ndarray is returned without creating a figure).

  • show_control_points (bool) – Whether to show control points.

  • max_control_points (int) – Maximum number of control points to render in pairwise plots (model.n_params >= 4). When the model has more control points than this limit, a random subset of max_control_points is drawn instead to avoid combinatorial slowdowns. Defaults to 500. Ignored when show_control_points is False or model.n_params < 4. Must be a non-negative integer.

  • max_pairwise_points (int) – Maximum number of sample points for the pairwise scatter plot (model.n_params >= 4). When the combinatorial meshgrid size would exceed this limit, max_pairwise_points uniformly random simplex samples are drawn instead to bound memory usage and plot time. Defaults to 2000. Ignored when model.n_params < 4. Must be a non-negative integer.

  • **kwargs – Additional keyword arguments forwarded to the plot call. For model.n_params == 2, forwarded to ax.plot (curve). For model.n_params == 3 and model.n_values >= 3, forwarded to ax.plot_trisurf (3D surface). For model.n_params == 3 and model.n_values == 2, ignored. For model.n_params >= 4, forwarded to ax.scatter (pairwise).

Returns:

The axes containing the plot. For model.n_params <= 3 a single Axes (or Axes3D) is returned. For model.n_params >= 4 a 2-D numpy.ndarray of Axes with shape (n_values, n_values) is returned (pairwise scatter plot).

Return type:

matplotlib.axes.Axes or mpl_toolkits.mplot3d.axes3d.Axes3D or numpy.ndarray

Raises:
  • ImportError – If matplotlib is not installed. This dependency is required for all plotting backends used by this function.

  • ImportError – If SciPy is not installed and model.n_params == 3. SciPy is required for the triangulation-based plotting used in the Bézier triangle case.

  • ValueError – If model is not a Bézier simplex, i.e. its type does not have exactly one factor.

Examples

The plotting lines are skipped by doctest because matplotlib is optional.

>>> import torch_bsf
>>> from torch_bsf.plotting import plot_bezier_simplex
>>> from torch_bsf.sampling import simplex_grid
>>> ts = simplex_grid(n_params=3, degree=4)
>>> bs = torch_bsf.fit(ts, ts ** 2, degree=2)
>>> ax = plot_bezier_simplex(bs, num=20)
torch_bsf.plotting.plot_bezier_simploid(model: BezierSimploid, num: int = 20, ax: Any = None, show_control_points: bool = True, **kwargs: Any) → Any[source]

Plots a Bézier simploid whose values are 2- or 3-dimensional.

The image of a grid on the parameter simploid is drawn: a curve for a simploid of dimension 1 (type (1,)) and a triangulated surface, with its boundary curves, for a simploid of dimension 2 (a triangle (2,) or a square (1, 1)). A simploid of dimension 3 or more (e.g. a prism (2, 1) or a cube (1, 1, 1)) is drawn through all its 2-dimensional faces, i.e. as the boundary surface of the solid. A vertex (type ()) is drawn as a point.

Parameters:
  • model (torch_bsf.simploidal.BezierSimploid) – The Bézier simploid to plot. model.n_values must be 2 or 3.

  • num (int) – The number of grid intervals along each edge of the parameter simploid.

  • ax (matplotlib.axes.Axes or None) – The axes to plot on: a 2D axes when model.n_values == 2 and a 3D axes (projection="3d") when model.n_values == 3. If None, a new figure is created.

  • show_control_points (bool) – Whether to show the control net: the control points, and dashed segments joining the control points whose indices differ by \(e_k - e_l\) in a single factor.

  • **kwargs – Additional keyword arguments forwarded to the drawing call of the curves (ax.plot) and surfaces (ax.plot_trisurf in 3D, matplotlib.collections.PolyCollection in 2D). color sets the color of the model (default "C0").

Returns:

The axes containing the plot.

Return type:

matplotlib.axes.Axes or mpl_toolkits.mplot3d.axes3d.Axes3D

Raises:
  • ValueError – If model.n_values is neither 2 nor 3, num is not a positive integer, or ax is a 2D axes for 3 values or a 3D axes for 2 values.

  • ImportError – If matplotlib is not installed.

Examples

The plotting lines are skipped by doctest because matplotlib is optional.

>>> from torch_bsf.simploidal import BezierSimploid
>>> from torch_bsf.plotting import plot_bezier_simploid
>>> patch = BezierSimploid.randn((1, 1), degree=2, n_values=3)
>>> ax = plot_bezier_simploid(patch, num=10)
>>> ax.name
'3d'
torch_bsf.plotting.plot_simploidal_bezier_space(space: SimploidalBezierSpace, num: int = 20, ax: Any = None, show_control_points: bool = True, **kwargs: Any) → Any[source]

Plots a simploidal Bézier space whose values are 2- or 3-dimensional.

Every main simploid of the simploidal set (see main_simploids()) is drawn on one axes as plot_bezier_simploid() draws a Bézier simploid, each in its own color from the Matplotlib color cycle. The boundary curves of the pieces are drawn as solid lines, so that pieces sharing a face can be seen to meet along it.

Parameters:
  • space (torch_bsf.simploidal.SimploidalBezierSpace) – The simploidal Bézier space to plot. space.n_values must be 2 or 3.

  • num (int) – The number of grid intervals along each edge of every piece.

  • ax (matplotlib.axes.Axes or None) – The axes to plot on: a 2D axes when space.n_values == 2 and a 3D axes (projection="3d") when space.n_values == 3. If None, a new figure is created.

  • show_control_points (bool) – Whether to show the control net of every piece. The control points of a shared face are drawn once per piece containing it.

  • **kwargs – Additional keyword arguments forwarded to the drawing call of every piece, as in plot_bezier_simploid(). color sets one color for all pieces instead of the color cycle.

Returns:

The axes containing the plot.

Return type:

matplotlib.axes.Axes or mpl_toolkits.mplot3d.axes3d.Axes3D

Raises:
  • ValueError – If space.n_values is neither 2 nor 3, num is not a positive integer, or ax is a 2D axes for 3 values or a 3D axes for 2 values.

  • ImportError – If matplotlib is not installed.

Examples

The plotting lines are skipped by doctest because matplotlib is optional.

>>> import torch
>>> from torch_bsf.simploidal import SimploidalSet, SimploidalBezierSpace
>>> from torch_bsf.plotting import plot_simploidal_bezier_space
>>> S = SimploidalSet()
>>> tri, sq = S.add_simplex(2), S.add_simploid((1, 1))
>>> _ = S.identify(tri.face(0, 2), sq.face(1, 1))
>>> space = SimploidalBezierSpace(S, degree=2, n_values=2)
>>> _ = torch.nn.init.normal_(space.proper_control_points)
>>> ax = plot_simploidal_bezier_space(space, num=10)
>>> ax.name
'rectilinear'

torch_bsf.preprocessing module

class torch_bsf.preprocessing.MinMaxScaler[source]

Bases: object

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

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).

Returns:

Tensor rescaled back to the original value range.

Return type:

torch.Tensor

mins: Tensor[source]
scales: Tensor[source]
transform(values: Tensor) → Tensor[source]

Normalize values with the fitted minimum and scale.

Parameters:

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

Returns:

Transformed tensor of the same shape as values.

Return type:

torch.Tensor

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.

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).

Returns:

Tensor rescaled back to the original value range.

Return type:

torch.Tensor

mins: Tensor[source]
q: float = 0.05[source]
scales: Tensor[source]
transform(values: Tensor) → Tensor[source]

Scale values with the fitted quantile bounds.

Parameters:

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

Returns:

Transformed tensor of the same shape as values.

Return type:

torch.Tensor

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.

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).

Returns:

Tensor rescaled back to the original value range.

Return type:

torch.Tensor

means: Tensor[source]
stds: Tensor[source]
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).

Returns:

Transformed tensor of the same shape as values.

Return type:

torch.Tensor

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]]

torch_bsf.sampling module

torch_bsf.sampling.simplex_grid(n_params: int, degree: int) → Tensor[source]

Generates a uniform grid on a simplex.

Parameters:
  • n_params (int) – The number of parameters (vertices of the simplex).

  • degree (int) – The degree of the grid.

Returns:

Array of grid points in shape (N, n_params), where N = binom(degree + n_params - 1, n_params - 1).

Return type:

torch.Tensor

torch_bsf.sampling.simplex_random(n_params: int, n_samples: int, seed: int | None = None) → Tensor[source]

Generates random points on a simplex using Dirichlet distribution.

Parameters:
  • n_params (int) – The number of parameters (vertices of the simplex).

  • n_samples (int) – The number of samples.

  • seed (int or None, optional) – Random seed for reproducibility. When provided, a new numpy.random.Generator is created with this seed so the global NumPy random state is not affected. When None (default), the global numpy.random state is used.

Returns:

Array of sample points in shape (n_samples, n_params).

Return type:

torch.Tensor

Raises:

ValueError – If n_params is not positive or n_samples is negative.

torch_bsf.sampling.simplex_sobol(n_params: int, n_samples: int, seed: int | None = None) → Tensor[source]

Generates quasi-random points on a simplex using Sobol sequence.

Uses a scrambled Sobol sequence projected onto the simplex via the sorted-differences mapping. Sobol sequences are low-discrepancy: they fill space more uniformly than pseudo-random draws, giving a convergence rate of roughly O((log N)^(d-1) / N) instead of the O(1/sqrt(N)) rate of Monte Carlo sampling (where d = n_params - 1).

Note

Power-of-two sample sizes are strongly recommended. Sobol sequences are constructed in base 2 and achieve their best uniformity guarantees when n_samples is an exact power of 2 (e.g. 64, 128, 256, …). When n_samples is not a power of 2, the strongest low-discrepancy guarantees no longer apply and the coverage of the simplex can be somewhat less uniform. A UserWarning is emitted automatically when a non-power-of-two value is requested.

Note

scipy is required. This function relies on scipy.stats.qmc.Sobol. Install it with pip install scipy or pip install pytorch-bsf[sampling].

Parameters:
  • n_params (int) – The number of parameters (vertices of the simplex). Must be at least 2. The Sobol sequence is drawn in n_params - 1 dimensions and then mapped to the simplex.

  • n_samples (int) – The number of samples. For best coverage, use a power of 2 (e.g. 64, 128, 256).

  • seed (int or None, optional) – Random seed for the scrambled Sobol sequence, passed directly to scipy.stats.qmc.Sobol as its seed argument. When None (default), a random scramble is used on each call. Pass an integer for reproducible sequences.

Returns:

Array of sample points in shape (n_samples, n_params). Each row is non-negative and sums to 1.

Return type:

torch.Tensor

Raises:
  • ImportError – If SciPy is not installed.

  • ValueError – If n_params is less than 2 or n_samples is negative.

Warns:

UserWarning – If n_samples is not a power of 2. The samples are still returned, but the low-discrepancy coverage guarantee is weakened.

Examples

>>> import torch
>>> from torch_bsf.sampling import simplex_sobol
>>> pts = simplex_sobol(n_params=3, n_samples=128)
>>> pts.shape
torch.Size([128, 3])
>>> pts.sum(dim=1).allclose(torch.ones(128))
True

torch_bsf.simploidal module

Simploidal sets and simploidal Bézier spaces.

This module implements the simploidal sets of Peltier and Lienhardt [PL18] and the fitting of the Bézier spaces they carry.

A simploid of type \((a_1, \ldots, a_n)\) is a cartesian product \(\Delta^{a_1} \times \cdots \times \Delta^{a_n}\) of simplices; simplices (type \((a)\)) and cubes (type \((1, \ldots, 1)\)) are special cases. A Bézier simploid of type \((a_1, \ldots, a_n)\) and degree \((d_1, \ldots, d_n)\) is the tensor product of Bézier simplices

\[B(\mathbf u^1, \ldots, \mathbf u^n) = \sum_{\boldsymbol\alpha^1 \in \Gamma^{a_1}_{d_1}} \cdots \sum_{\boldsymbol\alpha^n \in \Gamma^{a_n}_{d_n}} P_{(\boldsymbol\alpha^1, \ldots, \boldsymbol\alpha^n)} \prod_{i=1}^n B^{d_i}_{\boldsymbol\alpha^i}(\mathbf u^i),\]

where each \(\mathbf u^i\) is a point of the standard \(a_i\)-simplex given in barycentric coordinates. Throughout this module the parameter of a simploid is the concatenation \((\mathbf u^1, \ldots, \mathbf u^n)\), a vector of length \(\sum_i (a_i + 1)\).

A simploidal set (SimploidalSet) is an assembly of simploids glued along common faces, described by face operators \(d^i_j\) and degeneracy operators \(s^i_j\). A simploidal Bézier space (SimploidalBezierSpace) associates a Bézier simploid with every simploid of a simploidal set. As in Section 4.3 of the paper, only the proper control points of each non-degenerate simploid are stored, so control points on shared faces are stored once and the space is continuous across them by construction.

Package layout

The public names below are re-exported from the submodules:

Both models are plain torch.nn.Module objects that are linear in their only parameter; the solvers of torch_bsf.simploidal.fitting rely on this alone.

Indexing convention

Factor indices are 0-based, whereas the paper uses 1-based ones:

  • face(i, j) is the paper’s \(d^{i+1}_j\) (remove vertex j of factor i);

  • degeneracy(i, j) with j >= 0 is \(s^{i+1}_j\) (repeat vertex j of factor i);

  • degeneracy(i, -1) is \(s^i_{-1}\) (insert a degenerate edge as the new factor i).

Examples

Glue a triangle and a square along an edge and fit the resulting space:

>>> import torch
>>> from torch_bsf.simploidal import SimploidalSet, fit_simploidal_set, sample_simploid
>>> S = SimploidalSet()
>>> triangle = S.add_simplex(2)
>>> square = S.add_simploid((1, 1))
>>> edge = S.identify(triangle.face(0, 0), square.face(0, 0))
>>> S.check()
>>> t1, t2 = sample_simploid((2,), 50, seed=0), sample_simploid((1, 1), 50, seed=1)
>>> space = fit_simploidal_set(S, [(triangle, t1, t1[:, :1]), (square, t2, t2[:, 1:2])], degree=2)
>>> space.n_values
1
class torch_bsf.simploidal.BezierSimploid(simploid_type: Sequence[int], degree: int | Sequence[int], control_points: Tensor | Mapping[Any, Sequence[float] | Tensor] | None = None, *, n_values: int | None = None)[source]

Bases: Module

A Bézier simploid.

This is a plain torch.nn.Module holding the control points; fit one to data with torch_bsf.fit().

Parameters:
  • simploid_type – The type \((a_1, \ldots, a_n)\). () gives a constant (a vertex).

  • degree – The degree of every factor, or one degree per factor.

  • control_points – The control points: a tensor of shape (n_indices, n_values) whose rows follow simploid_indices(), or a mapping from every index to its value. A Bézier simplex (a single factor) also accepts its multi-indices alone as keys, e.g. (2, 1, 0). Zeros if None, in which case n_values is required.

  • n_values – The number of values (target dimension); used only when control_points is None.

Examples

A bilinear patch (a simploid of type (1, 1)):

>>> import torch
>>> from torch_bsf.simploidal import BezierSimploid
>>> patch = BezierSimploid((1, 1), 1, {
...     ((1, 0), (1, 0)): [0.0, 0.0, 0.0],
...     ((1, 0), (0, 1)): [0.0, 1.0, 0.0],
...     ((0, 1), (1, 0)): [1.0, 0.0, 0.0],
...     ((0, 1), (0, 1)): [1.0, 1.0, 1.0],
... })
>>> patch(torch.tensor([[0.5, 0.5, 0.5, 0.5]]))
tensor([[0.5000, 0.5000, 0.2500]], grad_fn=<MmBackward0>)
adjacency() → Tensor[source]

The pairs of adjacent control points, as rows of control_points.

Two control points are adjacent if their multi-indices differ in a single factor, by moving one unit from one component to another. For a Bézier simplex, these are the edges of its control net.

Return type:

A tensor of shape (n_pairs, 2) whose rows (p, q) satisfy p < q, in lexicographic order.

Examples

>>> from torch_bsf.simploidal import BezierSimploid
>>> BezierSimploid.zeros((1,), 2, n_values=1).adjacency()
tensor([[0, 1],
        [1, 2]])
>>> BezierSimploid.zeros((1, 1), 1, n_values=1).adjacency()
tensor([[0, 1],
        [0, 2],
        [1, 3],
        [2, 3]])
degenerate(i: int, j: int, degree: int | None = None) → BezierSimploid[source]

The Bézier simploid of the degenerate simploid s^{i+1}_j or s^i_{-1} (Property 2-2).

The result has the same shape as this Bézier simploid.

Parameters:
  • i – The degeneracy operator, as in Simploid.degeneracy().

  • j – The degeneracy operator, as in Simploid.degeneracy().

  • degree – The degree of the inserted edge factor when j == -1. Defaults to the common degree of the factors.

Examples

>>> import torch
>>> from torch_bsf.simploidal import BezierSimploid
>>> _ = torch.manual_seed(0)
>>> curve = BezierSimploid.randn((1,), 3, n_values=2)
>>> triangle = curve.degenerate(0, 1)  # repeat vertex 1: (v0, v1, v1)
>>> torch.allclose(triangle(torch.tensor([[0.2, 0.3, 0.5]])), curve(torch.tensor([[0.2, 0.8]])), atol=1e-6)
True
property degree: tuple[int, ...][source]

The degree of each factor.

design_matrix(t: Tensor) → Tensor[source]

The Bernstein basis at t, so that forward(t) == design_matrix(t) @ control_points.

Parameters:

t – Parameters of shape (batch, n_params).

Returns:

  • The basis of shape (batch, n_indices) on the device of control_points, in the dtype

  • of t (the default dtype if t is not floating).

Examples

>>> import torch
>>> from torch_bsf.simploidal import BezierSimploid
>>> bs = BezierSimploid.randn((2, 1), 2, n_values=3)
>>> t = torch.tensor([[0.2, 0.3, 0.5, 0.4, 0.6]])
>>> torch.allclose(bs.design_matrix(t) @ bs.control_points, bs(t))
True
extra_repr() → str[source]
face(i: int, j: int) → BezierSimploid[source]

The Bézier simploid of the face d^{i+1}_j (Property 2-1 of Peltier and Lienhardt [PL18]).

Its control points are those whose multi-index of factor i has a zero at position j.

Examples

>>> import torch
>>> from torch_bsf.simploidal import BezierSimploid
>>> _ = torch.manual_seed(0)
>>> prism = BezierSimploid.randn((2, 1), 3, n_values=2)
>>> bottom = prism.face(1, 1)  # the triangle at u^2 = (1, 0)
>>> u = torch.tensor([[0.2, 0.3, 0.5]])
>>> torch.allclose(prism(torch.cat([u, torch.tensor([[1.0, 0.0]])], dim=1)), bottom(u), atol=1e-6)
True
forward(t: Tensor) → Tensor[source]

Evaluates the Bézier simploid.

Parameters:

t – Parameters of shape (batch, n_params).

Return type:

Values of shape (batch, n_values).

indices() → list[tuple[tuple[int, ...], ...]][source]

The control point indices in the order of the rows of control_points.

property n_params: int[source]

The length \(\sum_i (a_i + 1)\) of the parameter vector.

property n_values: int[source]

The number of values (target dimension).

product(other: BezierSimploid) → BezierSimploid[source]

The cartesian product with another Bézier simploid (Minkowski sum of the control points).

The product of Bézier simploids \(B_1\) and \(B_2\) is \(B(\mathbf u, \mathbf v) = B_1(\mathbf u) + B_2(\mathbf v)\).

Examples

>>> import torch
>>> from torch_bsf.simploidal import BezierSimploid
>>> _ = torch.manual_seed(0)
>>> b1, b2 = BezierSimploid.randn((2,), 2, n_values=3), BezierSimploid.randn((1,), 3, n_values=3)
>>> u, v = torch.tensor([[0.2, 0.3, 0.5]]), torch.tensor([[0.4, 0.6]])
>>> torch.allclose(b1.product(b2)(torch.cat([u, v], dim=1)), b1(u) + b2(v), atol=1e-6)
True
classmethod rand(simploid_type: Sequence[int], degree: int | Sequence[int], n_values: int) → BezierSimploid[source]

A Bézier simploid whose control points are uniformly distributed on \([0, 1)\).

classmethod randn(simploid_type: Sequence[int], degree: int | Sequence[int], n_values: int) → BezierSimploid[source]

A Bézier simploid whose control points follow the standard normal distribution.

row(index: tuple[tuple[int, ...], ...] | Sequence[int]) → int[source]

The row of control_points holding the control point of an index.

A Bézier simplex (a single factor) also accepts its multi-index alone, e.g. (2, 1, 0) for ((2, 1, 0),).

Raises:

ValueError – If index is not a control point index of this Bézier simploid.

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

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

classmethod zeros(simploid_type: Sequence[int], degree: int | Sequence[int], n_values: int) → BezierSimploid[source]

A Bézier simploid whose control points are all zero.

class torch_bsf.simploidal.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.simploidal 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.simploidal.SimploidalBezierSpace(simploidal_set: SimploidalSet, degree: int | Mapping[Simploid, int | Sequence[int]], n_values: int, proper_control_points: Tensor | None = None, *, default_degree: int | None = None)[source]

Bases: Module

A simploidal Bézier space [PL18].

A Bézier simploid is associated with every simploid of a SimploidalSet. Only the proper control points of the non-degenerate simploids (those whose multi-indices have no zero component, and the single point of each vertex) are stored, in proper_control_points. The full control net of any simploid is rebuilt from its proper points and those of its boundary (Property 2-1), and the control net of a degenerate simploid from that of its base (Property 2-2). Hence every control point is stored exactly once and simploids sharing a face agree on it.

The structure of the simploidal set is captured when the space is created; modifying the set afterwards is not reflected in the space.

Every factor of every non-degenerate simploid has its own degree. The degrees must be consistent: a factor of a simploid and the corresponding factor of each of its faces have the same degree, so that a shared face has a single control net. In particular, the degree of a factor of a simploid is the degree of its edges in the direction of that factor. An edge factor inserted by a degeneracy (degeneracy(i, -1)), along which the Bézier simploid is constant, imposes no constraint; it gets degree degree in simploid_degree().

Parameters:
  • simploidal_set – The simploidal set.

  • degree – Either the degree of every factor of every simploid (at least 1), or a mapping from non-degenerate simploids to their degrees (an integer for all their factors, or one degree per factor). With a mapping, the degrees propagate through shared faces to the simploids it does not mention; a factor whose degree is still undetermined gets default_degree.

  • n_values – The number of values (target dimension).

  • proper_control_points – Initial proper control points of shape (n_proper_control_points, n_values). Zeros if None.

  • default_degree – The degree of the factors left undetermined by a degree mapping (at least 1). Ignored when degree is an integer.

Raises:

ValueError – If a degree is less than 1, if the degree mapping is inconsistent across shared faces or mentions a simploid that is not a non-degenerate simploid of the set, or if a degree is left undetermined without default_degree.

Examples

Two triangles sharing an edge share the control points of that edge:

>>> import torch
>>> from torch_bsf.simploidal import SimploidalSet, SimploidalBezierSpace
>>> S = SimploidalSet()
>>> t1, t2 = S.add_simplex(2), S.add_simplex(2)
>>> e = S.identify(t1.face(0, 0), t2.face(0, 0))
>>> space = SimploidalBezierSpace(S, degree=3, n_values=2)
>>> space.n_proper_control_points  # 4 vertices, 5 edges x 2, 2 triangles x 1
16
>>> _ = torch.manual_seed(0)
>>> _ = torch.nn.init.normal_(space.proper_control_points)
>>> u = torch.tensor([[0.0, 0.3, 0.7]])  # a point on the shared edge
>>> torch.allclose(space.evaluate(t1, u), space.evaluate(t2, u), atol=1e-6)
True

A prism that is linear across its triangles and cubic along its height, glued to a cube along a square face. The degrees of the cube’s factors along that face follow from the prism; the remaining one is the default:

>>> S = SimploidalSet()
>>> prism, cube = S.add_simploid((2, 1)), S.add_simploid((1, 1, 1))
>>> _ = S.identify(prism.face(0, 0), cube.face(0, 0))
>>> space = SimploidalBezierSpace(S, {prism: (1, 3)}, n_values=1, default_degree=2)
>>> space.simploid_degree(cube)
(2, 1, 3)
adjacency() → Tensor[source]

The pairs of adjacent stored control points, as rows of proper_control_points.

Two stored control points are adjacent if they are adjacent in the control net of some non-degenerate simploid (see BezierSimploid.adjacency()). A pair on a face shared by several simploids is listed once.

Return type:

A tensor of shape (n_pairs, 2) whose rows (p, q) satisfy p < q, in lexicographic order.

Examples

Two segments glued at a vertex form a path of 5 control points with 4 adjacent pairs:

>>> from torch_bsf.simploidal import SimploidalSet, SimploidalBezierSpace
>>> S = SimploidalSet()
>>> e1, e2 = S.add_simplex(1), S.add_simplex(1)
>>> _ = S.identify(e1.face(0, 0), e2.face(0, 1))
>>> SimploidalBezierSpace(S, degree=2, n_values=1).adjacency().shape
torch.Size([4, 2])
bezier_simploid(simploid: Simploid) → BezierSimploid[source]

A standalone copy of the Bézier simploid associated with a simploid.

control_point_rows(simploid: Simploid) → list[int][source]

The rows of proper_control_points holding the full control net of a simploid.

Parameters:

simploid – Any simploid of the set, degenerate or not.

Return type:

One row per control point, in the order of simploid_indices().

control_points(simploid: Simploid) → Tensor[source]

The full control net of a simploid, of shape (n_indices, n_values) (differentiable).

design_matrix(t: Tensor, simploid_ids: Tensor) → Tensor[source]

The linear map from the proper control points to the values of a batch mixing several simploids.

forward(t, simploid_ids) == design_matrix(t, simploid_ids) @ proper_control_points.

Parameters:
Returns:

  • A matrix of shape (batch, n_proper_control_points) on the device of

  • proper_control_points, in the dtype of t (the default dtype if t is not floating).

Examples

>>> import torch
>>> from torch_bsf.simploidal import SimploidalSet, SimploidalBezierSpace
>>> S = SimploidalSet()
>>> tri = S.add_simplex(2)
>>> space = SimploidalBezierSpace(S, degree=2, n_values=1)
>>> _ = torch.nn.init.normal_(space.proper_control_points)
>>> t, ids = torch.tensor([[0.2, 0.3, 0.5]]), torch.tensor([space.simploid_id(tri)])
>>> torch.allclose(space.design_matrix(t, ids) @ space.proper_control_points, space(t, ids))
True
evaluate(simploid: Simploid, t: Tensor) → Tensor[source]

Evaluates the Bézier simploid of a simploid.

Parameters:
  • simploid – Any simploid of the set, degenerate or not.

  • t – Parameters of shape (batch, sum(a_i + 1)).

Return type:

Values of shape (batch, n_values).

extra_repr() → str[source]
forward(t: Tensor, simploid_ids: Tensor) → Tensor[source]

Evaluates a batch mixing several simploids.

Parameters:
  • t – Parameters of shape (batch, width): row k holds the parameters of simploid simploids[simploid_ids[k]] in its first sum(a_i + 1) columns (the rest is ignored).

  • simploid_ids – Indices into simploids (see simploid_id()) of shape (batch,).

Return type:

Values of shape (batch, n_values).

property n_proper_control_points: int[source]

The number of stored (proper) control points.

property n_values: int[source]

The number of values (target dimension).

row(simploid: Simploid, index: tuple[tuple[int, ...], ...]) → int[source]

The row of proper_control_points holding a control point of a simploid.

Parameters:
  • simploid – Any simploid of the set, degenerate or not.

  • index – A control point index of the Bézier simploid of simploid.

Raises:

ValueError – If index is not a control point index of that Bézier simploid.

simploid_degree(simploid: Simploid) → tuple[int, ...][source]

The degree of each factor of the Bézier simploid of a simploid.

A degenerate simploid has the degrees of the factors of its base it comes from, and degree degree on the edge factors inserted by degeneracies.

Parameters:

simploid – Any simploid of the set, degenerate or not.

Return type:

One degree per factor.

Examples

>>> from torch_bsf.simploidal import SimploidalSet, SimploidalBezierSpace
>>> S = SimploidalSet()
>>> square = S.add_simploid((1, 1))
>>> space = SimploidalBezierSpace(S, {square: (2, 3)}, n_values=1)
>>> space.simploid_degree(square), space.simploid_degree(square.face(1, 0))
((2, 3), (2,))
>>> space.simploid_degree(square.degeneracy(0, -1))
(3, 2, 3)
simploid_id(simploid: Simploid) → int[source]

The position of a non-degenerate simploid in simploids, used by forward().

class torch_bsf.simploidal.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.simploidal 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.simploidal 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.simploidal 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.simploidal 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.simploidal 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.simploidal 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.simploidal 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.simploidal 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.simploidal 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.

torch_bsf.simploidal.bernstein_basis(simploid_type: Sequence[int], degree: int | Sequence[int], t: Tensor) → Tensor[source]

Evaluates the tensor-product Bernstein basis of a Bézier simploid.

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

  • degree – The degree of every factor, or one degree per factor.

  • t – Parameters of shape (batch, sum(a_i + 1)): the concatenated barycentric coordinates of every factor.

Return type:

The basis of shape (batch, n_indices), columns in the order of simploid_indices().

Examples

>>> import torch
>>> from torch_bsf.simploidal import bernstein_basis
>>> bernstein_basis((1, 1), 1, torch.tensor([[0.5, 0.5, 1.0, 0.0]]))
tensor([[0.5000, 0.0000, 0.5000, 0.0000]])
torch_bsf.simploidal.fit_sgd(model: _Model, loss: Callable[[...], Tensor], *, inputs: Sequence[Tensor] | None = None, n_samples: int = 100, batch_size: int | None = None, max_iter: int = 100, step_size: float = 0.05, step_decay: Literal['constant', 'sqrt'] = 'constant', preconditioner: Literal['gram', 'none'] = 'gram', weights: Mapping[Simploid, float] | None = None, freeze: Iterable[tuple[tuple[int, ...], ...] | Sequence[int] | tuple[Simploid, tuple[tuple[int, ...], ...]]] | None = None, smoothness_weight: float = 0.0, seed: int | None = None, callback: Callable[[int, _Model, float], bool | None] | None = None) → _Model[source]

Fits a model to the minimizers of a loss by (preconditioned) stochastic gradient descent.

This is Algorithm 1 of Hikima et al. [HKT+25], generalized to Bézier simploids and simploidal Bézier spaces (see torch_bsf.simploidal.sgd). It minimizes the mean of loss over fixed samples inputs of the domain: at every iteration k, it draws a minibatch of batch_size samples uniformly without replacement and updates the parameter \(P\) of the model as \(P \leftarrow P - \alpha_k \Sigma^{-1} \nabla \mathcal L_B(P)\), where \(\Sigma\) is gram_matrix() (or the identity for plain SGD).

The control points in freeze keep their values in model, and only the free rows \(F\) of \(P\) move. With smoothness_weight \(w > 0\), the loss also has the smoothness penalty \(w \sum_{(p, q)} \|P_p - P_q\|^2 = w \operatorname{tr}(P^\top L P)\) over the adjacent control points (see model.adjacency()), as in fit(), where \(L\) is the Laplacian of the adjacency graph. The step then minimizes the linearized loss plus the exact penalty plus the move of the model over the free rows, which gives

\[P_F \leftarrow P_F - \alpha_k (\Sigma_{FF} + 2 \alpha_k w L_{FF})^{-1} \nabla_F \bigl(\mathcal L_B(P) + w \operatorname{tr}(P^\top L P)\bigr).\]

Treating the penalty exactly rather than by its gradient keeps the step stable for any \(w\), while \(\Sigma^{-1} L\) has eigenvalues in the thousands already for a cubic Bézier triangle. Without frozen rows and with \(w = 0\), this is the step above.

Parameters:
  • model – The model to fit, which also gives the initial control points (zeros in the paper). It is not modified.

  • loss – The loss loss(values, *inputs) of every sample (see SGDLoss), differentiable with respect to values; e.g., linear_scalarization() to approximate a Pareto set.

  • inputs – The samples, as the arguments of model.forward. Drawn with sample_domain() if None.

  • n_samples – The number of samples drawn when inputs is None.

  • batch_size – The minibatch size (full batch if None or larger than the number of samples).

  • max_iter – The number of iterations.

  • step_size – The step size \(\alpha > 0\).

  • step_decay – "constant" uses \(\alpha_k = \alpha\); "sqrt" uses \(\alpha_k = \alpha / \sqrt{k + 1}\).

  • preconditioner – "gram" preconditions the gradient by \(\Sigma^{-1}\) (the proposed method); "none" is plain stochastic gradient descent.

  • weights – For a space, the probability of every non-degenerate simploid, used both to sample inputs and in \(\Sigma\). Uniform over the main simploids of the space (those that are not a face of another) if None.

  • freeze – Control points that keep their values in model. For a Bézier simploid, their indices as in fit() (a Bézier simplex also accepts its multi-indices alone, e.g. (2, 0, 0)); for a space, pairs (simploid, index) as in fit_simploidal_set().

  • smoothness_weight – The weight \(w \ge 0\) of the smoothness penalty added to the loss.

  • seed – Random seed for the samples and the minibatches.

  • callback – Called as callback(k, model, batch_loss) after the update of every iteration k = 0, 1, ... (see SGDCallback), where model is the model being fitted and batch_loss is the mean loss of the minibatch at the parameters before the update, without the smoothness penalty of smoothness_weight. If it returns True, the fitting stops after this iteration. It may evaluate model, preferably under torch.no_grad(), but must not modify it: copy the parameters to keep a snapshot.

Return type:

A fitted copy of model.

Raises:

ValueError – If an option is out of range, freeze has an invalid index, the inputs are empty or have different lengths, or loss does not return one value per sample.

Examples

The Pareto set of \(f_m(\mathbf x) = \|\mathbf x - \mathbf e_m\|^2\) (\(m = 1, 2, 3\)) is the triangle spanned by \(\mathbf e_1, \mathbf e_2, \mathbf e_3\), reached at \(\mathbf x = \mathbf t\):

>>> import torch
>>> import torch_bsf
>>> from torch_bsf.simploidal import BezierSimploid, linear_scalarization
>>> def objective(x):
...     return ((x.unsqueeze(1) - torch.eye(3)) ** 2).sum(dim=-1)
>>> init = BezierSimploid.zeros((2,), 1, n_values=3)
>>> bs = torch_bsf.fit_sgd(init, linear_scalarization(objective), batch_size=20, step_size=0.2, seed=0)
>>> torch.allclose(bs.control_points, torch.eye(3), atol=1e-3)
True

A callback records the loss of every iteration and stops the fitting once the loss stops decreasing:

>>> history = []
>>> def callback(k, model, batch_loss):
...     history.append(batch_loss)
...     return k > 0 and history[-2] - batch_loss < 1e-4
>>> bs = torch_bsf.fit_sgd(init, linear_scalarization(objective), step_size=0.2, seed=0, callback=callback)
>>> len(history) < 100  # stopped before max_iter
True

Keep the vertices of a quadratic Bézier triangle at the known minimizers of the single objectives, and smooth its control net:

>>> init = BezierSimploid.zeros((2,), 2, n_values=3)
>>> vertices = [(2, 0, 0), (0, 2, 0), (0, 0, 2)]
>>> with torch.no_grad():
...     for m, vertex in enumerate(vertices):
...         _ = init[vertex].copy_(torch.eye(3)[m])
>>> bs = torch_bsf.fit_sgd(
...     init, linear_scalarization(objective), freeze=vertices, smoothness_weight=1e-3, step_size=0.2, seed=0
... )
>>> bs[(2, 0, 0)].tolist()
[1.0, 0.0, 0.0]
torch_bsf.simploidal.fit_simploidal_set(simploidal_set: SimploidalSet, data: Sequence[tuple[Simploid, Tensor, Tensor]], degree: int | Mapping[Simploid, int | Sequence[int]], *, default_degree: int | None = None, init: SimploidalBezierSpace | Tensor | None = None, freeze: Iterable[tuple[Simploid, tuple[tuple[int, ...], ...]]] | None = None, smoothness_weight: float = 0.0, solver: Literal['lstsq', 'trainer'] = 'lstsq', ridge: float = 0.0, batch_size: int | None = None, seed: int | None = None, **kwargs: Any) → SimploidalBezierSpace[source]

Fits a simploidal Bézier space to data on its simploids.

Each sample is attached to a non-degenerate simploid (of any dimension) and given in that simploid’s parameters. Since simploids sharing a face share its control points, all pieces are fitted jointly and the result is continuous across shared faces. The proper control points minimize the mean squared error over all samples plus smoothness_weight times the sum of \(\|P_p - P_q\|^2\) over the adjacent stored control points (see SimploidalBezierSpace.adjacency()), as in fit().

Parameters:
  • simploidal_set – The simploidal set.

  • data – A sequence of (simploid, params, values), where params has shape (n, sum(a_i + 1)) for the type of simploid and values has shape (n, n_values). A simploid may appear several times.

  • degree – The degree of every factor of every simploid (at least 1), or a mapping from non-degenerate simploids to their degrees; see SimploidalBezierSpace.

  • default_degree – The degree of the factors left undetermined by a degree mapping.

  • init – Initial proper control points: a space with the same number of them, or a tensor of shape (n_proper_control_points, n_values). "trainer" starts from them (from zero control points if None); "lstsq" uses them only for the frozen rows.

  • freeze – Control points that keep their values in init, which is then required, given as pairs (simploid, index) of any simploid of the set and a control point index of its Bézier simploid.

  • smoothness_weight – The weight \(w \ge 0\) of the smoothness penalty.

  • solver – "lstsq" solves the (linear) least-squares problem exactly; "trainer" trains with lightning.pytorch.Trainer.

  • ridge – Weight of the Tikhonov penalty \(\mathrm{ridge}\,\|P\|^2\) on the free proper control points, added to \(nm\) times the objective ("lstsq" only). Without it, under-determined problems get the minimum-norm solution.

  • batch_size – Minibatch size for "trainer" (full batch by default).

  • seed – Random seed passed to lightning.pytorch.seed_everything() ("trainer" only).

  • kwargs – Arguments for lightning.pytorch.Trainer ("trainer" only).

Return type:

The fitted simploidal Bézier space.

Examples

Fit a piecewise model on a triangle glued to a square, with data that is continuous across the shared edge:

>>> import torch
>>> from torch_bsf.simploidal import SimploidalSet, fit_simploidal_set, sample_simploid
>>> S = SimploidalSet()
>>> tri, sq = S.add_simplex(2), S.add_simploid((1, 1))
>>> _ = S.identify(tri.face(0, 2), sq.face(1, 1))
>>> t1, t2 = sample_simploid((2,), 30, seed=0), sample_simploid((1, 1), 30, seed=1)
>>> x1 = t1[:, 0:1] - t1[:, 1:2]  # the shared edge is (w0, w1, 0) on the triangle
>>> x2 = (t2[:, 0:1] - t2[:, 1:2]) * t2[:, 2:3]  # and (w0, w1, 1, 0) on the square
>>> space = fit_simploidal_set(S, [(tri, t1, x1), (sq, t2, x2)], degree=2)
>>> torch.allclose(space.evaluate(tri, t1), x1, atol=1e-5)
True

The same with a quadratic triangle and a square that is cubic across the shared edge (the degree along the shared edge is common to both pieces):

>>> space = fit_simploidal_set(S, [(tri, t1, x1), (sq, t2, x2)], degree={tri: 2, sq: (2, 3)})
>>> space.simploid_degree(sq)
(2, 3)
torch_bsf.simploidal.gram_matrix(model: BezierSimploid | SimploidalBezierSpace, weights: Mapping[Simploid, float] | None = None) → Tensor[source]

The Gram matrix \(\Sigma = \mathbb E[\mathbf z(\mathbf u) \mathbf z(\mathbf u)^\top]\) of a model.

\(\mathbf z(\mathbf u)\) is the row of model.design_matrix at \(\mathbf u\), which is uniform on the domain of the model (see sample_domain()). The matrix is computed exactly (see torch_bsf.simploidal.sgd).

Parameters:
  • model – A Bézier simploid or a simploidal Bézier space.

  • weights – For a space, the probability of every non-degenerate simploid (normalized to sum to one). Uniform over the main simploids of the space (those that are not a face of another) if None. Ignored for a Bézier simploid.

Returns:

  • A positive definite matrix of shape (n, n) in double precision on the CPU, where n is the number

  • of rows of the parameter of model.

Raises:

ValueError – If weights has a negative entry, sums to zero, mentions a simploid that is not a non-degenerate simploid of the space, or leaves a proper control point outside every simploid it weights.

Examples

The Gram matrix of the linear Bézier curve (the paper’s closed form for \(M = 2\), \(D = 1\)):

>>> from torch_bsf.simploidal import BezierSimploid, gram_matrix
>>> import torch
>>> gram = gram_matrix(BezierSimploid.zeros((1,), 1, n_values=1))
>>> torch.allclose(gram * 6, torch.tensor([[2.0, 1.0], [1.0, 2.0]], dtype=torch.float64))
True
torch_bsf.simploidal.linear_scalarization(objective: Callable[[Tensor], Tensor]) → Callable[[...], Tensor][source]

The loss \(\ell(\mathbf x, \mathbf t) = \mathbf t^\top f(\mathbf x)\) of Hikima et al. [HKT+25].

It weights the objectives \(f = (f_1, \ldots, f_M)\) by the parameter \(\mathbf t\) itself, so it is meant for a Bézier simplex on \(\Delta^{M-1}\), whose parameters have M columns.

Parameters:

objective – The objectives: a differentiable map from points of shape (batch, L) to objective values of shape (batch, M).

Return type:

A loss for fit_sgd() that raises ValueError unless the parameters have M columns.

Examples

>>> import torch
>>> from torch_bsf.simploidal import linear_scalarization
>>> loss = linear_scalarization(lambda x: torch.stack([x[:, 0] ** 2, (x[:, 0] - 1) ** 2], dim=1))
>>> loss(torch.tensor([[0.5]]), torch.tensor([[0.25, 0.75]]))
tensor([0.2500])
torch_bsf.simploidal.load(path: str | Path) → BezierSimploid[source]

Loads a Bézier simploid saved by save().

The simploid type and the degree are inferred from the control point indices; every control point of that type and degree must be present exactly once. A flat key such as "(2, 0, 0)" is the index of a Bézier simplex (a single factor), and a nested key such as "((2, 0, 0), (1, 0))" is the index of any Bézier simploid. A flat key with a single coordinate such as "(3,)", written by earlier versions for a model with one parameter, is read as the vertex "()".

Parameters:

path – The path to a .csv, .tsv, .json, .yml, .yaml or .pt file.

Return type:

The Bézier simploid.

Raises:

ValueError – If the file type is unknown or the control points are invalid or incomplete.

Examples

>>> import tempfile
>>> from pathlib import Path
>>> import torch
>>> import torch_bsf
>>> from torch_bsf.simploidal import load, sample_simploid, save
>>> t = sample_simploid((2, 1), 50, seed=0)
>>> bs = torch_bsf.fit(t, t[:, :3] * t[:, 3:4], degree=1, simploid_type=(2, 1))
>>> with tempfile.TemporaryDirectory() as d:
...     save(Path(d) / "simploid.json", bs)
...     loaded = load(Path(d) / "simploid.json")
>>> loaded.simploid_type, loaded.degree
((2, 1), (1, 1))
>>> torch.allclose(loaded(t), bs(t))
True
torch_bsf.simploidal.sample_domain(model: BezierSimploid | SimploidalBezierSpace, n_samples: int, *, weights: Mapping[Simploid, float] | None = None, seed: int | None = None) → list[Tensor][source]

Samples inputs uniformly on the domain of a model.

For a Bézier simploid, the parameters are uniform on its simploid (see sample_simploid()). For a simploidal Bézier space, each sample lies on a simploid drawn with probability weights and is uniform on it; its parameters are padded with zeros to the widest simploid, as SimploidalBezierSpace.forward() expects.

Parameters:
  • model – A Bézier simploid or a simploidal Bézier space.

  • n_samples – The number of samples.

  • weights – For a space, the probability of every non-degenerate simploid (normalized to sum to one). Uniform over the main simploids of the space (those that are not a face of another) if None. Ignored for a Bézier simploid.

  • seed – Random seed for reproducibility.

Returns:

  • The inputs of the model ([t] for a Bézier simploid and [t, simploid_ids] for a space,)

  • so that model(*inputs) evaluates it.

Examples

>>> from torch_bsf.simploidal import SimploidalSet, SimploidalBezierSpace, sample_domain
>>> S = SimploidalSet()
>>> tri, sq = S.add_simplex(2), S.add_simploid((1, 1))
>>> space = SimploidalBezierSpace(S, degree=2, n_values=1)
>>> t, ids = sample_domain(space, 5, seed=0)
>>> t.shape, ids.shape
(torch.Size([5, 4]), torch.Size([5]))
torch_bsf.simploidal.sample_simploid(simploid_type: Sequence[int], n_samples: int, seed: int | None = None) → Tensor[source]

Samples points uniformly on a simploid.

Each factor is sampled from the flat Dirichlet distribution.

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

  • n_samples – The number of samples.

  • seed – Random seed for reproducibility.

Return type:

Parameters of shape (n_samples, sum(a_i + 1)).

Examples

>>> from torch_bsf.simploidal import sample_simploid
>>> t = sample_simploid((2, 1), 4, seed=0)
>>> t.shape
torch.Size([4, 5])
>>> bool(((t[:, :3].sum(dim=1) - 1).abs() < 1e-6).all())
True
torch_bsf.simploidal.save(path: str | Path, data: BezierSimploid) → None[source]

Saves a Bézier simploid to a file.

The format is chosen by the file extension:

  • .csv / .tsv: one control point per row, its index key followed by its values;

  • .json / .yml / .yaml: a mapping from index keys to lists of values;

  • .pt: a dictionary with the keys "simploid_type", "degree" and "control_points" saved with torch.save().

An index key is the string representation of the index \((\boldsymbol\alpha^1, \ldots, \boldsymbol\alpha^n)\), e.g. "((2, 0, 0), (1, 0))", except that the key of a Bézier simplex (a single factor) is its multi-index \(\boldsymbol\alpha^1\) alone, e.g. "(2, 0, 0)". Keys are written in the order of simploid_indices(). The simploid type and the degree are not stored separately: load() recovers them from the keys.

Parameters:
  • path – The file path to save.

  • data – The Bézier simploid to save.

Raises:

ValueError – If the file type is unknown.

Examples

>>> import tempfile
>>> from pathlib import Path
>>> import torch
>>> from torch_bsf.simploidal import BezierSimploid, save
>>> bs = BezierSimploid((1, 1), 1, torch.arange(8.0).reshape(4, 2))
>>> with tempfile.TemporaryDirectory() as d:
...     save(Path(d) / "simploid.csv", bs)
...     print((Path(d) / "simploid.csv").read_text(), end="")
"((1, 0), (1, 0))",0.0,1.0
"((1, 0), (0, 1))",2.0,3.0
"((0, 1), (1, 0))",4.0,5.0
"((0, 1), (0, 1))",6.0,7.0

The keys of a Bézier simplex are flat:

>>> simplex = BezierSimploid((2,), 1, torch.eye(3))
>>> with tempfile.TemporaryDirectory() as d:
...     save(Path(d) / "simplex.csv", simplex)
...     print((Path(d) / "simplex.csv").read_text(), end="")
"(1, 0, 0)",1.0,0.0,0.0
"(0, 1, 0)",0.0,1.0,0.0
"(0, 0, 1)",0.0,0.0,1.0
torch_bsf.simploidal.simploid_indices(simploid_type: Sequence[int], degree: int | Sequence[int]) → list[tuple[tuple[int, ...], ...]][source]

Lists the control point indices of a Bézier simploid in canonical order.

The order is the lexicographic order of the tuples of multi-indices, the first factor varying slowest (Algorithm 4 of Peltier and Lienhardt [PL18]); the multi-indices of each factor are in reverse lexicographic order, e.g. (2, 0), (1, 1), (0, 2).

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

  • degree – The degree of every factor, or one degree per factor.

Return type:

The indices.

Examples

>>> from torch_bsf.simploidal import simploid_indices
>>> simploid_indices((1, 1), 1)
[((1, 0), (1, 0)), ((1, 0), (0, 1)), ((0, 1), (1, 0)), ((0, 1), (0, 1))]
>>> simploid_indices((), 3)
[()]
torch_bsf.simploidal.simploid_n_params(simploid_type: Sequence[int]) → int[source]

The length \(\sum_i (a_i + 1)\) of the parameter vector of a simploid.

Examples

>>> from torch_bsf.simploidal import simploid_n_params
>>> simploid_n_params((2, 1))
5

torch_bsf.sklearn module

class torch_bsf.sklearn.BezierSimploidRegressor(*args: Any, **kwargs: Any)[source]

Bases: RegressorMixin, BaseEstimator

Scikit-learn wrapper for Bézier simplex and Bézier simploid fitting.

Each sample of X is the parameter of a point on a simplex, or on a simploid of type simploid_type, and the model is fitted with torch_bsf.fit().

Parameters:
  • degree (int | Sequence[int], default=3) – The degree of every factor, or one degree per factor.

  • simploid_type (Sequence[int] | None, default=None) – The type of the simploid. If None, the samples are on a simplex: the type is (n_features - 1,), or () (a constant) for a single feature.

  • solver ({"lstsq", "trainer"}, default="lstsq") – "lstsq" solves the least-squares problem exactly in double precision, and the fitted model keeps the floating dtype of y; "trainer" trains the control points with lightning.pytorch.Trainer in single precision.

  • ridge (float, default=0.0) – Weight of the Tikhonov penalty on the control points (solver="lstsq" only).

  • smoothness_weight (float, default=0.0) – The weight of the smoothness penalty.

  • init (BezierSimploid | tensor | mapping | None, default=None) – Initial control points; required by freeze.

  • freeze (Iterable of indices | None, default=None) – Indices of control points that keep their values in init.

  • normalize ({"none", "max", "std", "quantile"}, default="none") – How y is normalized while fitting; the predictions are in the scale of y either way.

  • batch_size (int | None, default=None) – Size of minibatches (solver="trainer" only).

  • max_epochs (int, default=1000) – Maximum number of epochs to train (solver="trainer" only).

  • accelerator (str, default="auto") – Hardware accelerator to use (“cpu”, “gpu”, “auto”, etc.; solver="trainer" only).

  • devices (int | str, default="auto") – Number of devices to use (solver="trainer" only).

  • precision (str, default="32-true") – Floating point precision (solver="trainer" only).

  • trainer_kwargs (dict | None, default=None) – Additional keyword arguments for lightning.pytorch.Trainer (solver="trainer" only).

  • solver="lstsq". (The options marked solver="trainer" only are ignored by)

model_[source]

The fitted model.

Type:

BezierSimploid

simploid_type_[source]

The type of the fitted model.

Type:

tuple[int, …]

Examples

>>> import numpy as np
>>> from torch_bsf.sklearn import BezierSimploidRegressor
>>> X = np.array([[1.0, 0.0], [0.75, 0.25], [0.5, 0.5], [0.25, 0.75], [0.0, 1.0]])
>>> y = X[:, :1] ** 2
>>> reg = BezierSimploidRegressor(degree=2).fit(X, y)
>>> np.allclose(reg.predict(X), y)
True

On a square, the product of two segments:

>>> from torch_bsf.simploidal import sample_simploid
>>> X = sample_simploid((1, 1), 20, seed=0).numpy()
>>> y = X[:, 0] * X[:, 2]
>>> reg = BezierSimploidRegressor(degree=1, simploid_type=(1, 1)).fit(X, y)
>>> np.allclose(reg.predict(X), y, atol=1e-6)
True
fit(X: Any, y: Any) → BezierSimploidRegressor[source]

Fit the model.

Parameters:
  • X (array-like of shape (n_samples, n_features)) – Training parameters on the simplex or simploid.

  • y (array-like of shape (n_samples,) or (n_samples, n_values)) – Target values. A one-dimensional y is treated as a single output, and predict() then returns a one-dimensional array.

Returns:

self – Fitted estimator.

Return type:

object

Raises:

ValueError – If the data do not match simploid_type, or an option is not valid (see torch_bsf.fit()).

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

Predict using the fitted model.

Parameters:

X (array-like of shape (n_samples, n_features)) – Input parameters.

Returns:

y_pred – Predicted values; one-dimensional if the model was fitted with a one-dimensional y.

Return type:

ndarray of shape (n_samples, n_values) or (n_samples,)

score(X: Any, y: Any, sample_weight: Any = None) → float[source]

Return the coefficient of determination R^2 of the prediction.

Parameters:
  • X (array-like of shape (n_samples, n_features)) – Test samples.

  • y (array-like of shape (n_samples, n_values)) – True values.

  • sample_weight (array-like of shape (n_samples,), default=None) – Sample weights.

Returns:

score – R^2 of self.predict(X) wrt. y.

Return type:

float

torch_bsf.splitting module

Splitting Bézier simplices and Bézier simploids via the de Casteljau algorithm.

This module splits a Bézier simplex along a single edge by inserting a new vertex, and computes the control points of the two resulting sub-simplices with the de Casteljau algorithm. A Bézier simploid, a tensor product of Bézier simplices, is split along an edge of one of its factors in the same way; the other factors are left as they are.

The split can be chosen explicitly or determined automatically by a SplitCriterion.

Examples

Split a Bézier curve at its midpoint:

>>> import torch
>>> from torch_bsf import BezierSimploid
>>> from torch_bsf.splitting import split
>>> bs = BezierSimploid.rand((1,), 3, n_values=3)
>>> bs_A, bs_B = split(bs, i=0, j=1, s=0.5)
>>> bs_A.simploid_type == bs.simploid_type and bs_A.degree == bs.degree
True

Use the longest-edge criterion to choose the split automatically:

>>> from torch_bsf.splitting import longest_edge_criterion, split_by_criterion
>>> bs_A, bs_B = split_by_criterion(bs, longest_edge_criterion)
torch_bsf.splitting.SplitCriterion[source]

a callable that accepts a BezierSimploid and returns (factor, i, j, s): the factor to split, the vertices i and j of its edge and the split parameter. For a Bézier simplex, factor is 0.

Type:

Type of a split criterion

alias of Callable[[BezierSimploid], tuple[int, int, int, float]]

torch_bsf.splitting.longest_edge_criterion(model: BezierSimploid, s: float = 0.5) → tuple[int, int, int, float][source]

Select the edge with the greatest value-space length and split it at s.

The “length” of edge \((i, j)\) of a Bézier simplex is the Euclidean distance between its values at the two vertices of the parameter domain:

\[\ell_{ij} = \|B(e_i) - B(e_j)\|_2 = \|b_{n \cdot e_i} - b_{n \cdot e_j}\|_2\]

where \(n\) is the degree and \(e_k\) is the \(k\)-th unit vector. An edge of a factor of a Bézier simploid is parallel to an edge for every vertex of the other factors, and its length is the greatest length of them. Ties go to the first edge in the order of factors, then of (i, j).

Parameters:
  • model – The Bézier simplex or simploid.

  • s – Split parameter included verbatim in the returned tuple (defaults to 0.5).

Returns:

The edge with the greatest value-space length, together with s.

Return type:

(factor, i, j, s)

Examples

>>> from torch_bsf import BezierSimploid
>>> from torch_bsf.splitting import longest_edge_criterion
>>> bs = BezierSimploid((2,), 2, {
...     (2, 0, 0): [0.0], (1, 1, 0): [0.0], (1, 0, 1): [0.0],
...     (0, 2, 0): [1.0], (0, 1, 1): [0.0], (0, 0, 2): [3.0],
... })
>>> longest_edge_criterion(bs)
(0, 0, 2, 0.5)
torch_bsf.splitting.max_error_criterion(params: Tensor, values: Tensor, grid_size: int = 10) → Callable[[BezierSimploid], tuple[int, int, int, float]][source]

Build a criterion that minimises the combined approximation error.

For each candidate edge (i, j) of each factor and split parameter s drawn from a uniform grid over (0, 1), the combined mean-squared error

\[E(i, j, s) = \mathrm{MSE}_A + \mathrm{MSE}_B\]

is computed, where \(\mathrm{MSE}_A\) and \(\mathrm{MSE}_B\) are evaluated on the portions of the data that fall within each piece. The candidate minimising E is returned.

Parameters:
  • params – Parameter vectors of shape (N, n_params).

  • values – Target value vectors of shape (N, n_values).

  • grid_size – Number of candidate s values in the grid search (default 10). Larger values give a finer search at higher cost.

Returns:

A callable criterion(model) -> (factor, i, j, s).

Return type:

SplitCriterion

Examples

>>> import torch
>>> from torch_bsf import BezierSimploid
>>> from torch_bsf.splitting import max_error_criterion
>>> params = torch.tensor([[1.0, 0.0], [0.5, 0.5], [0.0, 1.0]])
>>> values = torch.tensor([[0.0], [0.5], [1.0]])
>>> bs = BezierSimploid.rand((1,), 1, n_values=1)
>>> criterion = max_error_criterion(params, values)
>>> criterion(bs)[:3]
(0, 0, 1)
torch_bsf.splitting.reparametrize(t: Tensor, i: int, j: int, s: float, part: Literal['A', 'B'], *, simploid_type: Sequence[int] | None = None, factor: int = 0) → tuple[Tensor, Tensor][source]

Re-parameterise points of a split model to one of its two pieces.

After splitting edge (i, j) at s, each point of the original domain belongs to one of the two pieces. This function converts the original parameters to the local parameters of a piece.

Parameters:
  • t – Parameters of shape (N, n_params) of the original model.

  • i – Edge vertex indices used in split().

  • j – Edge vertex indices used in split().

  • s – Split parameter.

  • part – "A" for the piece covering \(\{t : t_j / (t_i + t_j) \le s\}\), or "B" for the complementary region.

  • simploid_type – The type of the original model; None for a simplex (whose barycentric coordinates are all of t).

  • factor – The factor that was split, as in split().

Returns:

(u, mask) –

  • u — local parameters of the piece, of the same shape as t.

  • mask — boolean tensor of shape (N,) indicating which input points belong to the requested piece. Points where \(t_i = t_j = 0\) belong to both pieces and are included in both masks.

Return type:

tuple[torch.Tensor, torch.Tensor]

Notes

Writing \(t_k\) for the coordinates of the factor, piece A transforms them as

\[u_j = \frac{t_j}{s}, \quad u_i = t_i - \frac{1-s}{s}\,t_j, \quad u_k = t_k \; (k \ne i, j),\]

and piece B as

\[u_i = \frac{t_i}{1-s}, \quad u_j = t_j - \frac{s}{1-s}\,t_i, \quad u_k = t_k \; (k \ne i, j).\]

The coordinates of the other factors are unchanged.

Examples

>>> import torch
>>> from torch_bsf.splitting import reparametrize
>>> t = torch.tensor([[0.8, 0.2], [0.2, 0.8]])
>>> u, mask = reparametrize(t, 0, 1, 0.5, "A")
>>> mask.tolist(), [round(x, 6) for x in u[0].tolist()]
([True, False], [0.6, 0.4])
torch_bsf.splitting.split(model: BezierSimploid, i: int, j: int, s: float = 0.5, *, factor: int = 0) → tuple[BezierSimploid, BezierSimploid][source]

Split a Bézier simplex or simploid along edge (i, j) using the de Casteljau algorithm.

A new vertex is inserted on the edge between vertex i and vertex j of the parameter domain at the relative position s. The original simplex is thereby subdivided into two sub-simplices that together cover the entire original domain. For a Bézier simploid, the edge is one of the simplex of factor factor, and the other factors are left as they are.

Parameters:
  • model – The Bézier simplex or simploid to split.

  • i – Indices of the two vertices that define the split edge (0-indexed within the factor, i ≠ j).

  • j – Indices of the two vertices that define the split edge (0-indexed within the factor, i ≠ j).

  • s – Split parameter in the open interval (0, 1). s = 0.5 produces a midpoint split. The new vertex is located at \((1-s)\,v_i + s\,v_j\) in the parameter domain.

  • factor – The factor to split; 0 for a Bézier simplex.

Returns:

(bs_A, bs_B) – Two models of the same type and degree as model:

  • bs_A — replaces vertex \(j\) with the new vertex. It covers the sub-domain \(\{t : t_j / (t_i + t_j) \le s\}\).

  • bs_B — replaces vertex \(i\) with the new vertex. It covers the sub-domain \(\{t : t_j / (t_i + t_j) \ge s\}\).

Here \(t_i\) and \(t_j\) are the coordinates of the factor; reparametrize() maps points of model to those of the two pieces.

Return type:

tuple[BezierSimploid, BezierSimploid]

Raises:

ValueError – If factor, i, j or s is out of range.

Notes

The algorithm runs \(n\) de Casteljau steps along the chosen edge direction, where \(n\) is the degree of the factor.

At each step \(r = 1, \ldots, n\) the control-point matrix is updated as

\[\begin{split}c^{(r)}_\alpha = \begin{cases} s \cdot c^{(r-1)}_\alpha + (1-s) \cdot c^{(r-1)}_{\alpha + e_i - e_j} & \text{if } \alpha_j \ge 1 \\ c^{(r-1)}_\alpha & \text{otherwise,} \end{cases}\end{split}\]

where \(\alpha\) is the multi-index of the factor, and rows with \(\alpha_j = r\) are saved as control points of bs_A. An analogous recursion with the roles of \(i\) and \(j\) swapped gives bs_B. The two pieces agree on their common face.

Examples

Split the identity Bézier curve at the midpoint:

>>> from torch_bsf import BezierSimploid
>>> from torch_bsf.splitting import split
>>> bs = BezierSimploid((1,), 1, {(1, 0): [0.0], (0, 1): [1.0]})
>>> bs_A, bs_B = split(bs, i=0, j=1, s=0.5)
>>> bs_A[(0, 1)].item()  # split-point value
0.5
>>> bs_B[(1, 0)].item()  # same split point from other side
0.5

Split a bilinear patch along its second factor:

>>> import torch
>>> patch = BezierSimploid.rand((1, 1), 1, n_values=1)
>>> bs_A, bs_B = split(patch, 0, 1, 0.5, factor=1)
>>> torch.allclose(bs_A(torch.tensor([[0.3, 0.7, 0.0, 1.0]])), patch(torch.tensor([[0.3, 0.7, 0.5, 0.5]])))
True
torch_bsf.splitting.split_by_criterion(model: BezierSimploid, criterion: Callable[[BezierSimploid], tuple[int, int, int, float]]) → tuple[BezierSimploid, BezierSimploid][source]

Split a Bézier simplex or simploid using a split criterion.

Convenience wrapper that calls criterion(model) to obtain the factor, edge and split parameter, then delegates to split().

Parameters:
  • model – The Bézier simplex or simploid to split.

  • criterion – A SplitCriterion callable that returns (factor, i, j, s).

Returns:

(bs_A, bs_B) – The two pieces; see split() for details.

Return type:

tuple[BezierSimploid, BezierSimploid]

Examples

>>> from torch_bsf import BezierSimploid
>>> from torch_bsf.splitting import longest_edge_criterion, split_by_criterion
>>> bs = BezierSimploid.rand((1,), 2, n_values=3)
>>> bs_A, bs_B = split_by_criterion(bs, longest_edge_criterion)
>>> bs_A.degree == bs.degree
True

torch_bsf.validator module

Parsers of command-line arguments.

torch_bsf.validator.index_list(val: str) → list[list[int]][source]

Parse val into a list of indices.

Parameters:

val – A string expression of a list of indices.

Return type:

The persed indices.

torch_bsf.validator.int_or_str(val: str) → int | str[source]

Try to parse int. Return the int value if the parse is succeeded; the original string otherwise.

Parameters:

val – The value to try to convert into int.

Returns:

The converted integer or the original value.

Return type:

Union[int, str]

Module contents

torch_bsf: PyTorch implementation of Bézier simploid fitting.

BSF stands for Bézier Simploid Fitting (formerly Bézier Simplex Fitting). It fits Bézier simplices and, more generally, Bézier simploids, i.e., tensor products of Bézier simplices, and simploidal sets glued from them. A Bézier simplex is the Bézier simploid of a single factor, so both are BezierSimploid models fitted by fit().

torch_bsf.fit(params: Tensor, values: Tensor, degree: int | Sequence[int] | None = None, *, simploid_type: Sequence[int] | None = None, init: BezierSimploid | Tensor | Mapping[Any, Sequence[float] | Tensor] | None = None, freeze: Iterable[tuple[tuple[int, ...], ...] | Sequence[int]] | None = None, smoothness_weight: float = 0.0, normalize: Literal['max', 'std', 'quantile', 'none'] = 'none', solver: Literal['lstsq', 'trainer'] = 'lstsq', ridge: float = 0.0, batch_size: int | None = None, seed: int | None = None, **kwargs: Any) → BezierSimploid[source]

Fits a Bézier simplex, or more generally a Bézier simploid, to data.

Without simploid_type, params are the barycentric coordinates of a simplex, and the result is a Bézier simplex: the Bézier simploid of type (n_params - 1,).

The control points minimize the mean squared error \(\frac{1}{nm} \sum_k \|B(\mathbf t_k) - \mathbf x_k\|^2\) plus smoothness_weight times the sum of \(\|P_p - P_q\|^2\) over the adjacent control points (see BezierSimploid.adjacency()), where n is the number of samples and m that of values. Both solvers minimize this objective; "lstsq" does so exactly (see torch_bsf.simploidal.fitting).

Parameters:
  • params – Parameters of shape (n, n_params): the barycentric coordinates of a simplex, or of every factor of a simploid of type \((a_1, \ldots, a_n)\), concatenated (n_params == sum(a_i + 1)).

  • values – Values of shape (n, n_values).

  • degree – The degree of every factor, or one degree per factor. Optional if init is a Bézier simploid, whose degree is then used.

  • simploid_type – The type \((a_1, \ldots, a_n)\). Defaults to the type of init if it is a Bézier simploid, and to the simplex (n_params - 1,) otherwise.

  • init – Initial control points: a Bézier simploid of the same type, degree and number of values, or control points as accepted by BezierSimploid, in the scale of values. "trainer" starts from them (from random control points if None); "lstsq" uses them only for the frozen rows.

  • freeze – Indices of control points that keep their values in init, which is then required. A Bézier simplex also accepts its multi-indices alone, e.g. (3, 0, 0).

  • smoothness_weight – The weight \(w \ge 0\) of the smoothness penalty.

  • normalize – How the values are normalized while fitting: "max", "std", "quantile" or "none" (see torch_bsf.preprocessing). The normalization is undone on the control points, so the returned model predicts in the scale of values either way; it changes only how the columns of values are weighted in the objective.

  • solver – "lstsq" solves the (linear) least-squares problem exactly; "trainer" trains with lightning.pytorch.Trainer.

  • ridge – Weight of the Tikhonov penalty \(\mathrm{ridge}\,\|P\|^2\) on the free control points, added to \(nm\) times the objective ("lstsq" only). Without it, under-determined problems get the minimum-norm solution. With normalize, the penalty applies to the normalized control points, so it shrinks them toward the offset of the normalization (e.g., the mean of values for "std") rather than zero, like an unpenalized intercept.

  • batch_size – Minibatch size ("trainer" only; full batch by default).

  • seed – Random seed passed to lightning.pytorch.seed_everything() ("trainer" only).

  • kwargs – Arguments for lightning.pytorch.Trainer ("trainer" only).

Return type:

The fitted Bézier simploid, predicting in the scale of values.

Raises:

ValueError – If the shapes do not match, there are no samples, degree is missing, freeze is given without init, or an option is not supported by solver.

Examples

Fit a Bézier triangle to data on a triangle:

>>> import torch
>>> import torch_bsf
>>> from torch_bsf.sampling import simplex_grid
>>> ts = simplex_grid(n_params=3, degree=3)  # parameters on a triangle
>>> xs = 1 - ts * ts  # values corresponding to the parameters
>>> bs = torch_bsf.fit(ts, xs, degree=2)
>>> bs.simploid_type
(2,)
>>> torch.allclose(bs(ts), xs, atol=1e-5)
True

Fit a Bézier simploid on a prism, the product of a triangle and an edge:

>>> from torch_bsf.simploidal import sample_simploid
>>> t = sample_simploid((2, 1), 100, seed=0)
>>> x = t[:, :3] * t[:, 3:4]
>>> bs = torch_bsf.fit(t, x, degree=(1, 1), simploid_type=(2, 1))
>>> torch.allclose(bs(t), x, atol=1e-5)
True

Keep the control point at a vertex and smooth the others:

>>> init = torch_bsf.fit(t, x, degree=2, simploid_type=(2, 1))
>>> vertex = ((2, 0, 0), (2, 0))
>>> with torch.no_grad():
...     _ = init[vertex].fill_(1.0)
>>> bs = torch_bsf.fit(t, x, init=init, freeze=[vertex], smoothness_weight=0.01)
>>> bs[vertex].tolist()
[1.0, 1.0, 1.0]
torch_bsf.fit_kfold(params: Tensor, values: Tensor, degree: int | Sequence[int] | None = None, *, n_folds: int = 5, shuffle: bool = True, seed: int | None = None, **kwargs: Any) → ModuleList[source]

Fits an ensemble of Bézier simploids by k-fold cross-validation.

Splits the samples into n_folds folds and fits one model per fold, with fit(), to the samples of the other folds. The resulting ModuleList can be passed directly to torch_bsf.active_learning.suggest_next_points() to drive a Query-By-Committee active learning loop.

If len(params) < n_folds, the number of folds is capped at len(params).

Parameters:
  • params – Parameters of shape (n, n_params), as in fit().

  • values – Values of shape (n, n_values).

  • degree – The degree, as in fit().

  • n_folds – The number of folds (committee size).

  • shuffle – Whether to shuffle the samples before splitting them into folds. Otherwise, every fold is a contiguous block of samples.

  • seed – Random seed for shuffling, also passed to fit().

  • kwargs – Other arguments for fit(), e.g. simploid_type, smoothness_weight, solver and lightning.pytorch.Trainer arguments.

Returns:

  • A ModuleList of min(n_folds, len(params)) fitted BezierSimploid

  • models; model k is fitted to all samples but those of fold k.

Raises:

ValueError – If n_folds < 2, len(params) < 2 or len(params) != len(values), or from fit().

Examples

>>> import torch
>>> import torch_bsf
>>> from torch_bsf.active_learning import suggest_next_points
>>> from torch_bsf.sampling import simplex_grid

Prepare training data

>>> params = simplex_grid(n_params=3, degree=3)
>>> values = params.pow(2).sum(dim=1, keepdim=True)

Build a 5-fold ensemble and suggest the 2 most uncertain points

>>> models = torch_bsf.fit_kfold(params, values, degree=2, seed=0)
>>> len(models)
5
>>> suggestions = suggest_next_points(models, n_suggestions=2, method="qbc")
>>> suggestions.shape
torch.Size([2, 3])

See also

fit

Fit a single model.

torch_bsf.active_learning.suggest_next_points

Use the ensemble for active learning.

torch_bsf.fit_sgd(model: _Model, loss: Callable[[...], Tensor], *, inputs: Sequence[Tensor] | None = None, n_samples: int = 100, batch_size: int | None = None, max_iter: int = 100, step_size: float = 0.05, step_decay: Literal['constant', 'sqrt'] = 'constant', preconditioner: Literal['gram', 'none'] = 'gram', weights: Mapping[Simploid, float] | None = None, freeze: Iterable[tuple[tuple[int, ...], ...] | Sequence[int] | tuple[Simploid, tuple[tuple[int, ...], ...]]] | None = None, smoothness_weight: float = 0.0, seed: int | None = None, callback: Callable[[int, _Model, float], bool | None] | None = None) → _Model[source]

Fits a model to the minimizers of a loss by (preconditioned) stochastic gradient descent.

This is Algorithm 1 of Hikima et al. [HKT+25], generalized to Bézier simploids and simploidal Bézier spaces (see torch_bsf.simploidal.sgd). It minimizes the mean of loss over fixed samples inputs of the domain: at every iteration k, it draws a minibatch of batch_size samples uniformly without replacement and updates the parameter \(P\) of the model as \(P \leftarrow P - \alpha_k \Sigma^{-1} \nabla \mathcal L_B(P)\), where \(\Sigma\) is gram_matrix() (or the identity for plain SGD).

The control points in freeze keep their values in model, and only the free rows \(F\) of \(P\) move. With smoothness_weight \(w > 0\), the loss also has the smoothness penalty \(w \sum_{(p, q)} \|P_p - P_q\|^2 = w \operatorname{tr}(P^\top L P)\) over the adjacent control points (see model.adjacency()), as in fit(), where \(L\) is the Laplacian of the adjacency graph. The step then minimizes the linearized loss plus the exact penalty plus the move of the model over the free rows, which gives

\[P_F \leftarrow P_F - \alpha_k (\Sigma_{FF} + 2 \alpha_k w L_{FF})^{-1} \nabla_F \bigl(\mathcal L_B(P) + w \operatorname{tr}(P^\top L P)\bigr).\]

Treating the penalty exactly rather than by its gradient keeps the step stable for any \(w\), while \(\Sigma^{-1} L\) has eigenvalues in the thousands already for a cubic Bézier triangle. Without frozen rows and with \(w = 0\), this is the step above.

Parameters:
  • model – The model to fit, which also gives the initial control points (zeros in the paper). It is not modified.

  • loss – The loss loss(values, *inputs) of every sample (see SGDLoss), differentiable with respect to values; e.g., linear_scalarization() to approximate a Pareto set.

  • inputs – The samples, as the arguments of model.forward. Drawn with sample_domain() if None.

  • n_samples – The number of samples drawn when inputs is None.

  • batch_size – The minibatch size (full batch if None or larger than the number of samples).

  • max_iter – The number of iterations.

  • step_size – The step size \(\alpha > 0\).

  • step_decay – "constant" uses \(\alpha_k = \alpha\); "sqrt" uses \(\alpha_k = \alpha / \sqrt{k + 1}\).

  • preconditioner – "gram" preconditions the gradient by \(\Sigma^{-1}\) (the proposed method); "none" is plain stochastic gradient descent.

  • weights – For a space, the probability of every non-degenerate simploid, used both to sample inputs and in \(\Sigma\). Uniform over the main simploids of the space (those that are not a face of another) if None.

  • freeze – Control points that keep their values in model. For a Bézier simploid, their indices as in fit() (a Bézier simplex also accepts its multi-indices alone, e.g. (2, 0, 0)); for a space, pairs (simploid, index) as in fit_simploidal_set().

  • smoothness_weight – The weight \(w \ge 0\) of the smoothness penalty added to the loss.

  • seed – Random seed for the samples and the minibatches.

  • callback – Called as callback(k, model, batch_loss) after the update of every iteration k = 0, 1, ... (see SGDCallback), where model is the model being fitted and batch_loss is the mean loss of the minibatch at the parameters before the update, without the smoothness penalty of smoothness_weight. If it returns True, the fitting stops after this iteration. It may evaluate model, preferably under torch.no_grad(), but must not modify it: copy the parameters to keep a snapshot.

Return type:

A fitted copy of model.

Raises:

ValueError – If an option is out of range, freeze has an invalid index, the inputs are empty or have different lengths, or loss does not return one value per sample.

Examples

The Pareto set of \(f_m(\mathbf x) = \|\mathbf x - \mathbf e_m\|^2\) (\(m = 1, 2, 3\)) is the triangle spanned by \(\mathbf e_1, \mathbf e_2, \mathbf e_3\), reached at \(\mathbf x = \mathbf t\):

>>> import torch
>>> import torch_bsf
>>> from torch_bsf.simploidal import BezierSimploid, linear_scalarization
>>> def objective(x):
...     return ((x.unsqueeze(1) - torch.eye(3)) ** 2).sum(dim=-1)
>>> init = BezierSimploid.zeros((2,), 1, n_values=3)
>>> bs = torch_bsf.fit_sgd(init, linear_scalarization(objective), batch_size=20, step_size=0.2, seed=0)
>>> torch.allclose(bs.control_points, torch.eye(3), atol=1e-3)
True

A callback records the loss of every iteration and stops the fitting once the loss stops decreasing:

>>> history = []
>>> def callback(k, model, batch_loss):
...     history.append(batch_loss)
...     return k > 0 and history[-2] - batch_loss < 1e-4
>>> bs = torch_bsf.fit_sgd(init, linear_scalarization(objective), step_size=0.2, seed=0, callback=callback)
>>> len(history) < 100  # stopped before max_iter
True

Keep the vertices of a quadratic Bézier triangle at the known minimizers of the single objectives, and smooth its control net:

>>> init = BezierSimploid.zeros((2,), 2, n_values=3)
>>> vertices = [(2, 0, 0), (0, 2, 0), (0, 0, 2)]
>>> with torch.no_grad():
...     for m, vertex in enumerate(vertices):
...         _ = init[vertex].copy_(torch.eye(3)[m])
>>> bs = torch_bsf.fit_sgd(
...     init, linear_scalarization(objective), freeze=vertices, smoothness_weight=1e-3, step_size=0.2, seed=0
... )
>>> bs[(2, 0, 0)].tolist()
[1.0, 0.0, 0.0]
torch_bsf.longest_edge_criterion(model: BezierSimploid, s: float = 0.5) → tuple[int, int, int, float][source]

Select the edge with the greatest value-space length and split it at s.

The “length” of edge \((i, j)\) of a Bézier simplex is the Euclidean distance between its values at the two vertices of the parameter domain:

\[\ell_{ij} = \|B(e_i) - B(e_j)\|_2 = \|b_{n \cdot e_i} - b_{n \cdot e_j}\|_2\]

where \(n\) is the degree and \(e_k\) is the \(k\)-th unit vector. An edge of a factor of a Bézier simploid is parallel to an edge for every vertex of the other factors, and its length is the greatest length of them. Ties go to the first edge in the order of factors, then of (i, j).

Parameters:
  • model – The Bézier simplex or simploid.

  • s – Split parameter included verbatim in the returned tuple (defaults to 0.5).

Returns:

The edge with the greatest value-space length, together with s.

Return type:

(factor, i, j, s)

Examples

>>> from torch_bsf import BezierSimploid
>>> from torch_bsf.splitting import longest_edge_criterion
>>> bs = BezierSimploid((2,), 2, {
...     (2, 0, 0): [0.0], (1, 1, 0): [0.0], (1, 0, 1): [0.0],
...     (0, 2, 0): [1.0], (0, 1, 1): [0.0], (0, 0, 2): [3.0],
... })
>>> longest_edge_criterion(bs)
(0, 0, 2, 0.5)
torch_bsf.max_error_criterion(params: Tensor, values: Tensor, grid_size: int = 10) → Callable[[BezierSimploid], tuple[int, int, int, float]][source]

Build a criterion that minimises the combined approximation error.

For each candidate edge (i, j) of each factor and split parameter s drawn from a uniform grid over (0, 1), the combined mean-squared error

\[E(i, j, s) = \mathrm{MSE}_A + \mathrm{MSE}_B\]

is computed, where \(\mathrm{MSE}_A\) and \(\mathrm{MSE}_B\) are evaluated on the portions of the data that fall within each piece. The candidate minimising E is returned.

Parameters:
  • params – Parameter vectors of shape (N, n_params).

  • values – Target value vectors of shape (N, n_values).

  • grid_size – Number of candidate s values in the grid search (default 10). Larger values give a finer search at higher cost.

Returns:

A callable criterion(model) -> (factor, i, j, s).

Return type:

SplitCriterion

Examples

>>> import torch
>>> from torch_bsf import BezierSimploid
>>> from torch_bsf.splitting import max_error_criterion
>>> params = torch.tensor([[1.0, 0.0], [0.5, 0.5], [0.0, 1.0]])
>>> values = torch.tensor([[0.0], [0.5], [1.0]])
>>> bs = BezierSimploid.rand((1,), 1, n_values=1)
>>> criterion = max_error_criterion(params, values)
>>> criterion(bs)[:3]
(0, 0, 1)
torch_bsf.reparametrize(t: Tensor, i: int, j: int, s: float, part: Literal['A', 'B'], *, simploid_type: Sequence[int] | None = None, factor: int = 0) → tuple[Tensor, Tensor][source]

Re-parameterise points of a split model to one of its two pieces.

After splitting edge (i, j) at s, each point of the original domain belongs to one of the two pieces. This function converts the original parameters to the local parameters of a piece.

Parameters:
  • t – Parameters of shape (N, n_params) of the original model.

  • i – Edge vertex indices used in split().

  • j – Edge vertex indices used in split().

  • s – Split parameter.

  • part – "A" for the piece covering \(\{t : t_j / (t_i + t_j) \le s\}\), or "B" for the complementary region.

  • simploid_type – The type of the original model; None for a simplex (whose barycentric coordinates are all of t).

  • factor – The factor that was split, as in split().

Returns:

(u, mask) –

  • u — local parameters of the piece, of the same shape as t.

  • mask — boolean tensor of shape (N,) indicating which input points belong to the requested piece. Points where \(t_i = t_j = 0\) belong to both pieces and are included in both masks.

Return type:

tuple[torch.Tensor, torch.Tensor]

Notes

Writing \(t_k\) for the coordinates of the factor, piece A transforms them as

\[u_j = \frac{t_j}{s}, \quad u_i = t_i - \frac{1-s}{s}\,t_j, \quad u_k = t_k \; (k \ne i, j),\]

and piece B as

\[u_i = \frac{t_i}{1-s}, \quad u_j = t_j - \frac{s}{1-s}\,t_i, \quad u_k = t_k \; (k \ne i, j).\]

The coordinates of the other factors are unchanged.

Examples

>>> import torch
>>> from torch_bsf.splitting import reparametrize
>>> t = torch.tensor([[0.8, 0.2], [0.2, 0.8]])
>>> u, mask = reparametrize(t, 0, 1, 0.5, "A")
>>> mask.tolist(), [round(x, 6) for x in u[0].tolist()]
([True, False], [0.6, 0.4])
torch_bsf.split(model: BezierSimploid, i: int, j: int, s: float = 0.5, *, factor: int = 0) → tuple[BezierSimploid, BezierSimploid][source]

Split a Bézier simplex or simploid along edge (i, j) using the de Casteljau algorithm.

A new vertex is inserted on the edge between vertex i and vertex j of the parameter domain at the relative position s. The original simplex is thereby subdivided into two sub-simplices that together cover the entire original domain. For a Bézier simploid, the edge is one of the simplex of factor factor, and the other factors are left as they are.

Parameters:
  • model – The Bézier simplex or simploid to split.

  • i – Indices of the two vertices that define the split edge (0-indexed within the factor, i ≠ j).

  • j – Indices of the two vertices that define the split edge (0-indexed within the factor, i ≠ j).

  • s – Split parameter in the open interval (0, 1). s = 0.5 produces a midpoint split. The new vertex is located at \((1-s)\,v_i + s\,v_j\) in the parameter domain.

  • factor – The factor to split; 0 for a Bézier simplex.

Returns:

(bs_A, bs_B) – Two models of the same type and degree as model:

  • bs_A — replaces vertex \(j\) with the new vertex. It covers the sub-domain \(\{t : t_j / (t_i + t_j) \le s\}\).

  • bs_B — replaces vertex \(i\) with the new vertex. It covers the sub-domain \(\{t : t_j / (t_i + t_j) \ge s\}\).

Here \(t_i\) and \(t_j\) are the coordinates of the factor; reparametrize() maps points of model to those of the two pieces.

Return type:

tuple[BezierSimploid, BezierSimploid]

Raises:

ValueError – If factor, i, j or s is out of range.

Notes

The algorithm runs \(n\) de Casteljau steps along the chosen edge direction, where \(n\) is the degree of the factor.

At each step \(r = 1, \ldots, n\) the control-point matrix is updated as

\[\begin{split}c^{(r)}_\alpha = \begin{cases} s \cdot c^{(r-1)}_\alpha + (1-s) \cdot c^{(r-1)}_{\alpha + e_i - e_j} & \text{if } \alpha_j \ge 1 \\ c^{(r-1)}_\alpha & \text{otherwise,} \end{cases}\end{split}\]

where \(\alpha\) is the multi-index of the factor, and rows with \(\alpha_j = r\) are saved as control points of bs_A. An analogous recursion with the roles of \(i\) and \(j\) swapped gives bs_B. The two pieces agree on their common face.

Examples

Split the identity Bézier curve at the midpoint:

>>> from torch_bsf import BezierSimploid
>>> from torch_bsf.splitting import split
>>> bs = BezierSimploid((1,), 1, {(1, 0): [0.0], (0, 1): [1.0]})
>>> bs_A, bs_B = split(bs, i=0, j=1, s=0.5)
>>> bs_A[(0, 1)].item()  # split-point value
0.5
>>> bs_B[(1, 0)].item()  # same split point from other side
0.5

Split a bilinear patch along its second factor:

>>> import torch
>>> patch = BezierSimploid.rand((1, 1), 1, n_values=1)
>>> bs_A, bs_B = split(patch, 0, 1, 0.5, factor=1)
>>> torch.allclose(bs_A(torch.tensor([[0.3, 0.7, 0.0, 1.0]])), patch(torch.tensor([[0.3, 0.7, 0.5, 0.5]])))
True
torch_bsf.split_by_criterion(model: BezierSimploid, criterion: Callable[[BezierSimploid], tuple[int, int, int, float]]) → tuple[BezierSimploid, BezierSimploid][source]

Split a Bézier simplex or simploid using a split criterion.

Convenience wrapper that calls criterion(model) to obtain the factor, edge and split parameter, then delegates to split().

Parameters:
  • model – The Bézier simplex or simploid to split.

  • criterion – A SplitCriterion callable that returns (factor, i, j, s).

Returns:

(bs_A, bs_B) – The two pieces; see split() for details.

Return type:

tuple[BezierSimploid, BezierSimploid]

Examples

>>> from torch_bsf import BezierSimploid
>>> from torch_bsf.splitting import longest_edge_criterion, split_by_criterion
>>> bs = BezierSimploid.rand((1,), 2, n_values=3)
>>> bs_A, bs_B = split_by_criterion(bs, longest_edge_criterion)
>>> bs_A.degree == bs.degree
True

The Bézier simploid classes and fit_simploidal_set() that torch_bsf re-exports are documented in torch_bsf.simploidal.