# Microstructure Bases¶

## PrimitiveBasis¶

class pymks.bases.PrimitiveBasis(n_states=2, domain=None)

Discretize the microstructure function into n_states local states such that:

$\frac{1}{\Delta x} \int_{H} \int_{s} \Lambda(h - l) m(h, x) dx dh = m[l, s]$

where $$\Lambda$$ is the primitive basis (also called hat basis) function

$\Lambda (h - l) = max \Bigg (1-\Bigg |\frac{h(L - 1)}{H} - \frac{Hl}{L-1} \Bigg|, 0\Bigg)$

A microstructure function discretized with this basis is subject to the following constraint

$\sum_{l=0}^L m[l, s] = 1$

which is equivalent of saying that every location is filled with some configuration of local states.

Here is an example with 3 discrete local states in a microstructure.

>>> X = np.array([[[1, 1, 0],
...                [1, 0 ,2],
...                [0, 1, 0]]])
>>> assert(X.shape == (1, 3, 3))


The when a microstructure is discretized, the different local states are mapped into local state space, which results in an array of shape (n_samples, n_x, n_y, n_states), where n_states=3 in this case. For example, if a cell has a label of 2, its local state will be [0, 0, 1]. The local state can only have values of 0 or 1.

>>> prim_basis = PrimitiveBasis(n_states=3)
>>> X_prim = np.array([[[[0, 1, 0],
...                      [0, 1, 0],
...                      [1, 0, 0]],
...                     [[0, 1, 0],
...                      [1, 0, 0],
...                      [0, 0, 1]],
...                     [[1, 0, 0],
...                      [0, 1, 0],
...                      [1, 0, 0]]]])
>>> assert(np.allclose(X_prim, prim_basis.discretize(X)))


Check that the basis works when all the states are present in the microstructure.

>>> prim_basis = PrimitiveBasis(n_states=3)
>>> X = np.array([1, 1, 0])
>>> X_prim = np.array([[0, 1, 0],
...                    [0, 1, 0],
...                    [1, 0, 0]])
>>> assert(np.allclose(X_prim, prim_basis.discretize(X)))


In previous two microstructures had values that fell on the peak of the primitive (or hat) basis functions. If a local state value falls between two peaks of the primitive basis functions the value will be shared by both basis functions. To ensure that all local states fall between the peaks of two basis functions, we need to specify the local state domain. For example, if a cell has a value of 0.4, and the basis has n_states=2 and the domain=[0, 1] then the local state is (0.6, 0.4) (the local state must sum to 1).

Here are a few examples where the local states fall between the picks of local states. The first specifies the local state space domain between [0, 1].

>>> n_states = 10
>>> np.random.seed(4)
>>> X = np.random.random((2, 5, 3, 2))
>>> X_ = PrimitiveBasis(n_states, [0, 1]).discretize(X)
>>> H = np.linspace(0, 1, n_states)
>>> Xtest = np.sum(X_ * H[None,None,None,:], axis=-1)
>>> assert np.allclose(X, Xtest)


Here is an example where the local state space domain is between [-1, 1].

>>> n_states = 3
>>> X = np.array([-1, 0, 1, 0.5])
>>> X_test = [[1, 0, 0], [0, 1, 0], [0, 0, 1], [0, 0.5, 0.5]]
>>> X_ = PrimitiveBasis(n_states, [-1, 1]).discretize(X)
>>> assert np.allclose(X_, X_test)


If the local state values in the microstructure are outside of the domain they can no longer be represented by two primitive basis functions and violates constraint above.

>>> n_states = 2
>>> X = np.array([-1, 1])
>>> prim_basis = PrimitiveBasis(n_states, domain=[0, 1])
>>> prim_basis.discretize(X)
Traceback (most recent call last):
...
RuntimeError: X must be within the specified domain


Instantiate a Basis

Parameters: n_states (int, list) – The number of local states, or an array of local states to be used. domain (list, optional) – indicate the range of expected values for the microstructure, default is [0, n_states - 1].
discretize(X)

Discretize X.

Parameters: X (ND array) – The microstructure, an (n_samples, n_x, ...) shaped array where n_samples is the number of samples and n_x is thes patial discretization. Float valued field of local states between 0 and 1.

## LegendreBasis¶

class pymks.bases.LegendreBasis(n_states=2, domain=None)

Discretize a continuous field into deg local states using a Legendre polynomial basis such that,

$\frac{1}{\Delta x} \int_s m(h, x) dx = \sum_0^{L-1} m[l, s] P_l(h)$

where the $$P_l$$ are Legendre polynomials and the local state space $$H$$ is mapped into the orthogonal domain of the Legendre polynomials

$-1 \le H \le 1$

The mapping of $$H$$ into the domain is done automatically in PyMKS by using the domain key work argument.

>>> n_states = 3
>>> X = np.array([[0.25, 0.1],
...               [0.5, 0.25]])
>>> def P(x):
...    x = 4 * x - 1
...    polys = np.array((np.ones_like(x), x, (3.*x**2 - 1.) / 2.))
...    tmp = (2. * np.arange(3)[:, None, None] + 1.) / 2. * polys
...    return np.rollaxis(tmp, 0, 3)
>>> basis = LegendreBasis(n_states, [0., 0.5])
>>> assert(np.allclose(basis.discretize(X), P(X)))


If the microstructure local state values fall outside of the specified domain they will no longer be mapped into the orthogonal domain of the legendre polynomials.

>>> n_states = 2
>>> X = np.array([-1, 1])
>>> leg_basis = LegendreBasis(n_states, domain=[0, 1])
>>> leg_basis.discretize(X)
Traceback (most recent call last):
...
RuntimeError: X must be within the specified domain


Instantiate a Basis

Parameters: n_states (int, list) – The number of local states, or an array of local states to be used. domain (list, optional) – indicate the range of expected values for the microstructure, default is [0, n_states - 1].
discretize(X)

Discretize X.

Parameters: X (ND array) – The microstructure, an (n_samples, n_x, ...) shaped array where n_samples is the number of samples and n_x is the spatial discretization. Float valued field of of Legendre polynomial coefficients.
>>> X = np.array([[-1, 1],
...               [0, -1]])
>>> leg_basis = LegendreBasis(3, [-1, 1])
>>> def p(x):
...    polys = np.array((np.ones_like(x), x, (3.*x**2 - 1.) / 2.))
...    tmp = (2. * np.arange(3)[:, None, None] + 1.) / 2. * polys
...    return np.rollaxis(tmp, 0, 3)
>>> assert(np.allclose(leg_basis.discretize(X), p(X)))


## FourierBasis¶

class pymks.bases.FourierBasis(n_states=5, domain=None)

Discretize a continuous field into deg local states using complex exponentials such that,

$\frac{1}{\Delta x} \int_s m(h, x) dx = \sum_{- L / 2}^{L / 2} m[l, s] exp(l*h*I)$

and the local state space $$H$$ is mapped into the orthogonal domain

$0 \le H \le 2 \pi$

The mapping of $$H$$ into the domain is done automatically in PyMKS by using the domain key work argument.

>>> n_states = 3
>>> X = np.array([[0., np.pi / 3],
...               [2 * np.pi / 3, 2 * np.pi]])
>>> X_result = np.array(([[[1, 1, 1],
...                        [1, np.exp(np.pi / 3 * 1j),
...                         np.exp(- np.pi / 3 * 1j)]],
...                       [[1, np.exp(2 *np.pi / 3 * 1j),
...                         np.exp(- 2 * np.pi / 3 * 1j)],
...                         [1, 1, 1]]]))
>>> basis = FourierBasis(n_states, [0., 2 * np.pi])
>>> assert(np.allclose(basis.discretize(X), X_result))


If the microstructure local state values fall outside of the period of the specified domain, the values will be mapped back into the domain.

>>> n_states = 2
>>> X = np.array([[0, 1.5]])
>>> four_basis = FourierBasis(n_states, domain=[0, 1])
>>> X_result = np.array([[[1, 1],
...                       [1, -1]]])
>>> assert np.allclose(X_result, four_basis.discretize(X))


Instantiate a FourierBasis

Parameters: n_states (int, list) – The number of local states, or list of local states to be used. domain (list, optional) – indicate the range of expected values for the microstructure, default is [0, 2pi].
discretize(X)

Discretize X.

Parameters: X (ND array) – The microstructure, an (n_samples, n_x, ...) shaped array where n_samples is the number of samples and n_x is the spatial discretization. Float valued field of Fourier series coefficients in the following order 0, 1, ,-1, 2, -2, with shaped (n_samples, n_x, ..., n_states).
>>> X = np.array([[-1, -1./2, 0.,  1./2, 1]])
>>> f_basis = FourierBasis(4, [-1, 1.])
>>> X_result = np.array([[[1, 1, 1, 1],
...                       [1, 1j, -1j, -1],
...                       [1, -1, -1, 1],
...                       [1, -1j, 1j, -1],
...                       [1, 1, 1, 1]]])
>>> assert(np.allclose(X_result, f_basis.discretize(X)))


## GSHBasis¶

class pymks.bases.GSHBasis(n_states=15, domain=None)

Discretize a continuous field made up three Euler angles (in radians) used to represent crystal orientation into continuous local states using the Generalized Spherical Harmonic (GSH) basis. This basis uses the following equation to discretize the orientation field.

$\frac{1}{\Delta x} \int_s m(g, x) dx = \sum_{l, m, n} m[l, \tilde{m}, n, s] T_l^{\tilde{m}n}(g)$

where the $$T_l^{\tilde{m}n}$$ are GSH basis functions and the local state space $$H$$ is mapped into the orthogonal, periodic domain of the GSH functions

The mapping of $$H$$ into some desired periodic domain is done automatically in PyMKS by using the domain key work argument to select the desired crystal symmetry.

>>> X = np.array([[0.1, 0.2, 0.3],
...               [6.5, 2.3, 3.4]])
>>> gsh_basis = GSHBasis(n_states = [3], domain='hexagonal')
>>> def test_gsh(x):
...     phi = x[:, 1]
...     t915 = np.cos(phi)
...     return 0.15e2 / 0.2e1 * t915 ** 2 - 0.5e1 / 0.2e1

>>> assert(np.allclose(np.squeeze(gsh_basis.discretize(X)), test_gsh(X)))


If you select an invalid crystal symmetry PyMKS will give an error

>>> gsh_basis = GSHBasis(n_states=[3], domain='squishy')
...
Traceback (most recent call last):
...
RuntimeError: invalid crystal symmetry

>>> gsh_basis = GSHBasis(n_states=[3], domain='hex')
Traceback (most recent call last):
...
RuntimeError: invalid crystal symmetry


Instantiate a Basis

Parameters: n_states (int, array) – An array of local states to be used. states requested. If an integer is provided, all local states up to that number will be used. domain (list, optional) – indicate the desired crystal symmetry for the GSH. Valid choices for symmetry are “hexagonal”, “cubic” or “triclinic” if no symmetry is desired (not specifying any symmetry has the same effect)
check(X)

Warns the user if Euler angles apear to be defined in degrees instead of radians

discretize(X)

Discretize X.

Parameters: X (ND array) – The microstructure, an (n_samples, n_x, ..., 3) shaped array where n_samples is the number of samples, n_x is the spatial discretization and the last dimension contains the Bunge Euler angles in radians. Float valued field of of Generalized Spherical Harmonics coefficients.
>>> X = np.array([[0.1, 0.2, 0.3],
...               [6.5, 2.3, 3.4]])
>>> gsh_basis = GSHBasis(n_states = [1])
>>> def q(x):
...     phi1 = x[:, 0]
...     phi = x[:, 1]
...     phi2 = x[:, 2]
...     x_GSH = ((0.3e1 / 0.2e1) * (0.1e1 + np.cos(phi)) *
...              np.exp((-1*1j) * (phi1 + phi2)))
...     return x_GSH
>>> assert(np.allclose(np.squeeze(gsh_basis.discretize(X)), q(X)))