download as script · edit page

Density Estimation showed that kernel width decides which features survive smoothing. A width that is too small leaves oscillations and sharp peaks at individual observations. A width that is too large erases genuine features of the unknown density.

There is no universally optimal halfwidth. The useful value depends on both the number of independent observations and the smoothness of the unknown density. This page compares the selection methods provided by calcKernel and shows how to check their choices against a known model.

import matplotlib.pyplot as plt
import numpy as np

from mtex import *

plottingConvention.default('y↑→x')
rng = np.random.default_rng(1)

Build a model with several kinds of structure

The original example described the following crystal symmetry as cubic. Point group 321 is trigonal, so the code and its resulting fundamental region must be read as a trigonal example.

cs = crystalFrame('321')

Combine a uniform background, one localized component, and one fibre. Their coefficients sum to one, so modelODF remains normalized.

modelODF = (0.25 * uniformODF(cs)
            + 0.25 * unimodalODF(orientation.byEuler(np.array([35, 45, 0]) * degree, cs))
            + 0.5 * fibreODF(Miller(7, -1, 10, cs), vector3d(7, -5, 11), halfwidth=10 * degree))

plot(modelODF, 'sigma', sections=6)
mtexColorbar(title='mrd')

The sections contain a localized maximum and an elongated fibre feature above a non-zero background. A useful selected kernel should retain both shapes without turning finite-sample noise into new maxima.

Select a kernel from sampled orientations

Draw 10000 orientations from the model. The random generator seeded at the top of the page makes the sample and every reported result reproducible.

ori = discreteSample(modelODF, 10000, rng=rng)
sampleCount = len(ori)
sampleCount
10000

MATLAB’s calcKernel uses Kullback–Leibler cross-validation (KLCV) when no method is named; the port’s default is 'UCV', described below, so KLCV is named here. It returns a de la Vallée Poussin kernel rather than an ODF.

psi = calcKernel(ori, method='KLCV', rng=rng)
psi
SO3DeLaValleePoussinKernel
  bandwidth: 35
  halfwidth: 7.14°
selectedHalfwidth = psi.halfwidth() / degree
selectedHalfwidth
7.1380

Pass that kernel to calcDensity. The same psi can therefore be reused for another sample that represents the same population and sampling process.

reconstructedODF = calcDensity(ori, kernel=psi)
reconstructionError = calcError(reconstructedODF, modelODF, resolution=5 * degree)
reconstructionError
0.1390
plot(reconstructedODF, 'sigma', sections=6)
mtexColorbar(title='mrd')

Compare these sections with the model above. The localized maximum and the fibre remain in the same places, while their contours are not identical because only a finite random sample was reconstructed.

reconstructionError compares density values over orientation space. It is not an angular error and must not be labelled in degrees.

Select from the error of the density

Two further methods judge a candidate kernel by the error of the density it produces. 'UCV' (unbiased cross-validation), the port’s default, estimates the integrated squared error of every candidate from the harmonic coefficients of the sample and takes the halfwidth where it is least. 'conservative' replaces the texture’s harmonic energies by lower confidence bounds and ignores the harmonic degrees it cannot resolve; the halfwidth it returns is then at least as wide as the one of least error. Both are bounded below by a noise floor: no halfwidth so narrow that the sampling noise could raise bumps above a fifth of the estimate’s maximum, with noiseFloor=False to switch it off. Both cost about one harmonic transform of the sample, under a second for a million orientations. They use their own random generator here, which splits the sample into the parts it compares.

rngSelect = np.random.default_rng(2)
for m in ['UCV', 'conservative']:
  psiM = calcKernel(ori, method=m, rng=rngSelect)
  errorM = calcError(calcDensity(ori, kernel=psiM), modelODF, resolution=5 * degree)
  print(f'{m:12s} halfwidth {psiM.halfwidth() / degree:.3g} degree, error {errorM:.3g}')
UCV          halfwidth 4.07 degree, error 0.131
conservative halfwidth 4.07 degree, error 0.131

Both ask for 4.1 degrees against the 7.1 degrees of KLCV above, and their error, 0.131, lies below KLCV’s 0.139. With 10000 orientations the harmonic energies of this texture are resolved well enough that the conservative bound and the unbiased estimate agree.

What the methods use

The method names are sometimes described as flags in older material. The current interface passes each name as the value of method.

method information used practical reading
'KLCV' leave-one-out likelihood on candidate kernels MATLAB’s default; data-adaptive
'RuleOfThumb' nearest-neighbour resolution of the sample quick scale estimate
'magicRule' sample size and crystal/specimen symmetry conservative asymptotic rule
'UCV' unbiased estimate of the integrated squared error, with a noise floor the port’s default; aims at the best halfwidth
'conservative' lower bounds of the harmonic energies, a bound on the noise errs towards smoothing

Older descriptions say that 'RuleOfThumb' uses the variance of the orientations to estimate smoothness. The current implementation instead uses a quantile of nearest-neighbour angular distances, with a lower halfwidth limit of 2 degrees.

KLCV evaluates how well kernels centred on the other orientations predict each omitted orientation. The samplingSize option limits how many observations contribute to that score. It reduces computation, but it does not create an independent validation dataset.

Compare the methods on the same samples

The full scaling experiment associated with this page uses 10, 100, …, 1000000 orientations. That run is appropriate for an attended benchmark, not an executable documentation page. Uncomment the alternative sampleSize line when that full study is intended.

sampleSize = [30, 100, 300, 1000]
# sampleSize = 10 ** np.arange(1, 7)   # full study: 10 through 1000000

method = ['KLCV', 'RuleOfThumb', 'magicRule', 'UCV', 'conservative']
halfwidthInDegree = np.zeros((len(sampleSize), len(method)))
estimationError = np.zeros((len(sampleSize), len(method)))

for i, n in enumerate(sampleSize):
  oriTrial = discreteSample(modelODF, n, rng=rng)
  for j, m in enumerate(method):
    psiTrial = calcKernel(oriTrial, method=m, samplingSize=1000, rng=rngSelect if m in ('UCV', 'conservative') else rng)
    trialODF = calcDensity(oriTrial, kernel=psiTrial)
    halfwidthInDegree[i, j] = psiTrial.halfwidth() / degree
    estimationError[i, j] = calcError(trialODF, modelODF, resolution=7.5 * degree)

rowName = [f'N_{n}' for n in sampleSize]
print('halfwidthTable')
print(f"{'':8}" + ''.join(f'{m:>13}' for m in method))
for r, row in zip(rowName, halfwidthInDegree):
  print(f'{r:8}' + ''.join(f'{v:13.3f}' for v in row))
print('\nerrorTable')
print(f"{'':8}" + ''.join(f'{m:>13}' for m in method))
for r, row in zip(rowName, estimationError):
  print(f'{r:8}' + ''.join(f'{v:13.5f}' for v in row))
halfwidthTable
                 KLCV  RuleOfThumb    magicRule          UCV conservative
N_30           23.811       55.110       26.117       38.020       40.000
N_100          11.906       33.501       22.018       12.442       16.037
N_300          11.906       24.646       18.836       11.240       11.240
N_1000          9.449       16.983       15.870        6.112        6.112

errorTable
                 KLCV  RuleOfThumb    magicRule          UCV conservative
N_30          0.44184      0.54391      0.45433      0.50625      0.51213
N_100         0.29798      0.46635      0.39014      0.30117      0.33489
N_300         0.28499      0.42540      0.36964      0.27891      0.27891
N_1000        0.21704      0.33845      0.32227      0.22162      0.22162

Read each row as one random sample tested five ways. This controls the sample-to-sample variation when comparing methods within a row. One draw at each size is a demonstration, not evidence that one method is always best.

With 30 orientations the two new rules return nearly their widest candidates, 38 and 40 degrees: the noise floor finds that nothing narrower would stand out of the sampling noise, and their error is the price of that caution. From 300 orientations on they agree, and at 1000 orientations they ask for 6.1 degrees where KLCV asks for 9.4, at nearly the same error.

plt.figure()
plt.loglog(sampleSize, estimationError, 'o-', linewidth=2)
plt.legend(method, loc='best')
plt.xlabel('number of orientations')
plt.ylabel('ODF estimation error')
plt.grid(True, which='both')

The curves compare density-space error, with smaller values indicating a closer reconstruction. Their differences show that kernel selection is part of the statistical model rather than a display preference.

Before trusting an automatic halfwidth

Automatic selection assumes that the input orientations represent the intended population. Neighbouring pixels in one EBSD grain are strongly correlated, so treating every pixel as independent can select a kernel that follows intragranular scatter. The orientations of a map therefore carry the mark notIID, and calcKernel refuses them. 'UCV' and 'conservative' accept them with the grain id of every pixel, groups=ebsd.grainId, or the map itself once its grains are computed; they then compare only parts of the sample that share no grain. Otherwise use grain means to select the kernel when grains are the independent sampling units.

Weighting answers a separate question. After selecting a kernel from grain means, an area-based ODF may still use pixels or grain-area weights. ODF Estimation from EBSD Data works through that choice.

Also inspect the selected halfwidth and the reconstructed plots. A value at the edge of the tested candidate range, or peaks supported by only one or two observations, is a reason to test nearby kernels manually.

The maths behind the sample-size rule

For the de la Vallée Poussin kernel, 'magicRule' sets the concentration parameter \(\kappa\) proportional to \(N^{2/7}\). The kernel halfwidth then behaves approximately as

\[\delta \mathrel{\sim} \kappa^{-1/2} \mathrel{\sim} N^{-1/7}.\]

The rule therefore narrows the kernel as the sample grows. It cannot use the unknown density’s feature sizes, which is why it is conservative. Rule-of-thumb and cross-validation methods use the observations to add information about those scales.

The maths behind UCV and the conservative rule

Write the density in harmonics orthonormal for the uniform distribution, \(f = 1 + \sum_{\ell \ge 1} f_\ell\), with the degree energies \(A_\ell = \|f_\ell\|^2\). A radial kernel multiplies degree \(\ell\) by a factor \(b_\ell\) that falls as the halfwidth grows. For \(N\) independent orientations the mean integrated squared error is exactly

\[\mathrm{MISE} = \sum_{\ell \ge 1} (1 - b_\ell)^2 A_\ell + \frac{1}{N} \sum_{\ell \ge 1} b_\ell^2 \left( d_\ell - A_\ell \right),\]

where \(d_\ell\) is the dimension of degree \(\ell\) reduced by the crystal and specimen symmetry. 'UCV' inserts unbiased estimates of \(A_\ell\), obtained from products of disjoint parts of the sample, and minimises over the candidates. 'conservative' inserts lower confidence bounds and sets the unresolved high degrees to zero. Lowering the energies can only move the minimiser to a wider kernel, so the choice is at least the halfwidth of least error whenever the bounds hold. Splitting a map into parts that share no grain keeps the estimates unbiased when pixels of one grain are correlated.

The noise floor looks at the estimate itself. At an orientation where the smoothed density is \(y\), the estimate is a sum of \(N\) independent terms, none larger than \(\psi(0)/N\), with variance \(y\,\|\psi\|^2/N\). Bennett’s inequality bounds its deviation at every one of the independent kernel footprints of orientation space, and the floor is the smallest halfwidth at which that bound stays below a fifth of the estimate’s maximum plus half the local density.

References

Next

Clustering replaces a continuous density by discrete groups of nearby orientations. Use it when group membership is the goal rather than estimating how probability varies through orientation space.

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/OptimalKernel_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.