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(seetorch_bsf.simploidal.sample_simploid()), or from the simplex of dimensionn_params - 1for 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 anySequenceofModuleinstances, includingModuleList.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_paramsif that attribute exists. When provided andmodels[0]exposes ann_paramsattribute, the two values must agree; aValueErroris raised on mismatch.simploid_type (Sequence[int], optional) – The type of the simploid to sample. When omitted, it is taken from
models[0].simploid_typeif that attribute exists (e.g., forBezierSimploidmodels), and the candidates are drawn from a simplex otherwise. All models with asimploid_typemust 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:
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 bytorch_bsf.fit().num (int) – The number of grid points for each edge. For
model.n_params >= 4this value is used only to decide whether to use a full meshgrid or random sampling: if the combinatorial meshgrid sizecomb(num + n_params - 1, n_params - 1)exceedsmax_pairwise_points, exactlymax_pairwise_pointsuniformly random simplex samples are drawn instead andnumno 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 whenmodel.n_values > 0(whenmodel.n_values == 0an 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 ofmax_control_pointsis drawn instead to avoid combinatorial slowdowns. Defaults to 500. Ignored whenshow_control_pointsisFalseormodel.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_pointsuniformly random simplex samples are drawn instead to bound memory usage and plot time. Defaults to 2000. Ignored whenmodel.n_params < 4. Must be a non-negative integer.**kwargs – Additional keyword arguments forwarded to the plot call. For
model.n_params == 2, forwarded toax.plot(curve). Formodel.n_params == 3andmodel.n_values >= 3, forwarded toax.plot_trisurf(3D surface). Formodel.n_params == 3andmodel.n_values == 2, ignored. Formodel.n_params >= 4, forwarded toax.scatter(pairwise).
- Returns:
The axes containing the plot. For
model.n_params <= 3a singleAxes(orAxes3D) is returned. Formodel.n_params >= 4a 2-Dnumpy.ndarrayofAxeswith 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
modelis 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_valuesmust 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 == 2and a 3D axes (projection="3d") whenmodel.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_trisurfin 3D,matplotlib.collections.PolyCollectionin 2D).colorsets 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_valuesis neither 2 nor 3,numis not a positive integer, oraxis 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 asplot_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_valuesmust 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 == 2and a 3D axes (projection="3d") whenspace.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().colorsets 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_valuesis neither 2 nor 3,numis not a positive integer, oraxis 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:
objectMin-max scaler that normalizes values to the [0, 1] range.
- scales[source]
Scale factors (max - min) per feature dimension. Dimensions where max equals min are set to 1 to avoid division by zero.
- Type:
- 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:
- 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:
- transform(values: Tensor) Tensor[source]
Normalize
valueswith 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:
- class torch_bsf.preprocessing.NoneScaler[source]
Bases:
objectPass-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(), andinverse_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
valuesunchanged.- Parameters:
values (torch.Tensor) – Input tensor of any shape.
- Returns:
The same tensor as
valueswithout any modification.- Return type:
- inverse_transform(values: Tensor) Tensor[source]
Return
valuesunchanged.- Parameters:
values (torch.Tensor) – Input tensor of any shape.
- Returns:
The same tensor as
valueswithout any modification.- Return type:
- transform(values: Tensor) Tensor[source]
Return
valuesunchanged.- Parameters:
values (torch.Tensor) – Input tensor of any shape.
- Returns:
The same tensor as
valueswithout any modification.- Return type:
- 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:
objectQuantile-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 toMinMaxScaler.- 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:
- scales[source]
Per-feature scale factors (
(1-q)-quantile minusq-quantile). Dimensions where the scale is zero are set to 1 to avoid division by zero.- Type:
- 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:
- 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:
- transform(values: Tensor) Tensor[source]
Scale
valueswith 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:
- class torch_bsf.preprocessing.Scaler(*args, **kwargs)[source]
Bases:
ProtocolProtocol 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_transform(values: Tensor) Tensor[source]
Fit the scaler to
valuesand return the transformed tensor.
- inverse_transform(values: Tensor) Tensor[source]
Reverse the transformation applied by
fit_transform().
- class torch_bsf.preprocessing.StdScaler[source]
Bases:
objectStandard-score (z-score) scaler that normalizes values to zero mean and unit variance.
- stds[source]
Per-feature standard deviations computed during
fit(). Dimensions with zero standard deviation are set to 1 to avoid division by zero.- Type:
- 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:
- 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:
- transform(values: Tensor) Tensor[source]
Standardize
valueswith 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_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
normalizeis 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:
- Returns:
Array of grid points in shape (N, n_params), where N = binom(degree + n_params - 1, n_params - 1).
- Return type:
- 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.Generatoris created with this seed so the global NumPy random state is not affected. WhenNone(default), the globalnumpy.randomstate is used.
- Returns:
Array of sample points in shape (n_samples, n_params).
- Return type:
- Raises:
ValueError – If
n_paramsis not positive orn_samplesis 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_samplesis an exact power of 2 (e.g. 64, 128, 256, …). Whenn_samplesis not a power of 2, the strongest low-discrepancy guarantees no longer apply and the coverage of the simplex can be somewhat less uniform. AUserWarningis 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 withpip install scipyorpip 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 - 1dimensions 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.Sobolas itsseedargument. WhenNone(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:
- Raises:
ImportError – If SciPy is not installed.
ValueError – If
n_paramsis less than 2 orn_samplesis negative.
- Warns:
UserWarning – If
n_samplesis 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
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:
torch_bsf.simploidal.combinatorics: simploid types, control point indices, Bernstein bases and sampling;torch_bsf.simploidal.sets:SimploidandSimploidalSet;torch_bsf.simploidal.bezier:BezierSimploid;torch_bsf.simploidal.space:SimploidalBezierSpace;torch_bsf.simploidal.fitting:torch_bsf.fit(),torch_bsf.fit_kfold()andfit_simploidal_set();torch_bsf.simploidal.sgd:torch_bsf.fit_sgd(), which fits either model to the minimizers of a loss, e.g., to a Pareto set [HKT+25];
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 vertexjof factori);degeneracy(i, j)withj >= 0is \(s^{i+1}_j\) (repeat vertexjof factori);degeneracy(i, -1)is \(s^i_{-1}\) (insert a degenerate edge as the new factori).
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:
ModuleA Bézier simploid.
This is a plain
torch.nn.Moduleholding the control points; fit one to data withtorch_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 followsimploid_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 ifNone, in which casen_valuesis required.n_values – The number of values (target dimension); used only when
control_pointsisNone.
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)satisfyp < 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}_jors^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
- design_matrix(t: Tensor) Tensor[source]
The Bernstein basis at
t, so thatforward(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 ofcontrol_points, in the dtypeof
t(the default dtype iftis 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
- 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
ihas a zero at positionj.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.
- 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_pointsholding 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
indexis not a control point index of this Bézier simploid.
- class torch_bsf.simploidal.Simploid(simploid_type: Sequence[int])[source]
Bases:
objectA 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 aDegeneracyMap, 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 < lengthifj >= 0,0 <= i <= lengthifj == -1.j – The vertex of factor
ito repeat, or-1to insert a degenerate edge as the new factori.
- Return type:
The degenerate simploid.
- property degeneracy_map: tuple[tuple[int, tuple[int, ...]], ...][source]
The degeneracy map from
baseto this simploid (the identity map if not degenerate).
- face(i: int, j: int) Simploid[source]
The face \(d^{i+1}_j\): removes vertex
jof factori.- Parameters:
i – The factor index,
0 <= i < length.j – The vertex index,
0 <= j <= a_i.
- Return type:
The face.
- 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:
ModuleA 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, inproper_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 degreedegreeinsimploid_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 ifNone.default_degree – The degree of the factors left undetermined by a
degreemapping (at least 1). Ignored whendegreeis 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)satisfyp < 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_pointsholding 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 ofproper_control_points, in the dtype oft(the default dtype iftis 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).
- forward(t: Tensor, simploid_ids: Tensor) Tensor[source]
Evaluates a batch mixing several simploids.
- Parameters:
t – Parameters of shape
(batch, width): rowkholds the parameters of simploidsimploids[simploid_ids[k]]in its firstsum(a_i + 1)columns (the rest is ignored).simploid_ids – Indices into
simploids(seesimploid_id()) of shape(batch,).
- Return type:
Values of shape
(batch, n_values).
- row(simploid: Simploid, index: tuple[tuple[int, ...], ...]) int[source]
The row of
proper_control_pointsholding 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
indexis 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
degreeon 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)
- class torch_bsf.simploidal.SimploidalSet[source]
Bases:
objectA 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:
add_vertex(),add_simplex(),add_simploid(): complete cells with their boundary;product(): cartesian product of two simploidal sets;identify(): identification of two simploids (and of their boundaries);degenerate(): degeneracy of a simploid onto a degenerate simploid of one of its faces;cone(): cone with a new vertex.
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)
- 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
0todim(empty if the set is empty).- Raises:
ValueError – If
fieldis 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
cholds the boundary ofsimploids(dim)[c]in the basissimploids(dim - 1).boundary_matrix(0)has no rows.
- Raises:
ValueError – If
dimis 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 conecofsigmasatisfiesc.face(k, 1) is sigmaandc.face(k, 0)is the degeneracy ofvwith the type ofsigma, wherek = 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
simploidwithface.degeneracy(i, j); the two must have the same type, and their boundaries are identified accordingly.- Parameters:
simploid – The simploid to degenerate.
face – The simploid it degenerates into.
i – The degeneracy operator, as in
Simploid.degeneracy().j – The degeneracy operator, as in
Simploid.degeneracy().
- 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
- 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
aandbare identified face by face (a.face(i, j)withb.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 howdegenerate()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 tosigma1 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)
- 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 ofsimploid_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 oflossover fixed samplesinputsof the domain: at every iterationk, it draws a minibatch ofbatch_sizesamples 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\) isgram_matrix()(or the identity for plain SGD).The control points in
freezekeep their values inmodel, and only the free rows \(F\) of \(P\) move. Withsmoothness_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 (seemodel.adjacency()), as infit(), 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 (seeSGDLoss), differentiable with respect tovalues; e.g.,linear_scalarization()to approximate a Pareto set.inputs – The samples, as the arguments of
model.forward. Drawn withsample_domain()ifNone.n_samples – The number of samples drawn when
inputsisNone.batch_size – The minibatch size (full batch if
Noneor 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
inputsand in \(\Sigma\). Uniform over the main simploids of the space (those that are not a face of another) ifNone.freeze – Control points that keep their values in
model. For a Bézier simploid, their indices as infit()(a Bézier simplex also accepts its multi-indices alone, e.g.(2, 0, 0)); for a space, pairs(simploid, index)as infit_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 iterationk = 0, 1, ...(seeSGDCallback), wheremodelis the model being fitted andbatch_lossis the mean loss of the minibatch at the parameters before the update, without the smoothness penalty ofsmoothness_weight. If it returnsTrue, the fitting stops after this iteration. It may evaluatemodel, preferably undertorch.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,
freezehas an invalid index, the inputs are empty or have different lengths, orlossdoes 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_weighttimes the sum of \(\|P_p - P_q\|^2\) over the adjacent stored control points (seeSimploidalBezierSpace.adjacency()), as infit().- Parameters:
simploidal_set – The simploidal set.
data – A sequence of
(simploid, params, values), whereparamshas shape(n, sum(a_i + 1))for the type ofsimploidandvalueshas 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
degreemapping.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 ifNone);"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 withlightning.pytorch.Trainer, which needs thelightningextra (pip install "pytorch-bsf[lightning]").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_matrixat \(\mathbf u\), which is uniform on the domain of the model (seesample_domain()). The matrix is computed exactly (seetorch_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, wherenis the numberof rows of the parameter of
model.
- Raises:
ValueError – If
weightshas 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
Mcolumns.- 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 raisesValueErrorunless the parameters haveMcolumns.
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,.yamlor.ptfile.- 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 probabilityweightsand is uniform on it; its parameters are padded with zeros to the widest simploid, asSimploidalBezierSpace.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 withtorch.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 ofsimploid_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.sklearn module
- class torch_bsf.sklearn.BezierSimploidRegressor(*args: Any, **kwargs: Any)[source]
Bases:
RegressorMixin,BaseEstimatorScikit-learn wrapper for Bézier simplex and Bézier simploid fitting.
Each sample of
Xis the parameter of a point on a simplex, or on a simploid of typesimploid_type, and the model is fitted withtorch_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 ofy;"trainer"trains the control points withlightning.pytorch.Trainerin single precision, which needs thelightningextra (pip install "pytorch-bsf[lightning]").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
yis normalized while fitting; the predictions are in the scale ofyeither 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)
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
yis treated as a single output, andpredict()then returns a one-dimensional array.
- Returns:
self – Fitted estimator.
- Return type:
- Raises:
ValueError – If the data do not match
simploid_type, or an option is not valid (seetorch_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:
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
BezierSimploidand returns(factor, i, j, s): the factor to split, the verticesiandjof its edge and the split parameter. For a Bézier simplex,factoris0.- 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 parametersdrawn 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
Eis returned.- Parameters:
params – Parameter vectors of shape
(N, n_params).values – Target value vectors of shape
(N, n_values).grid_size – Number of candidate
svalues in the grid search (default10). 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)ats, 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;
Nonefor a simplex (whose barycentric coordinates are all oft).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:
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
iand vertexjof the parameter domain at the relative positions. 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 factorfactor, 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.5produces 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;
0for 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 ofmodelto those of the two pieces.- Return type:
- Raises:
ValueError – If
factor,i,jorsis 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 tosplit().- Parameters:
model – The Bézier simplex or simploid to split.
criterion – A
SplitCriterioncallable that returns(factor, i, j, s).
- Returns:
(bs_A, bs_B) – The two pieces; see
split()for details.- Return type:
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.
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,paramsare 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_weighttimes the sum of \(\|P_p - P_q\|^2\) over the adjacent control points (seeBezierSimploid.adjacency()), wherenis the number of samples andmthat of values. Both solvers minimize this objective;"lstsq"does so exactly (seetorch_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
initis a Bézier simploid, whose degree is then used.simploid_type – The type \((a_1, \ldots, a_n)\). Defaults to the type of
initif 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 ofvalues."trainer"starts from them (from random control points ifNone);"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"(seetorch_bsf.preprocessing). The normalization is undone on the control points, so the returned model predicts in the scale ofvalueseither way; it changes only how the columns ofvaluesare weighted in the objective.solver –
"lstsq"solves the (linear) least-squares problem exactly;"trainer"trains withlightning.pytorch.Trainer, which needs thelightningextra (pip install "pytorch-bsf[lightning]").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. Withnormalize, the penalty applies to the normalized control points, so it shrinks them toward the offset of the normalization (e.g., the mean ofvaluesfor"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,
degreeis missing,freezeis given withoutinit, or an option is not supported bysolver.ImportError – If
solver="trainer"is given and Lightning is not installed.
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_foldsfolds and fits one model per fold, withfit(), to the samples of the other folds. The resultingModuleListcan be passed directly totorch_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 atlen(params).- Parameters:
params – Parameters of shape
(n, n_params), as infit().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,solverandlightning.pytorch.Trainerarguments.
- Returns:
A
ModuleListofmin(n_folds, len(params))fittedBezierSimploidmodels; model
kis fitted to all samples but those of foldk.
- Raises:
ValueError – If
n_folds < 2,len(params) < 2orlen(params) != len(values), or fromfit().
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
fitFit a single model.
torch_bsf.active_learning.suggest_next_pointsUse 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 oflossover fixed samplesinputsof the domain: at every iterationk, it draws a minibatch ofbatch_sizesamples 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\) isgram_matrix()(or the identity for plain SGD).The control points in
freezekeep their values inmodel, and only the free rows \(F\) of \(P\) move. Withsmoothness_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 (seemodel.adjacency()), as infit(), 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 (seeSGDLoss), differentiable with respect tovalues; e.g.,linear_scalarization()to approximate a Pareto set.inputs – The samples, as the arguments of
model.forward. Drawn withsample_domain()ifNone.n_samples – The number of samples drawn when
inputsisNone.batch_size – The minibatch size (full batch if
Noneor 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
inputsand in \(\Sigma\). Uniform over the main simploids of the space (those that are not a face of another) ifNone.freeze – Control points that keep their values in
model. For a Bézier simploid, their indices as infit()(a Bézier simplex also accepts its multi-indices alone, e.g.(2, 0, 0)); for a space, pairs(simploid, index)as infit_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 iterationk = 0, 1, ...(seeSGDCallback), wheremodelis the model being fitted andbatch_lossis the mean loss of the minibatch at the parameters before the update, without the smoothness penalty ofsmoothness_weight. If it returnsTrue, the fitting stops after this iteration. It may evaluatemodel, preferably undertorch.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,
freezehas an invalid index, the inputs are empty or have different lengths, orlossdoes 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 parametersdrawn 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
Eis returned.- Parameters:
params – Parameter vectors of shape
(N, n_params).values – Target value vectors of shape
(N, n_values).grid_size – Number of candidate
svalues in the grid search (default10). 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)ats, 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;
Nonefor a simplex (whose barycentric coordinates are all oft).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:
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
iand vertexjof the parameter domain at the relative positions. 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 factorfactor, 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.5produces 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;
0for 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 ofmodelto those of the two pieces.- Return type:
- Raises:
ValueError – If
factor,i,jorsis 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 tosplit().- Parameters:
model – The Bézier simplex or simploid to split.
criterion – A
SplitCriterioncallable that returns(factor, i, j, s).
- Returns:
(bs_A, bs_B) – The two pieces; see
split()for details.- Return type:
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.