CPO representation

CPOs are represented by their distributions of crystallographic axes in orientation space (\(S^2\)), neglecting grain sizes/mass and other topological information.

Supported grain symmetry groups are:

Grain symmetry CPO components Definition
Transversely isotropic \(n(\theta,\phi)\) Distribution of slip-plane normals
Orthotropic \(n(\theta,\phi),\,b(\theta,\phi)\) Distribution of slip-plane normals and slip directions

Thus, depending on which crystallographic slip system is preferentially activated, \(n(\theta,\phi)\) and \(b(\theta,\phi)\) may refer to the distributions of different crystallographic axes.

Glacier ice

Since ice grains are approximately transversely isotropic, tracking \(n(\theta,\phi)\) (the \(c\)-axis distribution) is sufficient for representing the CPO.

Polycrystalline ice
Ensemble of slip elements

Olivine

For orthotropic grains such as olivine, both \(n(\theta,\phi)\) and \(b(\theta,\phi)\) distributions must be tracked to represent the CPO.

Polycrystalline olivine
Ensemble of slip elements

Note that \(n(\theta,\phi)\) and \(b(\theta,\phi)\) represent the distributions of particular crystallographic axes (\({\bf m}'_i\)) depending on fabric type (A—E type).

ODF

The orientation distribution function (ODF) of a given slip-system axis (crystallographic axis) \(f\in \lbrace n,b\rbrace\) is defined as the normalized distribution

\[ \mathrm{ODF} = \frac{f(\theta,\phi)}{N} \quad\text{where}\quad N=\int_{S^2} f(\theta,\phi) \,\mathrm{d}\Omega . \]

Normalization

\(n(\theta,\phi)\) may be understood either as the number density of grains with a given slip-plane normal orientation, or as the mass density fraction (Faria, 2006; Richards et al., 2021) of grains with a given slip-plane normal orientation; \(\varrho^*(\theta,\phi)\) in literature. The same goes for \(b(\theta,\phi)\).

From specfab's point-of-view, the difference is a matter of normalization: since the models of CPO evolution (lattice rotation, DDRX, CDRX) conserve the normalization, the two views are effectively the same, not least because CPO-derived quantities depend on the normalized distributions (which are identical). The mass-density-fraction interpretation rests, however, on stronger physical grounds as mass is conserved but grain numbers are not.

Harmonic expansion

The distributions \(n(\theta,\phi)\) and \(b(\theta,\phi)\) are represented as spherical harmonic expansion series:

\[ n(\theta,\phi)=\sum_{l=0}^{L}\sum_{m=-l}^{l}n_{l}^{m}Y_{l}^{m}(\theta,\phi) \quad\text{(distribution of slip-plane normals)}, \]
\[ b(\theta,\phi)=\sum_{l=0}^{L}\sum_{m=-l}^{l}b_{l}^{m}Y_{l}^{m}(\theta,\phi) \quad\text{(distribution of slip directions)}. \]

The CPO state is thus described by the state vectors of complex-valued expansion coefficients

\[ {\bf s}_n = [n_0^0,n_2^{-2},n_2^{-1},n_2^{0},n_2^{1},n_2^{2},n_4^{-4},\cdots,n_4^{4},\cdots,n_L^{-L},\cdots,n_L^{L}] \quad\text{($n$ state vector)}, \]
\[ {\bf s}_b = [b_0^0,b_2^{-2},b_2^{-1},b_2^{0},b_2^{1},b_2^{2},b_4^{-4},\cdots,b_4^{4},\cdots,b_L^{-L},\cdots,b_L^{L}] \quad\text{($b$ state vector)}, \]

where the magnitude and complex phase of the coefficients determine the size and rotation of the contribution from the associated harmonic mode.

Reduced form

Not all expansion coefficients are independent for real-valued expansion series, but must fulfill (likewise for \(b\))

\[ n_l^{-m}=(-1)^m(n_l^m)^* . \]

This can be taken advantage of for large problems where many (e.g. gridded) CPOs must be stored in memory, thereby effectively reducing the size of the problem. The vector of reduced expansion coefficients is defined as

\[ \tilde{{\bf s}}= [n_0^0,n_2^{0},n_2^{1},n_2^{2},n_4^{0},\cdots,n_4^{4},\cdots,n_L^{0},\cdots,n_L^{L}] \quad\text{(reduced state vector)}. \]

Converting between full and reduced forms is done as follows:

import numpy as np
from specfabpy import specfabpy as sf
lm, nlm_len = sf.init(2) # L=2 truncation is sufficient in this case

### Construct an arbitrary fabric
a2 = np.diag([0.1,0.2,0.7]) # arbitrary second-order structure tensor
nlm = np.zeros((nlm_len), dtype=np.complex64) # array of expansion coefficients
nlm[:sf.L2len] = sf.a2_to_nlm(a2) # determine l<=2 expansion coefficients of ODF
print('original:', nlm)

### Get reduced form of coefficient array, rnlm
rnlm_len = sf.get_rnlm_len() 
rnlm = np.zeros((rnlm_len), dtype=np.complex64) # array of reduced expansion coefficients
rnlm[:] = sf.nlm_to_rnlm(nlm, rnlm_len) # reduced form
print('reduced:', rnlm)

### Recover full form (nlm) from reduced form (rnlm)
nlm[:] = sf.rnlm_to_nlm(rnlm, nlm_len)
print('recovered:', nlm)