download as script · edit page

Approximating Orientation-Dependent Functions from Discrete Data defines the shared approximation problem and compares harmonic, RBF, and Bingham models. This page assumes that an RBF model is appropriate and shows how to choose density constraints, kernel halfwidth, centre orientations, and a least-squares solver.

RBF approximation is often useful for an ODF or another density when the number of observations and their noise level are not too large. It can also fit a general scalar response, but only 'density' imposes the nonnegativity and mean-1 constraints required of a density.

import time

import matplotlib.pyplot as plt
import numpy as np

from mtex import *

plottingConvention.default('y↑→x')

A noisy density data set

Start with the data used on the preceding two pages. This time the noise standard deviation is 20 percent of the standard deviation of the supplied values.

rng = np.random.default_rng(1)
fname = mtexdatafile('dubnaValues')
ori, S = loadOrientation(fname, columnNames=['phi1', 'Phi', 'phi2', 'values'])
val = S['values'] + rng.standard_normal(S['values'].shape) * 0.2 * np.std(S['values'], ddof=1)

plotSection(ori, val, 'sigma', all=True)

The broad high-value regions remain visible, but individual markers vary much more than on the harmonic page. The noise-free values S['values'] are retained only so that this controlled example can later measure recovery error. They would not be available for an unknown experimental response.

Fit a density

interp uses an RBF model by default and calls SO3FunRBF.interpolate. The 'density' flag selects modified least squares, 'mlsq'. This solver constrains the kernel weights to be nonnegative, and MTEX normalizes the fitted function to mean 1.

SO3FDensity = interp(ori, val, density=True)
SO3FDensity
SO3FunRBF (1 → y↑→x)
  multimodal components
    kernel: de la Vallee Poussin, halfwidth 5°
    center: 73610 orientations
    weight: 1
    Bunge Euler angles in degree
    phi1  Phi  phi2    weight
      90  2.5    90  2.03e-07
      90  2.5    95  3.98e-07
      90  2.5   100  9.05e-07
      90  2.5   105  6.66e-07
      90  2.5   110  8.11e-07
       ⋮    ⋮     ⋮         ⋮
      30  178   125   2.5e-08
      30  178   130  2.48e-07
      30  178   135  1.76e-07
      30  178   140  1.96e-07
      30  178   145  5.11e-07

minimumDensityValue = min(SO3FDensity)[0]
minimumDensityValue
0.0027

meanDensityValue = mean(SO3FDensity)
meanDensityValue
1

relativeDataResidual = norm(SO3FDensity.eval(ori) - val) / norm(val)
relativeDataResidual
0.1620

plot(SO3FDensity, 'sigma')
mtexColorbar()

The fit retains the broad peaks while smoothing the marker-to-marker fluctuations. Its printed minimum and mean verify the density constraints. A nonzero residual is expected because a smooth constrained model cannot reproduce every noisy value. If the fit reaches the 100-iteration 'mlsq' limit, its coefficients may not yet be optimal.

Fit without density constraints

Omitting 'density' selects unconstrained LSQR. The kernel grid still smooths the data, so the result is not literally “undenoised.” It simply has more freedom to reduce the sample residual, including by using negative weights or changing the mean.

SO3FFree = interp(ori, val)
SO3FFree
SO3FunRBF (1 → y↑→x)
  multimodal components
    kernel: de la Vallee Poussin, halfwidth 5°
    center: 119088 orientations
    weight: 0.9613
    Bunge Euler angles in degree
    phi1  Phi  phi2     weight
      90  2.5    90   2.16e-05
      90  2.5    95   1.08e-05
      90  2.5   100  -3.34e-06
      90  2.5   105  -1.03e-06
      90  2.5   110   1.24e-05
       ⋮    ⋮     ⋮          ⋮
      30  178   125  -9.61e-07
      30  178   130   9.85e-06
      30  178   135    8.8e-06
      30  178   140   9.21e-06
      30  178   145   1.15e-05

minimumFreeValue = min(SO3FFree)[0]
minimumFreeValue
-7.2995

meanFreeValue = mean(SO3FFree)
meanFreeValue
0.9613

relativeFreeResidual = norm(SO3FFree.eval(ori) - val) / norm(val)
relativeFreeResidual
6.5598e-04

plot(SO3FFree, 'sigma')
mtexColorbar()

The unconstrained residual is smaller, but the printed minimum and mean show that this fit is not automatically a density. Fine-scale features that only reduce the training residual are possible overfitting, not evidence that the unconstrained model is physically better.

Separate halfwidth from centre spacing

The kernel halfwidth controls how far each centre influences neighbouring orientations. A large halfwidth produces a smooth fit. A halfwidth that is small relative to the data spacing can reproduce noise.

By default, the halfwidth option also sets the resolution of the equispaced centre grid. That coupling changes both the kernel shape and the number of unknown weights. Specify resolution separately when the aim is to isolate the effect of halfwidth.

SO3FNarrow = interp(ori, val, density=True, halfwidth=2 * degree, resolution=5 * degree)
SO3FNarrow
SO3FunRBF (1 → y↑→x)
  multimodal components
    kernel: de la Vallee Poussin, halfwidth 2°
    center: 119086 orientations
    weight: 1
    Bunge Euler angles in degree
    phi1  Phi  phi2    weight
      90  2.5    90  4.69e-06
      90  2.5    95  2.97e-08
      90  2.5   100   2.8e-06
      90  2.5   105  1.47e-07
      90  2.5   110  1.82e-06
       ⋮    ⋮     ⋮         ⋮
      30  178   125  3.15e-06
      30  178   130   2.5e-06
      30  178   135  1.66e-06
      30  178   140  1.31e-06
      30  178   145  2.97e-06

plot(SO3FNarrow, 'sigma')

The 2 degree kernels are narrower than the fixed 5 degree centre spacing. The resulting small-scale peaks track local samples rather than the broad structure. As a rule of thumb, start with a halfwidth at least as large as the resolution of the data, then validate that choice.

SO3FSmooth = interp(ori, val, density=True, halfwidth=10 * degree, resolution=5 * degree)
SO3FSmooth
SO3FunRBF (1 → y↑→x)
  multimodal components
    kernel: de la Vallee Poussin, halfwidth 10°
    center: 69896 orientations
    weight: 1
    Bunge Euler angles in degree
    phi1  Phi  phi2    weight
      90  2.5    95  4.73e-08
      90  2.5   100  1.95e-07
      90  2.5   105  2.21e-07
      90  2.5   110   1.3e-07
      90  2.5   115  4.99e-08
       ⋮    ⋮     ⋮         ⋮
      30  178   125  1.51e-08
      30  178   130  1.42e-08
      30  178   135  1.19e-08
      30  178   140  2.15e-08
      30  178   145  5.17e-08

plot(SO3FSmooth, 'sigma')

The 10 degree kernels overlap much more. The broad peaks remain while narrow fluctuations merge into a smoother surface.

Supply the centre orientations

Use SO3Grid when the centre grid must be controlled directly. Its symmetry should match the sample orientations.

S3G = equispacedSO3Grid(ori.CS, resolution=5 * degree)
S3G
SO3Grid (1 → y↑→x)
  size      : 119088
  resolution: 5°

SO3FGrid = interp(ori, val, density=True, SO3Grid=S3G)
SO3FGrid
SO3FunRBF (1 → y↑→x)
  multimodal components
    kernel: de la Vallee Poussin, halfwidth 5°
    center: 73610 orientations
    weight: 1
    Bunge Euler angles in degree
    phi1  Phi  phi2    weight
      90  2.5    90  2.03e-07
      90  2.5    95  3.98e-07
      90  2.5   100  9.05e-07
      90  2.5   105  6.66e-07
      90  2.5   110  8.11e-07
       ⋮    ⋮     ⋮         ⋮
      30  178   125   2.5e-08
      30  178   130  2.48e-07
      30  178   135  1.76e-07
      30  178   140  1.96e-07
      30  178   145  5.11e-07

plot(SO3FGrid, 'sigma')

The fitted function reports the supplied 5 degree centre grid. The plot resembles the default density fit because that fit makes the same grid choice; the explicit form is useful when several fits must share centres.

Measure a halfwidth sweep

Keep the centre resolution at 5 degrees while changing only halfwidth. The error is measured against the noise-free values in this simulation, not against the noisy training values.

hw = np.array([20, 15, 12.5, 10, 7.5, 5, 2.5])
err = np.zeros(len(hw))
for k in range(len(hw)):
  SO3Fhw = interp(ori, val, density=True, halfwidth=hw[k] * degree, resolution=5 * degree)
  err[k] = norm(SO3Fhw.eval(ori) - S['values']) / norm(S['values'])

plt.figure(figsize=(6, 3.5))
plt.plot(hw, err, 'o--')
plt.gca().invert_xaxis()
plt.xlabel('halfwidth [deg]')
plt.ylabel('relative recovery error')

Read the curve from broad kernels on the left towards narrow kernels on the right. The minimum balances smoothing bias against sensitivity to noise. A sweep that also changed the centre-grid resolution would mix two effects; this one changes the kernel alone. If the fits reach the iteration cap, the result is the best of the capped fits rather than a claim that every solver reached its optimum.

bestIndex = int(np.argmin(err))
bestHalfwidth = hw[bestIndex]
bestHalfwidth
7.5000

bestRecoveryError = err[bestIndex]
bestRecoveryError
0.0533

SO3FBest = interp(ori, val, density=True, halfwidth=bestHalfwidth * degree, resolution=5 * degree)
plot(SO3FBest, 'sigma')

The selected fit retains the broad regions while suppressing much of the added noise. A small training residual alone could not select it; the known noise-free target makes this a validation experiment. With real data, use held-out samples or independent physical knowledge instead.

If too few observations constrain too many centre weights, the system is underdetermined. A small halfwidth does not supply the missing information; use fewer centres, a stronger constraint, or additional smoothness assumptions.

Choose another kernel

halfwidth constructs a SO3DeLaValleePoussinKernel. Pass kernel to use another rotational kernel function.

psi = SO3AbelPoissonKernel(halfwidth=5 * degree)
psi
SO3AbelPoissonKernel
  bandwidth: 48
  halfwidth: 5°

SO3FAbel = interp(ori, val, density=True, kernel=psi, resolution=5 * degree)
plot(SO3FAbel, 'sigma')

The Abel-Poisson fit places its peaks in the same broad regions, but its tails and peak shapes differ from the de la Vallee Poussin result. Kernel family and halfwidth are separate modelling choices.

Exact interpolation

If the values are noise-free, the input orientations themselves can be used as kernel centres with exact=True. For distinct nodes and a positive-definite kernel, the resulting kernel matrix is symmetric and positive definite, so the linear system has a unique exact solution.

That matrix is generally dense. Construction, solution, and later evaluation can therefore become prohibitively expensive for the complete data set. The example uses a reproducible subset to make the cost visible without attempting the original all-node dense system.

exactNodes = ori[::20]
exactValues = S['values'][::20]
numberOfExactNodes = exactNodes.size
numberOfExactNodes
522

t = time.perf_counter()
SO3FExact = SO3FunRBF.interpolate(exactNodes, exactValues, exact=True, halfwidth=7.5 * degree)
exactFitTime = time.perf_counter() - t
plot(SO3FExact, 'sigma')

The exact-centre fit follows the noise-free subset closely. Every value moves the weight of the kernel at its own node, so noise in the values would go straight into the fit, which is why exact interpolation is unsuitable for noisy data. Because these centres are not a structured grid, later evaluations are slower than for a grid-centred RBF model.

The iterative solver stops at a requested tolerance or iteration limit, so its computed residual need not reach machine precision. Exact centres make an exact solution available; they do not force the iterative solver to reach it.

relativeExactResidual = norm(SO3FExact.eval(exactNodes) - exactValues) / norm(exactValues)
relativeExactResidual
0.0144

minimumExactValue = min(SO3FExact)[0]
minimumExactValue
0.0023

The printed residual checks how closely this run solved the system. Exact interpolation of nonnegative values takes the positive solver here as well, so the minimum stays positive; with values of both signs the fit is unconstrained and may become negative.

Choose a least-squares solver

The harmonic page introduces LSQR stopping conditions. RBF interpolation uses tol and maxIter in the same spirit, but its defaults depend on the solver: unconstrained 'lsqr' uses at most 30 iterations, whereas density-constrained 'mlsq' uses at most 100. Both default to tol=1e-3.

The two solvers serve different constraints:

  • 'lsqr' is the fast default for an unconstrained least-squares fit.
  • 'mlsq' enforces positive normalized weights and is selected by 'density', or by values of one sign at fewer than 10,000 nodes.

With withIterations=True, SO3FunRBF.interpolate also returns the iteration count, not a convergence flag. Compare residuals and fits when the count reaches the selected limit.

f1, iter1 = SO3FunRBF.interpolate(ori, val, withIterations=True)
residualNorm1 = norm(f1.eval(ori) - val)
print(f'default: iterations {iter1}, residual norm {residualNorm1:.6g}')

f2, iter2 = SO3FunRBF.interpolate(ori, val, tol=1e-15, maxIter=100, withIterations=True)
residualNorm2 = norm(f2.eval(ori) - val)
print(f'tight tol: iterations {iter2}, residual norm {residualNorm2:.6g}')
default: iterations 8, residual norm 0.589965
tight tol: iterations 41, residual norm 0.374288

If the second run reaches 100 iterations, it has stopped at maxIter rather than satisfying the very small tolerance. Increase the limit only when the additional accuracy matters to validation or interpretation.

The maths behind RBF approximation

An RBF model places one rotational kernel \(\Psi\) at each centre \(R_n\):

\[f(x) = \sum_{n=1}^N c_n\, \Psi\!\left(\cos\frac{\omega(x,R_n)}{2}\right).\]

Here \(\omega(x,R_n)\) is the rotation angle between the evaluation orientation and the centre. The coefficients \(c_n\) are the unknown weights. With sample pairs \((x_m,v_m)\), unconstrained fitting solves

\[\min_c \lVert Kc-v\rVert_2^2, \qquad K_{mn}=\Psi\!\left(\cos\frac{\omega(x_m,R_n)}{2}\right).\]

The kernel matrix is sparse in the usual grid-centred approximation because MTEX neglects interactions outside a halfwidth-dependent angular neighbourhood. The exact flag evaluates every interaction, which is why that matrix loses the computational advantage.

For a density fit, modified least squares additionally requires nonnegative coefficients with a prescribed sum. MTEX then normalizes the resulting RBF function to mean 1. These constraints explain why a density fit can have a larger sample residual than unconstrained LSQR.

References

Next

Continue with Approximation and Quadrature to replace scattered observations by values on a quadrature grid and compute a harmonic representation of a known orientation-dependent function.

Technical Details

MATLAB’s second output of SO3FunRBF.interpolate is withIterations=True here. MATLAB also offers the solvers 'mlrl', 'lsqnonneg', 'lsqlin' and 'nnls'; the port has 'lsqr' and 'mlsq'. The noise is drawn by NumPy. MATLAB evaluates a sum of kernels through the sparse matrix of the kernel at every point; the port evaluates one of more than two million kernel terms through its harmonic form at the bandwidth of the kernel, which reproduces the sum to about \(4\mathbin{\cdot}10^{-4}\). The residuals at the samples cannot fall below that: the tight LSQR run converges in 41 iterations as MATLAB’s does, which reports a residual norm of \(7.9\mathbin{\cdot}10^{-13}\), while the port’s evaluation gives 0.37. MATLAB’s density fits keep 71802, 119075 and 69937 centres where the port’s keep 73610, 119086 and 69896.

Citing this page. This page is part of the documentation of MTEX, a free and open source MATLAB toolbox for analyzing and modeling crystallographic textures. It was written by The MTEX Developers and is published at https://mtex-toolbox.github.io/RBFApproximationTheory_py.html. If you use MTEX, or reuse text or figures from this page, in your research, please cite

F. Bachmann, R. Hielscher, H. Schaeben: Texture Analysis with MTEX - Free and Open Source Software Toolbox, Solid State Phenomena 160 (2010), 63-68. 10.4028/www.scientific.net/SSP.160.63

BibTeX
@article{bachmann2010mtex,
  author  = {F. Bachmann and R. Hielscher and H. Schaeben},
  title   = {Texture Analysis with MTEX - Free and Open Source Software Toolbox},
  journal = {Solid State Phenomena},
  volume  = {160},
  pages   = {63-68},
  year    = {2010},
  doi     = {10.4028/www.scientific.net/SSP.160.63},
  url     = {https://doi.org/10.4028/www.scientific.net/SSP.160.63}
}

Other papers describing specific MTEX methods are listed under Publications — please cite the one that best fits your application. The MTEX source code is licensed under the GNU General Public License v2.0; the text and figures of this documentation are licensed under CC BY 4.0, which permits reuse — including by automated systems — provided The MTEX Developers and this page are credited.