Exact local subdivision ======================= When to split a model --------------------- Subdivision replaces one fitted cell by two local Bézier models whose images together reproduce the parent. Use it to isolate a region for inspection, evaluate pieces in separate downstream systems, or prepare a later local refinement. It is useful when one global model spans regions with different geometric scales and you want explicit charts for those regions. Subdivision itself is an exact change of parameterization. It does not collect observations, optimize coefficients, change polynomial degree, or reduce prediction error. If a parent underfits a localized feature, its exact children still underfit that feature. Error reduction requires an additional refitting step, usually with new observations or independently adjustable piecewise coefficients. Keep this distinction visible in before/after plots. ``subdivide`` acts on one selected cell. It returns a ``SubdivisionResult`` with two models and two parameter maps. It does not automatically update neighboring cells of an assembled domain, rebuild shared topology, or create a globally remeshed model archive. The maps retain the selected parent cell ID so that evaluation can be traced back to the original model. To refine an assembled space as a whole, use ``subdivide_space`` (see :ref:`conforming-subdivision`). A complete exact split ---------------------- The example fits a scalar quadratic on a segment and splits the parameter edge at fraction .4. Each child has its own full local barycentric domain. The map converts those local points back to the appropriate parent interval. .. testcode:: splitting import torch from torch_bsf import BezierSpace, Samples, fit from torch_bsf.sampling import grid from torch_bsf.splitting import EdgeSplit, LongestEdge, subdivide space = BezierSpace.simplex(1, degree=2) observations = grid(space, 8) x = observations.parameters[:, 1] model = fit(space, Samples(observations, x.square()[:, None])).require_success().model pieces = subdivide(model, cell=0, policy=EdgeSplit(0, 1, fraction=.4)) for child, mapping in zip(pieces.models, pieces.maps, strict=True): child_points = grid(child.space, 40) parent_points = mapping.to_parent(child_points) assert torch.allclose(child(child_points), model(parent_points), atol=1e-12) assert len(pieces.models) == len(pieces.maps) == 2 The selected parent is unchanged. Child coefficient tensors retain its dtype and device, while each child receives a single-cell space. The representation is local even if the original model was assembled from many cells. Always pair each child with its corresponding map; swapping maps produces an incorrect comparison despite identical coordinate widths. .. figure:: ../_static/guides/subdivision.png :width: 100% :alt: A parent curve and two child curves displayed in the same parent parameter coordinate. Before: one fitted cubic. After: two exact pieces at fraction .4, plotted against parent coordinates. The curves join without changing the represented response. The generator checks maximum child/parent discrepancies below 1e-12; the figure does not claim a reduction in fitting error. The figure uses the noisy-data cubic from ``examples/generate_guide_figures.py``, while the executable example above uses an exactly representable quadratic. The same map invariant applies in both cases. The figure's measured errors are stored in :download:`metrics.json <../_static/guides/metrics.json>`. Understand the split fraction ----------------------------- For an edge joining factor vertices :math:`v_i` and :math:`v_j`, fraction :math:`s` inserts a new vertex .. math:: v_{\mathrm{new}}=(1-s)v_i+s v_j,\qquad 0", len(refined.main_cells)) print(sorted(result.parents[k] for k in refined.main_cells) == sorted([space.cell_id(tetra)] * 2 + [space.cell_id(prism)] * 2)) points = Points.from_cells(space, [(prism, torch.tensor([[.2, .3, .5, .6, .4]], dtype=torch.float64))]) print(torch.allclose(model(points), result.model(result.transfer(points)))) .. testoutput:: conforming 2 -> 4 True True ``parents`` gives, for every cell of the refined space, the original cell whose relative interior contains it, and ``transfer`` maps original ``Points`` to the refined space in the same order. A point on an interface between pieces goes to one of them; since the model is continuous there, the choice does not affect the value. ``split`` reports the split of the selected cell chosen by the policy; ``EdgeSplit(j, i, s)`` is the same split as ``EdgeSplit(i, j, 1 - s)``, computed without rounding ``1 - s``. The policies are those of ``subdivide``, evaluated on the selected cell. Keep comparisons in the same physical coordinates ------------------------------------------------- A useful split report includes the parent cell, factor and edge, the chosen fraction, child observation counts, and the maximum child-versus-parent prediction difference before any refit. Then report independent errors after refitting as a second comparison. This sequence distinguishes exact geometry preservation from an actual improvement in the surrogate. For a scalar graph, plot against the parent parameter or a known physical coordinate, as the figure does. Plotting both children against their own local zero-to-one coordinate stretches each piece to the same width and can make them appear unrelated to the parent. For an output-space image, use the same projection and common axis limits. A different control polygon after splitting is expected and is not itself a change of the represented function. Retain original observation IDs when applying membership masks so that later error reports can identify repeated cut-boundary rows. In a recursive process, store the complete mapping path beside each leaf model. A local model archive alone cannot reconstruct which portion of the original experiment its local coordinates describe.