pydrex.core

PyDRex: Core D-Rex functions and enums.

The function derivatives implements the core D-Rex solver, which computes the crystallographic rotation rate and changes in fractional grain volumes.

Acronyms:

  • CRSS = Critical Resolved Shear Stress, i.e. threshold stress required to initiate slip on a slip system, normalised to the stress required to initiate slip on the softest slip system
  1"""> PyDRex: Core D-Rex functions and enums.
  2
  3The function `derivatives` implements the core D-Rex solver, which computes the
  4crystallographic rotation rate and changes in fractional grain volumes.
  5
  6**Acronyms:**
  7- CRSS = Critical Resolved Shear Stress,
  8    i.e. threshold stress required to initiate slip on a slip system,
  9    normalised to the stress required to initiate slip on the softest slip system
 10
 11"""
 12
 13from dataclasses import asdict, dataclass
 14from enum import IntEnum, unique
 15
 16import numba as nb
 17import numpy as np
 18
 19# NOTE: Do NOT import any pydrex submodules here to avoid cyclical imports.
 20
 21
 22_USE_ORIGINAL_DREX = False  # Switch to use D-Rex 2004 version without corrections.
 23
 24
 25PERMUTATION_SYMBOL = np.array(
 26    [
 27        [[0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, -1.0, 0.0]],
 28        [[0.0, 0.0, -1.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0]],
 29        [[0.0, 1.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
 30    ]
 31)
 32"""Sometimes called the Levi-Civita symbol."""
 33
 34
 35@unique
 36class MineralPhase(IntEnum):
 37    """Supported mineral phases.
 38
 39    Forsterite and fayalite are grouped into “olivine”, because we treat them as
 40    rheologically equivalent.
 41
 42    """
 43
 44    olivine = 0
 45    """(Mg,Fe)₂SiO₄"""
 46    enstatite = 1
 47    """MgSiO₃"""
 48
 49
 50@unique
 51class MineralFabric(IntEnum):
 52    """Supported mineral fabrics.
 53
 54    The following fabric types are supported:
 55    - olivine A-E type fabrics according to e.g.
 56      [Karato et al. (2008)](https://doi.org/10.1146%2Fannurev.earth.36.031207.124120).
 57    - enstatite AB fabric, see
 58      [Bernard et al. (2021)](https://doi.org/10.1016/j.tecto.2021.228954).
 59
 60    """
 61
 62    olivine_A = 0
 63    olivine_B = 1
 64    olivine_C = 2
 65    olivine_D = 3
 66    olivine_E = 4
 67    enstatite_AB = 5
 68
 69
 70@unique
 71class DeformationRegime(IntEnum):
 72    r"""Ordinals to track distinct regimes of dominant deformation mechanisms.
 73
 74    The mechanism of deformation that dominates accommodation of plastic deformation
 75    depends in general both on material properties such as grain size and mineral phase
 76    content as well as on thermodynamic properties such as temperature, pressure and
 77    water fugacity.
 78
 79    The activity of diffusive mechanisms depends more strongly on grain size, whereas
 80    that of dislocation mechanisms depends more strongly on temperature. High
 81    temperatures enable more frequent recovery of dislocation density equilibrium via
 82    e.g. dislocation climb. Dislocation mechanisms are often accompanied by dynamic
 83    recrystallisation, which acts as an additional recovery mechanism.
 84
 85    Diffusive mechanisms are expected, however, to become dominant at depth, as these
 86    mechanisms are primarily facilitated by Si diffusion in olivine, which is
 87    temperature-dependent.
 88
 89    Rheology in the intra-granular dislocation regime was classically described by
 90    separate flow laws depending on temperature; a power-law at high temperature
 91    [$\dot{ε} ∝ σⁿ$] and an exponential-law at low temperature [$\dot{ε} ∝ \exp(σ)$].
 92    More recent work has suggested unified dislocation creep flow laws,
 93    e.g. [Gouriet et al. 2019](http://dx.doi.org/10.1016/j.epsl.2018.10.049),
 94    [Garel et al. 2020](http://dx.doi.org/10.1016/j.epsl.2020.116243) and
 95    [Demouchy et al. 2023](http://dx.doi.org/10.2138/gselements.19.3.151).
 96
 97    .. note:: Although a draft texture evolution behaviour is implemented in the
 98        `frictional_yielding` regime, it is experimental and not yet configurable via
 99        the parameter interface.
100
101    """
102
103    min_viscosity = 0
104    """Arbitrary lower-bound viscosity regime."""
105    matrix_diffusion = 1
106    """Intra-granular Nabarro-Herring creep, i.e. grains diffuse through the matrix."""
107    boundary_diffusion = 2
108    """Inter-granular Coble creep, i.e. grains diffuse along grain boundaries."""
109    sliding_diffusion = 3
110    """Inter-granular diffusion-assisted grain-boundary sliding (diffGBS)."""
111    matrix_dislocation = 4
112    """Intra-granular dislocation creep (glide + climb) and dynamic recrystallisation."""
113    sliding_dislocation = 5
114    """Inter-granular dislocation-assisted grain-boundary sliding (disGBS)."""
115    frictional_yielding = 6
116    """Frictional sliding along micro-fractures (Byerlee's law for yield strength)."""
117    max_viscosity = 7
118    """Arbitrary upper-bound viscosity regime."""
119
120
121@dataclass(frozen=True)
122class DefaultParams:
123    phase_assemblage: tuple = (MineralPhase.olivine,)
124    """Mineral phases present in the aggregate."""
125    phase_fractions: tuple = (1.0,)
126    """Volume fractions of each mineral phase present in the aggregate."""
127    stress_exponent: float = 1.5
128    """The value for $p$ in $ρ ∝ τᵖ$ where $ρ$ is the dislocation density and $τ$ the shear stress.
129
130    Default value taken from [Kaminski et al. 2004](https://doi.org/10.1111/j.1365-246X.2004.02308.x)
131    based on studies referenced therein.
132
133    """
134    deformation_exponent: float = 3.5
135    """The value for $n$ in $τ ∝ |D|^{1/n}$ where $τ$ is the shear stress and D the deformation rate.
136
137    Default value taken from [Kaminski et al. 2004](https://doi.org/10.1111/j.1365-246X.2004.02308.x)
138    based on studies referenced therein.
139
140    """
141    gbm_mobility: int = 125
142    """Dimensionless grain boundary mobility parameter (M*).
143
144    This controls the rate of all dynamic recrystallisation processes in
145    `DeformationRegime.matrix_dislocation`.
146
147    .. note:: This parameter is not easy to constrain. Reasonable values may lie in the
148        range [10, 150]. Comparing outputs for multiple M* values is recommended.
149
150    """
151    gbs_threshold: float = 0.3
152    """Grain boundary sliding threshold.
153
154    In `DeformationRegime.matrix_dislocation` or `DeformationRegime.sliding_dislocation`
155    this controls the smallest size of a grain, relative to its original size,
156    at which it will still deform via dislocation creep.
157    Smaller grains will instead deform via grain boundary sliding,
158    therefore not contributing to any further texture evolution.
159
160    .. note:: Values for this parameter do NOT correspond to a physical grain size
161        threshold. Grains in PyDRex are a surrogate numerical discretisation. There are
162        likely to be feedbacks between this parameter and `number_of_grains`. Values of
163        ~0.3 have been used in numerous studies which employed ~3000 grains.
164
165    """
166    nucleation_efficiency: float = 5.0
167    """Dimensionless nucleation efficiency (λ*).
168
169    This controls the nucleation of subgrains in `DeformationRegime.matrix_dislocation`.
170
171    The default value comes from [Kaminski & Ribe 2001](https://doi.org/10.1016%2Fs0012-821x%2801%2900356-9).
172
173    """
174    number_of_grains: int = 3500
175    """Number of surrogate grains for numerical discretisation of the aggregate.
176
177    This is a numerical discretisation, so generally more grains is better. However,
178    there is a sharp tradeoff in performance, especially for M-index calculation.
179
180    """
181    initial_olivine_fabric: MineralFabric = MineralFabric.olivine_A
182    """Olivine fabric (CRSS distribution) at the beginning of the simulation.
183
184    In general, the fabric should be allowed to change during the simulation,
185    but that is not yet implemented.
186
187    """
188    disl_Peierls_stress: float = 2
189    """Stress barrier in GPa for activation of dislocation motion at low temperatures.
190
191    - 2GPa suggested by [Demouchy et al. 2023](http://dx.doi.org/10.2138/gselements.19.3.151)
192
193    .. note:: Not relevant if the unified dislocation creep flow law is used.
194
195    """
196    # NOTE: For now, we just roll this into the prefactors.
197    # water_fugacity: float = 1.5
198    # """Approximate constant water fugacity of the aggregate (GPa)."""
199    # TODO: Check units of Garel 2020 pre-exp value below.
200    # TODO: Find and add references for the other value.
201    disl_prefactors: tuple = (1e-16, 1e-17)
202    """Prefactors for dislocation creep exponential & power laws (s⁻¹).
203
204    - B = 1.7e-16 s⁻¹ for the exponential law suggested by [Demouchy et al. 2023](http://dx.doi.org/10.2138/gselements.19.3.151)
205    - pre-exponential factor of 4.4e-17 Pa⁻ⁿs⁻¹ used for the exponential law by [Garel et al. 2020](http://dx.doi.org/10.1016/j.epsl.2020.116243)
206
207    """
208    # TODO: Add references, tweak default values if necessary.
209    diff_prefactors: tuple = (1e-10, 1e-10)
210    r"""Prefactors for (matrix and boundary) diffusion creep power laws (s⁻¹).
211
212    Dependence on molar volume and physical grain size are suppressed because these
213    diagnostics are not readily available. The prefactor is roughly equal to
214    $(Vₘ D⁰)/d²$ for $Vₘ$ the molar volume, $D⁰$ the reference diffusion coefficient and
215    $d²$ an average grain size assumed to be constant.
216
217    """
218    disl_lowtemp_switch: float = 0.7
219    """Threshold homologous temperature below which to use the exponential flow law.
220
221    The default value suggested here comes from
222    [Demouchy et al. 2023](http://dx.doi.org/10.2138/gselements.19.3.151).
223
224    .. note:: Not relevant if the unified dislocation creep flow law is used instead.
225
226    """
227    # TODO: Add more references from experimental studies/reviews.
228    disl_activation_energy: float = 460.0
229    """Activation energy for dislocation creep power law (kJ/mol).
230
231    - 443 kJ/mol used by [Gouriet et al. 2019](http://dx.doi.org/10.1016/j.epsl.2018.10.049),
232        but for an exponential law
233    - 540 kJ/mol used by [Garel et al. 2020](http://dx.doi.org/10.1016/j.epsl.2020.116243)
234
235    """
236    # TODO: Add more references, tweak default if necessary.
237    disl_activation_volume: float = 12.0
238    """Activation volume for dislocation creep power law (cm³/mol).
239
240    - 0, 6 and 12 cm³/mol used by [Garel et al. 2020](http://dx.doi.org/10.1016/j.epsl.2020.116243)
241
242    """
243    # TODO: Add more references, justify choice of lower value than Garel 2020.
244    diff_activation_energies: tuple = (430.0, 330)
245    """Activation energies for (matrix and boundary) diffusion creep power laws (kJ/mol).
246
247    - 530 kJ/mol reported for Si self-diffusion in olivine¹
248    - 410 kJ/mol used by [Garel et al. 2020](http://dx.doi.org/10.1016/j.epsl.2020.116243)
249
250    ¹[Dohmen et al. 2002](http://dx.doi.org/10.1029/2002GL015480)
251
252    """
253    # TODO: Add more references, tweak default values if necessary.
254    diff_activation_volumes: tuple = (4.0, 4.0)
255    """Activation volumes for (matrix and boundary) diffusion creep power laws (cm³/mol).
256
257    - 4 kJ/mol used by [Garel et al. 2020](http://dx.doi.org/10.1016/j.epsl.2020.116243)
258
259    """
260    disl_coefficients: tuple = (
261        4.4e8,  # a₀
262        -5.26e4,  # b₀
263        2.11e-2,  # a₁
264        1.74e-4,  # b₁
265        -41.8,  # a₂
266        4.21e-2,  # b₂
267        -1.14e-5,  # c₂
268    )
269    """Coefficients for polynomials used in the alternative dislocation creep flow law.
270
271    The defaults are for the TANH variant of the unified creep law, proposed in
272    [Garel et al. 2020](http://dx.doi.org/10.1016/j.epsl.2020.116243).
273    By contrast, [Gouriet et al. 2019](http://dx.doi.org/10.1016/j.epsl.2018.10.049)
274    used the ERF variant with: [4.4e8, -2.2e4, 3e-2, 1.3e-4, -42, 4.2e-2, -1.1e-5].
275
276    """
277
278    def as_dict(self):
279        """Return mutable copy of default arguments as a dictionary."""
280        return asdict(self)
281
282
283@nb.njit
284def get_crss(phase: MineralPhase, fabric: MineralFabric) -> np.ndarray:
285    """Get Critical Resolved Shear Stress for the mineral `phase` and `fabric`.
286
287    Returns an array of the normalised threshold stresses required to activate slip on
288    each slip system. Olivine slip systems are ordered according to the convention used
289    for `pydrex.minerals.OLIVINE_SLIP_SYSTEMS`.
290
291    """
292    if phase == MineralPhase.olivine:
293        match fabric:
294            case MineralFabric.olivine_A:
295                return np.array([1, 2, 3, np.inf])
296            case MineralFabric.olivine_B:
297                return np.array([3, 2, 1, np.inf])
298            case MineralFabric.olivine_C:
299                return np.array([3, 2, np.inf, 1])
300            case MineralFabric.olivine_D:
301                return np.array([1, 1, 3, np.inf])
302            case MineralFabric.olivine_E:
303                return np.array([3, 1, 2, np.inf])
304            case _:
305                raise ValueError(f"unsupported olivine fabric: {fabric}")
306    elif phase == MineralPhase.enstatite:
307        if fabric == MineralFabric.enstatite_AB:
308            return np.array([np.inf, np.inf, np.inf, 1])
309        raise ValueError(f"unsupported enstatite fabric: {fabric}")
310    raise ValueError(f"phase must be a valid `MineralPhase`, not {phase}")
311
312
313# 12+ args is a lot, but this way we can use numba
314# (only primitives and numpy containers allowed).
315@nb.njit(fastmath=True)
316def derivatives(
317    regime: DeformationRegime,
318    phase: MineralPhase,
319    fabric: MineralFabric,
320    n_grains: int,
321    orientations: np.ndarray,
322    fractions: np.ndarray,
323    strain_rate: np.ndarray,
324    velocity_gradient: np.ndarray,
325    deformation_gradient_spin: np.ndarray,
326    stress_exponent: float,
327    deformation_exponent: float,
328    nucleation_efficiency: float,
329    gbm_mobility: float,
330    volume_fraction: float,
331):
332    """Get derivatives of orientation and volume distribution.
333
334    - `regime` — ordinal number of the local deformation mechanism
335    - `phase` — ordinal number of the mineral phase
336    - `fabric` — ordinal number of the fabric type
337    - `n_grains` — number of "grains" i.e. discrete volume segments
338    - `orientations` — `n_grains`x3x3 orientations (direction cosines)
339    - `fractions` — volume fractions of the grains relative to aggregate volume
340    - `strain_rate` — 3x3 dimensionless macroscopic strain-rate tensor
341    - `velocity_gradient` — 3x3 dimensionless macroscopic velocity gradient
342    - `deformation_gradient_spin` — 3x3 spin tensor defining the rate of rotation of the
343      finite strain ellipse
344    - `stress_exponent` — `p` in `dislocation_density ∝ shear_stress^p`
345    - `deformation_exponent` — `n` in `shear_stress ∝ |deformation_rate|^(1/n)`
346    - `nucleation_efficiency` — parameter controlling grain nucleation
347    - `gmb_mobility` — grain boundary mobility parameter
348    - `volume_fraction` — volume fraction of the mineral phase relative to other phases
349      (multiphase simulations)
350
351    Returns a tuple with the rotation rates and grain volume fraction changes.
352
353    """
354    if regime == DeformationRegime.min_viscosity:
355        # Do absolutely nothing, all derivatives are zero.
356        return (
357            np.repeat(np.eye(3), n_grains).reshape(3, 3, n_grains).transpose(),
358            np.zeros(n_grains),
359        )
360    elif regime == DeformationRegime.matrix_diffusion:
361        # Passive rotation based on macroscopic vorticity for diffusion creep?
362        # vorticity = 0.5 * (velocity_gradient - velocity_gradient.transpose())
363        # Passive rotation based on spin of F for diffusion creep.
364        vorticity = deformation_gradient_spin
365        # Or just don't change at all?
366        # vorticity = np.zeros((3, 3))
367        # This 💃 is because numba doesn't let us use np.tile or even np.array([a] * n).
368        return (
369            np.repeat(vorticity.transpose(), n_grains)
370            .reshape(3, 3, n_grains)
371            .transpose(),
372            np.zeros(n_grains),
373        )
374    elif regime == DeformationRegime.boundary_diffusion:
375        raise ValueError("this deformation mechanism is not yet supported.")
376    elif regime == DeformationRegime.sliding_diffusion:
377        # Miyazaki et al. 2013 wrote controversial Nature article proposing that CPO can
378        # develop in the diffGBS regime. However, Karato 2024 gives some convincing
379        # arguments in the Journal of Geodynamics for why their results are likely
380        # inapplicable to Earth's upper mantle.
381        raise ValueError("this deformation mechanism is not yet supported.")
382    elif regime == DeformationRegime.matrix_dislocation:
383        # Based on subroutine DERIV in original Fortran.
384        strain_energies = np.empty(n_grains)
385        orientations_diff = np.empty((n_grains, 3, 3))
386        for grain_index in range(n_grains):
387            orientation_change, strain_energy = _get_rotation_and_strain(
388                phase,
389                fabric,
390                orientations[grain_index],
391                strain_rate,
392                velocity_gradient,
393                stress_exponent,
394                deformation_exponent,
395                nucleation_efficiency,
396            )
397            orientations_diff[grain_index] = orientation_change
398            strain_energies[grain_index] = strain_energy
399        # Volume average mean strain energy.
400        mean_energy = np.sum(fractions * strain_energies)
401        # Strain energy residual.
402        strain_residuals = mean_energy - strain_energies
403        fractions_diff = volume_fraction * gbm_mobility * fractions * strain_residuals
404        return orientations_diff, fractions_diff
405    elif regime == DeformationRegime.sliding_dislocation:
406        raise ValueError("this deformation mechanism is not yet supported.")
407    elif regime == DeformationRegime.frictional_yielding:
408        # For now, the same as matrix_dislocation, but we smooth the strain energy
409        # distribution and the orientation changes, since some energy is lost to
410        # micro-fracturing. Also increase the GBS threshold, dislocations will tend to
411        # pile up at grain boundaries.
412        # TODO: Maybe modify the stress/deformation exponents?
413        # TODO: Reduce nucleation efficiency?
414        strain_energies = np.empty(n_grains)
415        orientations_diff = np.empty((n_grains, 3, 3))
416        for grain_index in range(n_grains):
417            orientation_change, strain_energy = _get_rotation_and_strain(
418                phase,
419                fabric,
420                orientations[grain_index],
421                strain_rate,
422                velocity_gradient,
423                stress_exponent,
424                deformation_exponent,
425                nucleation_efficiency,
426            )
427            orientations_diff[grain_index] = 0.3 * orientation_change
428            strain_energies[grain_index] = strain_energy
429        # Volume average mean strain energy.
430        mean_energy = np.sum(fractions * strain_energies)
431        # Strain energy residuals, minus the energy lost to micro-fracturing.
432        strain_residuals = 0.3 * (mean_energy - strain_energies)
433        fractions_diff = volume_fraction * gbm_mobility * fractions * strain_residuals
434        return orientations_diff, fractions_diff
435    elif regime == DeformationRegime.max_viscosity:
436        # Do absolutely nothing, all derivatives are zero.
437        return (
438            np.repeat(np.eye(3), n_grains).reshape(3, 3, n_grains).transpose(),
439            np.zeros(n_grains),
440        )
441    else:
442        raise ValueError(f"regime must be a valid `DeformationRegime`, not {regime}")
443
444
445@nb.njit(fastmath=True)
446def _get_deformation_rate(
447    phase: MineralPhase, orientation: np.ndarray, slip_rates: np.ndarray
448):
449    """Calculate deformation rate tensor for olivine or enstatite.
450
451    Calculate the deformation rate with respect to the local coordinate frame,
452    defined by the principal strain axes (finite strain ellipsoid).
453
454    - `phase` — ordinal number of the mineral phase
455    - `orientation` — 3x3 orientation matrix (direction cosines)
456    - `slip_rates` — slip rates relative to slip rate on softest slip system
457
458    """
459    # This is called the 'G'/'slip' tensor in the Fortran code, aka the Schmid tensor.
460    deformation_rate = np.empty((3, 3))
461    for i in range(3):
462        for j in range(3):
463            deformation_rate[i, j] = 2 * (
464                slip_rates[0] * orientation[0, i] * orientation[1, j]
465                + slip_rates[1] * orientation[0, i] * orientation[2, j]
466                + slip_rates[2] * orientation[2, i] * orientation[1, j]
467                + slip_rates[3] * orientation[2, i] * orientation[0, j]
468            )
469    return deformation_rate
470
471
472@nb.njit(fastmath=True)
473def _get_slip_rate_softest(deformation_rate: np.ndarray, velocity_gradient: np.ndarray):
474    """Calculate dimensionless strain rate on the softest slip system.
475
476    - `deformation_rate` — 3x3 dimensionless deformation rate matrix
477    - `velocity_gradient` — 3x3 dimensionless velocity gradient matrix
478
479    """
480    # See eq. 4 in Fraters 2021.
481    enumerator = 0
482    denominator = 0
483
484    for j in range(3):
485        # NOTE: Mistake in original DRex code (j + 2), see Fraters & Billen 2021 S1.
486        if _USE_ORIGINAL_DREX:
487            k = (j + 2) % 3
488        else:
489            k = (j + 1) % 3
490        enumerator -= (velocity_gradient[j, k] - velocity_gradient[k, j]) * (
491            deformation_rate[j, k] - deformation_rate[k, j]
492        )
493        # NOTE: Mistake in Kaminski 2001 eq. 7: kl+1 instead of kk+1
494        # See Fraters & Billen 2021 supplementary informaton S1.
495        denominator -= (deformation_rate[j, k] - deformation_rate[k, j]) ** 2
496
497        for L in range(3):
498            enumerator += 2 * deformation_rate[j, L] * velocity_gradient[j, L]
499            denominator += 2 * deformation_rate[j, L] ** 2
500
501    # Avoid zero divisions:
502    # Tiny denominator means that relevant deformation_rate entries are zero.
503    # No deformation rate means no slip rate.
504    if -1e-15 < denominator < 1e-15:
505        return 0.0
506    return enumerator / denominator
507
508
509@nb.njit(fastmath=True)
510def _get_slip_rates_olivine(
511    invariants: np.ndarray,
512    slip_indices: np.ndarray,
513    crss: np.ndarray,
514    deformation_exponent: float,
515):
516    """Calculate relative slip rates of the active slip systems for olivine.
517
518    - `invariants` — strain rate invariants for the four slip systems
519    - `slip_indices` — indices that sort the CRSS by increasing slip-rate activity
520    - `crss` — reference resolved shear stresses (CRSS), see `pydrex.fabric`
521    - `deformation_exponent` — `n` in `shear_stress ∝ |deformation_rate|^(1/n)`
522
523    """
524    i_inac, i_min, i_int, i_max = slip_indices
525    # Ratio of slip rates on each slip system to slip rate on softest slip system.
526    # Softest slip system has max. slip rate (aka activity).
527    # See eq. 5, Kaminski 2001.
528    prefactor = crss[i_max] / invariants[i_max]
529    ratio_min = prefactor * invariants[i_min] / crss[i_min]
530    ratio_int = prefactor * invariants[i_int] / crss[i_int]
531    slip_rates = np.empty(4)
532    slip_rates[i_inac] = 0  # Hardest system is completely inactive in olivine.
533    slip_rates[i_min] = ratio_min * np.abs(ratio_min) ** (deformation_exponent - 1)
534    slip_rates[i_int] = ratio_int * np.abs(ratio_int) ** (deformation_exponent - 1)
535    slip_rates[i_max] = 1
536    return slip_rates
537
538
539@nb.njit(fastmath=True)
540def _get_slip_invariants(strain_rate: np.ndarray, orientation: np.ndarray):
541    r"""Calculate strain rate invariants for minerals with four slip systems.
542
543    Calculates $I = ∑_{ij} l_{i} n_{j} \dot{ε}_{ij}$ for each slip sytem of:
544    - (010)[100]
545    - (001)[100]
546    - (010)[001]
547    - (100)[001]
548    Only the last return value is relevant for enstatite.
549    These are not configurable for now.
550
551    - `strain_rate` — 3x3 dimensionless strain rate matrix
552    - `orientation` — 3x3 orientation matrix (direction cosines)
553
554    """
555    invariants = np.zeros(4)
556    for i in range(3):
557        for j in range(3):
558            # (010)[100]
559            invariants[0] += strain_rate[i, j] * orientation[0, i] * orientation[1, j]
560            # (001)[100]
561            invariants[1] += strain_rate[i, j] * orientation[0, i] * orientation[2, j]
562            # (010)[001]
563            invariants[2] += strain_rate[i, j] * orientation[2, i] * orientation[1, j]
564            # (100)[001]
565            invariants[3] += strain_rate[i, j] * orientation[2, i] * orientation[0, j]
566    return invariants
567
568
569@nb.njit(fastmath=True)
570def _get_orientation_change(
571    orientation: np.ndarray,
572    velocity_gradient: np.ndarray,
573    deformation_rate: np.ndarray,
574    slip_rate_softest: float,
575):
576    """Calculate the rotation rate for a grain undergoing dislocation creep.
577
578    - `orientation` — 3x3 orientation matrix (direction cosines)
579    - `velocity_gradient` — 3x3 dimensionless velocity gradient matrix
580    - `deformation_rate` — 3x3 dimensionless deformation rate matrix
581    - `slip_rate_softest` — slip rate on the softest (most active) slip system
582
583    """
584    orientation_change = np.zeros((3, 3))
585    # Spin vector for the grain, see eq. 3 in Fraters 2021.
586    # Includes rigid body rotation as well as the plastic deformation contribution.
587    # This means that even with no active slip systems, the rotation will be nonzero
588    # if there is a rotational (i.e. antisymmetric) component of the velocity_gradient.
589    spin_vector = np.empty(3)
590    for j in range(3):
591        r = (j + 1) % 3
592        s = (j + 2) % 3
593        spin_vector[j] = (
594            (velocity_gradient[s, r] - velocity_gradient[r, s])
595            - (deformation_rate[s, r] - deformation_rate[r, s]) * slip_rate_softest
596        ) / 2
597
598    # Calculate rotation rate, see eq. 9 Kaminski & Ribe (2001).
599    # Equivalent to:
600    #   spin_matrix = np.einsum("ikj,k->ij", PERMUTATION_SYMBOL, spin_vector)
601    #   orientation_change = spin_matrix.transpose() @ orientation
602    for p in range(3):
603        for q in range(3):
604            for r in range(3):
605                for s in range(3):
606                    orientation_change[p, q] += (
607                        PERMUTATION_SYMBOL[q, r, s] * orientation[p, s] * spin_vector[r]
608                    )
609
610    return orientation_change
611
612
613@nb.njit(fastmath=True)
614def _get_strain_energy(
615    crss: np.ndarray,
616    slip_rates: np.ndarray,
617    slip_indices: np.ndarray,
618    slip_rate_softest: float,
619    stress_exponent: float,
620    deformation_exponent: float,
621    nucleation_efficiency: float,
622):
623    """Calculate strain energy due to dislocations for an olivine grain.
624
625    - `crss` — reference resolved shear stresses (CRSS), see `pydrex.fabric`
626    - `slip_rates` — slip rates relative to slip rate on softest slip system
627    - `slip_indices` — indices that sort the CRSS by increasing slip-rate activity
628    - `slip_rate_softest` — slip rate on the softest (most active) slip system
629    - `stress_exponent` — `p` in `dislocation_density ∝ shear_stress^p`
630    - `deformation_exponent` — `n` in `shear_stress ∝ |deformation_rate|^(1/n)`
631    - `nucleation_efficiency` — parameter controlling grain nucleation
632
633    Note that "new" grains are assumed to rotate with their parent.
634
635    """
636    strain_energy = 0.0
637    # Dimensionless dislocation density for each slip system.
638    # See eq. 16 Fraters 2021.
639    # NOTE: Mistake in eq. 11, Kaminski 2004: spurious division by strain rate scale.
640    # NOTE: Here we call 'p' the 'stress_exponent' and 'n' the 'deformation_exponent',
641    # but in the original they use the variable 'stress_exponent' for 'n' (3.5).
642    for i in range(3):
643        dislocation_density = (1 / crss[i]) ** (
644            deformation_exponent - stress_exponent
645        ) * np.abs(slip_rates[i] * slip_rate_softest) ** (
646            stress_exponent / deformation_exponent
647        )
648        # Dimensionless strain energy for this grain, see eq. 14, Fraters 2021.
649        strain_energy += dislocation_density * np.exp(
650            -nucleation_efficiency * dislocation_density**2
651        )
652    return strain_energy
653
654
655@nb.njit(fastmath=True)
656def _get_rotation_and_strain(
657    phase: MineralPhase,
658    fabric: MineralFabric,
659    orientation: np.ndarray,
660    strain_rate: np.ndarray,
661    velocity_gradient: np.ndarray,
662    stress_exponent: float,
663    deformation_exponent: float,
664    nucleation_efficiency: float,
665):
666    """Get the crystal axes rotation rate and strain energy of individual grain.
667
668    - `phase` — ordinal number of the mineral phase
669    - `fabric` — ordinal number of the fabric type
670    - `orientation` — 3x3 orientation matrix (direction cosines)
671    - `strain_rate` — 3x3 dimensionless strain rate matrix
672    - `velocity_gradient` — 3x3 dimensionless velocity gradient matrix
673    - `stress_exponent` — `p` in `dislocation_density ∝ shear_stress^p`
674    - `deformation_exponent` — `n` in `shear_stress ∝ |deformation_rate|^(1/n)`
675    - `nucleation_efficiency — parameter controlling grain nucleation
676
677    Note that "new" grains are assumed to rotate with their parent.
678
679    Returns a tuple with the rotation rate of the crystalline axes
680    with respect to the principal strain axes and strain energy of the grain.
681
682    """
683    crss = get_crss(phase, fabric)
684    slip_invariants = _get_slip_invariants(strain_rate, orientation)
685    if phase == MineralPhase.olivine:
686        slip_indices = np.argsort(np.abs(slip_invariants / crss))
687        slip_rates = _get_slip_rates_olivine(
688            slip_invariants,
689            slip_indices,
690            crss,
691            deformation_exponent,
692        )
693    elif phase == MineralPhase.enstatite:
694        slip_indices = np.argsort(1 / crss)
695        slip_rates = np.zeros(4)
696        if _USE_ORIGINAL_DREX:
697            slip_rates[-1] = 1  # Original had an arbitrary slip always active (L1410).
698        else:
699            # Assumes exclusively (100)[001] slip for enstatite.
700            if np.abs(slip_invariants[-1]) > 1e-15:
701                slip_rates[-1] = 1
702    else:
703        assert False  # Should never happen.
704
705    deformation_rate = _get_deformation_rate(phase, orientation, slip_rates)
706    slip_rate_softest = _get_slip_rate_softest(deformation_rate, velocity_gradient)
707    orientation_change = _get_orientation_change(
708        orientation,
709        velocity_gradient,
710        deformation_rate,
711        slip_rate_softest,
712    )
713    strain_energy = _get_strain_energy(
714        crss,
715        slip_rates,
716        slip_indices,
717        slip_rate_softest,
718        stress_exponent,
719        deformation_exponent,
720        nucleation_efficiency,
721    )
722    return orientation_change, strain_energy
PERMUTATION_SYMBOL = array([[[ 0., 0., 0.], [ 0., 0., 1.], [ 0., -1., 0.]], [[ 0., 0., -1.], [ 0., 0., 0.], [ 1., 0., 0.]], [[ 0., 1., 0.], [-1., 0., 0.], [ 0., 0., 0.]]])

Sometimes called the Levi-Civita symbol.

@unique
class MineralPhase(enum.IntEnum):
36@unique
37class MineralPhase(IntEnum):
38    """Supported mineral phases.
39
40    Forsterite and fayalite are grouped into “olivine”, because we treat them as
41    rheologically equivalent.
42
43    """
44
45    olivine = 0
46    """(Mg,Fe)₂SiO₄"""
47    enstatite = 1
48    """MgSiO₃"""

Supported mineral phases.

Forsterite and fayalite are grouped into “olivine”, because we treat them as rheologically equivalent.

olivine = <MineralPhase.olivine: 0>

(Mg,Fe)₂SiO₄

enstatite = <MineralPhase.enstatite: 1>

MgSiO₃

Inherited Members
enum.Enum
name
value
builtins.int
conjugate
bit_length
bit_count
to_bytes
from_bytes
as_integer_ratio
is_integer
real
imag
numerator
denominator
@unique
class MineralFabric(enum.IntEnum):
51@unique
52class MineralFabric(IntEnum):
53    """Supported mineral fabrics.
54
55    The following fabric types are supported:
56    - olivine A-E type fabrics according to e.g.
57      [Karato et al. (2008)](https://doi.org/10.1146%2Fannurev.earth.36.031207.124120).
58    - enstatite AB fabric, see
59      [Bernard et al. (2021)](https://doi.org/10.1016/j.tecto.2021.228954).
60
61    """
62
63    olivine_A = 0
64    olivine_B = 1
65    olivine_C = 2
66    olivine_D = 3
67    olivine_E = 4
68    enstatite_AB = 5

Supported mineral fabrics.

The following fabric types are supported:

olivine_A = <MineralFabric.olivine_A: 0>
olivine_B = <MineralFabric.olivine_B: 1>
olivine_C = <MineralFabric.olivine_C: 2>
olivine_D = <MineralFabric.olivine_D: 3>
olivine_E = <MineralFabric.olivine_E: 4>
enstatite_AB = <MineralFabric.enstatite_AB: 5>
Inherited Members
enum.Enum
name
value
builtins.int
conjugate
bit_length
bit_count
to_bytes
from_bytes
as_integer_ratio
is_integer
real
imag
numerator
denominator
@unique
class DeformationRegime(enum.IntEnum):
 71@unique
 72class DeformationRegime(IntEnum):
 73    r"""Ordinals to track distinct regimes of dominant deformation mechanisms.
 74
 75    The mechanism of deformation that dominates accommodation of plastic deformation
 76    depends in general both on material properties such as grain size and mineral phase
 77    content as well as on thermodynamic properties such as temperature, pressure and
 78    water fugacity.
 79
 80    The activity of diffusive mechanisms depends more strongly on grain size, whereas
 81    that of dislocation mechanisms depends more strongly on temperature. High
 82    temperatures enable more frequent recovery of dislocation density equilibrium via
 83    e.g. dislocation climb. Dislocation mechanisms are often accompanied by dynamic
 84    recrystallisation, which acts as an additional recovery mechanism.
 85
 86    Diffusive mechanisms are expected, however, to become dominant at depth, as these
 87    mechanisms are primarily facilitated by Si diffusion in olivine, which is
 88    temperature-dependent.
 89
 90    Rheology in the intra-granular dislocation regime was classically described by
 91    separate flow laws depending on temperature; a power-law at high temperature
 92    [$\dot{ε} ∝ σⁿ$] and an exponential-law at low temperature [$\dot{ε} ∝ \exp(σ)$].
 93    More recent work has suggested unified dislocation creep flow laws,
 94    e.g. [Gouriet et al. 2019](http://dx.doi.org/10.1016/j.epsl.2018.10.049),
 95    [Garel et al. 2020](http://dx.doi.org/10.1016/j.epsl.2020.116243) and
 96    [Demouchy et al. 2023](http://dx.doi.org/10.2138/gselements.19.3.151).
 97
 98    .. note:: Although a draft texture evolution behaviour is implemented in the
 99        `frictional_yielding` regime, it is experimental and not yet configurable via
100        the parameter interface.
101
102    """
103
104    min_viscosity = 0
105    """Arbitrary lower-bound viscosity regime."""
106    matrix_diffusion = 1
107    """Intra-granular Nabarro-Herring creep, i.e. grains diffuse through the matrix."""
108    boundary_diffusion = 2
109    """Inter-granular Coble creep, i.e. grains diffuse along grain boundaries."""
110    sliding_diffusion = 3
111    """Inter-granular diffusion-assisted grain-boundary sliding (diffGBS)."""
112    matrix_dislocation = 4
113    """Intra-granular dislocation creep (glide + climb) and dynamic recrystallisation."""
114    sliding_dislocation = 5
115    """Inter-granular dislocation-assisted grain-boundary sliding (disGBS)."""
116    frictional_yielding = 6
117    """Frictional sliding along micro-fractures (Byerlee's law for yield strength)."""
118    max_viscosity = 7
119    """Arbitrary upper-bound viscosity regime."""

Ordinals to track distinct regimes of dominant deformation mechanisms.

The mechanism of deformation that dominates accommodation of plastic deformation depends in general both on material properties such as grain size and mineral phase content as well as on thermodynamic properties such as temperature, pressure and water fugacity.

The activity of diffusive mechanisms depends more strongly on grain size, whereas that of dislocation mechanisms depends more strongly on temperature. High temperatures enable more frequent recovery of dislocation density equilibrium via e.g. dislocation climb. Dislocation mechanisms are often accompanied by dynamic recrystallisation, which acts as an additional recovery mechanism.

Diffusive mechanisms are expected, however, to become dominant at depth, as these mechanisms are primarily facilitated by Si diffusion in olivine, which is temperature-dependent.

Rheology in the intra-granular dislocation regime was classically described by separate flow laws depending on temperature; a power-law at high temperature [$\dot{ε} ∝ σⁿ$] and an exponential-law at low temperature [$\dot{ε} ∝ \exp(σ)$]. More recent work has suggested unified dislocation creep flow laws, e.g. Gouriet et al. 2019, Garel et al. 2020 and Demouchy et al. 2023.

Although a draft texture evolution behaviour is implemented in the

frictional_yielding regime, it is experimental and not yet configurable via the parameter interface.

min_viscosity = <DeformationRegime.min_viscosity: 0>

Arbitrary lower-bound viscosity regime.

matrix_diffusion = <DeformationRegime.matrix_diffusion: 1>

Intra-granular Nabarro-Herring creep, i.e. grains diffuse through the matrix.

boundary_diffusion = <DeformationRegime.boundary_diffusion: 2>

Inter-granular Coble creep, i.e. grains diffuse along grain boundaries.

sliding_diffusion = <DeformationRegime.sliding_diffusion: 3>

Inter-granular diffusion-assisted grain-boundary sliding (diffGBS).

matrix_dislocation = <DeformationRegime.matrix_dislocation: 4>

Intra-granular dislocation creep (glide + climb) and dynamic recrystallisation.

sliding_dislocation = <DeformationRegime.sliding_dislocation: 5>

Inter-granular dislocation-assisted grain-boundary sliding (disGBS).

frictional_yielding = <DeformationRegime.frictional_yielding: 6>

Frictional sliding along micro-fractures (Byerlee's law for yield strength).

max_viscosity = <DeformationRegime.max_viscosity: 7>

Arbitrary upper-bound viscosity regime.

Inherited Members
enum.Enum
name
value
builtins.int
conjugate
bit_length
bit_count
to_bytes
from_bytes
as_integer_ratio
is_integer
real
imag
numerator
denominator
@dataclass(frozen=True)
class DefaultParams:
122@dataclass(frozen=True)
123class DefaultParams:
124    phase_assemblage: tuple = (MineralPhase.olivine,)
125    """Mineral phases present in the aggregate."""
126    phase_fractions: tuple = (1.0,)
127    """Volume fractions of each mineral phase present in the aggregate."""
128    stress_exponent: float = 1.5
129    """The value for $p$ in $ρ ∝ τᵖ$ where $ρ$ is the dislocation density and $τ$ the shear stress.
130
131    Default value taken from [Kaminski et al. 2004](https://doi.org/10.1111/j.1365-246X.2004.02308.x)
132    based on studies referenced therein.
133
134    """
135    deformation_exponent: float = 3.5
136    """The value for $n$ in $τ ∝ |D|^{1/n}$ where $τ$ is the shear stress and D the deformation rate.
137
138    Default value taken from [Kaminski et al. 2004](https://doi.org/10.1111/j.1365-246X.2004.02308.x)
139    based on studies referenced therein.
140
141    """
142    gbm_mobility: int = 125
143    """Dimensionless grain boundary mobility parameter (M*).
144
145    This controls the rate of all dynamic recrystallisation processes in
146    `DeformationRegime.matrix_dislocation`.
147
148    .. note:: This parameter is not easy to constrain. Reasonable values may lie in the
149        range [10, 150]. Comparing outputs for multiple M* values is recommended.
150
151    """
152    gbs_threshold: float = 0.3
153    """Grain boundary sliding threshold.
154
155    In `DeformationRegime.matrix_dislocation` or `DeformationRegime.sliding_dislocation`
156    this controls the smallest size of a grain, relative to its original size,
157    at which it will still deform via dislocation creep.
158    Smaller grains will instead deform via grain boundary sliding,
159    therefore not contributing to any further texture evolution.
160
161    .. note:: Values for this parameter do NOT correspond to a physical grain size
162        threshold. Grains in PyDRex are a surrogate numerical discretisation. There are
163        likely to be feedbacks between this parameter and `number_of_grains`. Values of
164        ~0.3 have been used in numerous studies which employed ~3000 grains.
165
166    """
167    nucleation_efficiency: float = 5.0
168    """Dimensionless nucleation efficiency (λ*).
169
170    This controls the nucleation of subgrains in `DeformationRegime.matrix_dislocation`.
171
172    The default value comes from [Kaminski & Ribe 2001](https://doi.org/10.1016%2Fs0012-821x%2801%2900356-9).
173
174    """
175    number_of_grains: int = 3500
176    """Number of surrogate grains for numerical discretisation of the aggregate.
177
178    This is a numerical discretisation, so generally more grains is better. However,
179    there is a sharp tradeoff in performance, especially for M-index calculation.
180
181    """
182    initial_olivine_fabric: MineralFabric = MineralFabric.olivine_A
183    """Olivine fabric (CRSS distribution) at the beginning of the simulation.
184
185    In general, the fabric should be allowed to change during the simulation,
186    but that is not yet implemented.
187
188    """
189    disl_Peierls_stress: float = 2
190    """Stress barrier in GPa for activation of dislocation motion at low temperatures.
191
192    - 2GPa suggested by [Demouchy et al. 2023](http://dx.doi.org/10.2138/gselements.19.3.151)
193
194    .. note:: Not relevant if the unified dislocation creep flow law is used.
195
196    """
197    # NOTE: For now, we just roll this into the prefactors.
198    # water_fugacity: float = 1.5
199    # """Approximate constant water fugacity of the aggregate (GPa)."""
200    # TODO: Check units of Garel 2020 pre-exp value below.
201    # TODO: Find and add references for the other value.
202    disl_prefactors: tuple = (1e-16, 1e-17)
203    """Prefactors for dislocation creep exponential & power laws (s⁻¹).
204
205    - B = 1.7e-16 s⁻¹ for the exponential law suggested by [Demouchy et al. 2023](http://dx.doi.org/10.2138/gselements.19.3.151)
206    - pre-exponential factor of 4.4e-17 Pa⁻ⁿs⁻¹ used for the exponential law by [Garel et al. 2020](http://dx.doi.org/10.1016/j.epsl.2020.116243)
207
208    """
209    # TODO: Add references, tweak default values if necessary.
210    diff_prefactors: tuple = (1e-10, 1e-10)
211    r"""Prefactors for (matrix and boundary) diffusion creep power laws (s⁻¹).
212
213    Dependence on molar volume and physical grain size are suppressed because these
214    diagnostics are not readily available. The prefactor is roughly equal to
215    $(Vₘ D⁰)/d²$ for $Vₘ$ the molar volume, $D⁰$ the reference diffusion coefficient and
216    $d²$ an average grain size assumed to be constant.
217
218    """
219    disl_lowtemp_switch: float = 0.7
220    """Threshold homologous temperature below which to use the exponential flow law.
221
222    The default value suggested here comes from
223    [Demouchy et al. 2023](http://dx.doi.org/10.2138/gselements.19.3.151).
224
225    .. note:: Not relevant if the unified dislocation creep flow law is used instead.
226
227    """
228    # TODO: Add more references from experimental studies/reviews.
229    disl_activation_energy: float = 460.0
230    """Activation energy for dislocation creep power law (kJ/mol).
231
232    - 443 kJ/mol used by [Gouriet et al. 2019](http://dx.doi.org/10.1016/j.epsl.2018.10.049),
233        but for an exponential law
234    - 540 kJ/mol used by [Garel et al. 2020](http://dx.doi.org/10.1016/j.epsl.2020.116243)
235
236    """
237    # TODO: Add more references, tweak default if necessary.
238    disl_activation_volume: float = 12.0
239    """Activation volume for dislocation creep power law (cm³/mol).
240
241    - 0, 6 and 12 cm³/mol used by [Garel et al. 2020](http://dx.doi.org/10.1016/j.epsl.2020.116243)
242
243    """
244    # TODO: Add more references, justify choice of lower value than Garel 2020.
245    diff_activation_energies: tuple = (430.0, 330)
246    """Activation energies for (matrix and boundary) diffusion creep power laws (kJ/mol).
247
248    - 530 kJ/mol reported for Si self-diffusion in olivine¹
249    - 410 kJ/mol used by [Garel et al. 2020](http://dx.doi.org/10.1016/j.epsl.2020.116243)
250
251    ¹[Dohmen et al. 2002](http://dx.doi.org/10.1029/2002GL015480)
252
253    """
254    # TODO: Add more references, tweak default values if necessary.
255    diff_activation_volumes: tuple = (4.0, 4.0)
256    """Activation volumes for (matrix and boundary) diffusion creep power laws (cm³/mol).
257
258    - 4 kJ/mol used by [Garel et al. 2020](http://dx.doi.org/10.1016/j.epsl.2020.116243)
259
260    """
261    disl_coefficients: tuple = (
262        4.4e8,  # a₀
263        -5.26e4,  # b₀
264        2.11e-2,  # a₁
265        1.74e-4,  # b₁
266        -41.8,  # a₂
267        4.21e-2,  # b₂
268        -1.14e-5,  # c₂
269    )
270    """Coefficients for polynomials used in the alternative dislocation creep flow law.
271
272    The defaults are for the TANH variant of the unified creep law, proposed in
273    [Garel et al. 2020](http://dx.doi.org/10.1016/j.epsl.2020.116243).
274    By contrast, [Gouriet et al. 2019](http://dx.doi.org/10.1016/j.epsl.2018.10.049)
275    used the ERF variant with: [4.4e8, -2.2e4, 3e-2, 1.3e-4, -42, 4.2e-2, -1.1e-5].
276
277    """
278
279    def as_dict(self):
280        """Return mutable copy of default arguments as a dictionary."""
281        return asdict(self)
DefaultParams( phase_assemblage: tuple = (<MineralPhase.olivine: 0>,), phase_fractions: tuple = (1.0,), stress_exponent: float = 1.5, deformation_exponent: float = 3.5, gbm_mobility: int = 125, gbs_threshold: float = 0.3, nucleation_efficiency: float = 5.0, number_of_grains: int = 3500, initial_olivine_fabric: MineralFabric = <MineralFabric.olivine_A: 0>, disl_Peierls_stress: float = 2, disl_prefactors: tuple = (1e-16, 1e-17), diff_prefactors: tuple = (1e-10, 1e-10), disl_lowtemp_switch: float = 0.7, disl_activation_energy: float = 460.0, disl_activation_volume: float = 12.0, diff_activation_energies: tuple = (430.0, 330), diff_activation_volumes: tuple = (4.0, 4.0), disl_coefficients: tuple = (440000000.0, -52600.0, 0.0211, 0.000174, -41.8, 0.0421, -1.14e-05))
phase_assemblage: tuple = (<MineralPhase.olivine: 0>,)

Mineral phases present in the aggregate.

phase_fractions: tuple = (1.0,)

Volume fractions of each mineral phase present in the aggregate.

stress_exponent: float = 1.5

The value for $p$ in $ρ ∝ τᵖ$ where $ρ$ is the dislocation density and $τ$ the shear stress.

Default value taken from Kaminski et al. 2004 based on studies referenced therein.

deformation_exponent: float = 3.5

The value for $n$ in $τ ∝ |D|^{1/n}$ where $τ$ is the shear stress and D the deformation rate.

Default value taken from Kaminski et al. 2004 based on studies referenced therein.

gbm_mobility: int = 125

Dimensionless grain boundary mobility parameter (M*).

This controls the rate of all dynamic recrystallisation processes in DeformationRegime.matrix_dislocation.

This parameter is not easy to constrain. Reasonable values may lie in the

range [10, 150]. Comparing outputs for multiple M* values is recommended.

gbs_threshold: float = 0.3

Grain boundary sliding threshold.

In DeformationRegime.matrix_dislocation or DeformationRegime.sliding_dislocation this controls the smallest size of a grain, relative to its original size, at which it will still deform via dislocation creep. Smaller grains will instead deform via grain boundary sliding, therefore not contributing to any further texture evolution.

Values for this parameter do NOT correspond to a physical grain size

threshold. Grains in PyDRex are a surrogate numerical discretisation. There are likely to be feedbacks between this parameter and number_of_grains. Values of ~0.3 have been used in numerous studies which employed ~3000 grains.

nucleation_efficiency: float = 5.0

Dimensionless nucleation efficiency (λ*).

This controls the nucleation of subgrains in DeformationRegime.matrix_dislocation.

The default value comes from Kaminski & Ribe 2001.

number_of_grains: int = 3500

Number of surrogate grains for numerical discretisation of the aggregate.

This is a numerical discretisation, so generally more grains is better. However, there is a sharp tradeoff in performance, especially for M-index calculation.

initial_olivine_fabric: MineralFabric = <MineralFabric.olivine_A: 0>

Olivine fabric (CRSS distribution) at the beginning of the simulation.

In general, the fabric should be allowed to change during the simulation, but that is not yet implemented.

disl_Peierls_stress: float = 2

Stress barrier in GPa for activation of dislocation motion at low temperatures.

Not relevant if the unified dislocation creep flow law is used.
disl_prefactors: tuple = (1e-16, 1e-17)

Prefactors for dislocation creep exponential & power laws (s⁻¹).

diff_prefactors: tuple = (1e-10, 1e-10)

Prefactors for (matrix and boundary) diffusion creep power laws (s⁻¹).

Dependence on molar volume and physical grain size are suppressed because these diagnostics are not readily available. The prefactor is roughly equal to $(Vₘ D⁰)/d²$ for $Vₘ$ the molar volume, $D⁰$ the reference diffusion coefficient and $d²$ an average grain size assumed to be constant.

disl_lowtemp_switch: float = 0.7

Threshold homologous temperature below which to use the exponential flow law.

The default value suggested here comes from Demouchy et al. 2023.

Not relevant if the unified dislocation creep flow law is used instead.
disl_activation_energy: float = 460.0

Activation energy for dislocation creep power law (kJ/mol).

disl_activation_volume: float = 12.0

Activation volume for dislocation creep power law (cm³/mol).

diff_activation_energies: tuple = (430.0, 330)

Activation energies for (matrix and boundary) diffusion creep power laws (kJ/mol).

  • 530 kJ/mol reported for Si self-diffusion in olivine¹
  • 410 kJ/mol used by Garel et al. 2020

¹Dohmen et al. 2002

diff_activation_volumes: tuple = (4.0, 4.0)

Activation volumes for (matrix and boundary) diffusion creep power laws (cm³/mol).

disl_coefficients: tuple = (440000000.0, -52600.0, 0.0211, 0.000174, -41.8, 0.0421, -1.14e-05)

Coefficients for polynomials used in the alternative dislocation creep flow law.

The defaults are for the TANH variant of the unified creep law, proposed in Garel et al. 2020. By contrast, Gouriet et al. 2019 used the ERF variant with: [4.4e8, -2.2e4, 3e-2, 1.3e-4, -42, 4.2e-2, -1.1e-5].

def as_dict(self):
279    def as_dict(self):
280        """Return mutable copy of default arguments as a dictionary."""
281        return asdict(self)

Return mutable copy of default arguments as a dictionary.

@nb.njit
def get_crss( phase: MineralPhase, fabric: MineralFabric) -> numpy.ndarray:
284@nb.njit
285def get_crss(phase: MineralPhase, fabric: MineralFabric) -> np.ndarray:
286    """Get Critical Resolved Shear Stress for the mineral `phase` and `fabric`.
287
288    Returns an array of the normalised threshold stresses required to activate slip on
289    each slip system. Olivine slip systems are ordered according to the convention used
290    for `pydrex.minerals.OLIVINE_SLIP_SYSTEMS`.
291
292    """
293    if phase == MineralPhase.olivine:
294        match fabric:
295            case MineralFabric.olivine_A:
296                return np.array([1, 2, 3, np.inf])
297            case MineralFabric.olivine_B:
298                return np.array([3, 2, 1, np.inf])
299            case MineralFabric.olivine_C:
300                return np.array([3, 2, np.inf, 1])
301            case MineralFabric.olivine_D:
302                return np.array([1, 1, 3, np.inf])
303            case MineralFabric.olivine_E:
304                return np.array([3, 1, 2, np.inf])
305            case _:
306                raise ValueError(f"unsupported olivine fabric: {fabric}")
307    elif phase == MineralPhase.enstatite:
308        if fabric == MineralFabric.enstatite_AB:
309            return np.array([np.inf, np.inf, np.inf, 1])
310        raise ValueError(f"unsupported enstatite fabric: {fabric}")
311    raise ValueError(f"phase must be a valid `MineralPhase`, not {phase}")

Get Critical Resolved Shear Stress for the mineral phase and fabric.

Returns an array of the normalised threshold stresses required to activate slip on each slip system. Olivine slip systems are ordered according to the convention used for pydrex.minerals.OLIVINE_SLIP_SYSTEMS.

@nb.njit(fastmath=True)
def derivatives( regime: DeformationRegime, phase: MineralPhase, fabric: MineralFabric, n_grains: int, orientations: numpy.ndarray, fractions: numpy.ndarray, strain_rate: numpy.ndarray, velocity_gradient: numpy.ndarray, deformation_gradient_spin: numpy.ndarray, stress_exponent: float, deformation_exponent: float, nucleation_efficiency: float, gbm_mobility: float, volume_fraction: float):
316@nb.njit(fastmath=True)
317def derivatives(
318    regime: DeformationRegime,
319    phase: MineralPhase,
320    fabric: MineralFabric,
321    n_grains: int,
322    orientations: np.ndarray,
323    fractions: np.ndarray,
324    strain_rate: np.ndarray,
325    velocity_gradient: np.ndarray,
326    deformation_gradient_spin: np.ndarray,
327    stress_exponent: float,
328    deformation_exponent: float,
329    nucleation_efficiency: float,
330    gbm_mobility: float,
331    volume_fraction: float,
332):
333    """Get derivatives of orientation and volume distribution.
334
335    - `regime` — ordinal number of the local deformation mechanism
336    - `phase` — ordinal number of the mineral phase
337    - `fabric` — ordinal number of the fabric type
338    - `n_grains` — number of "grains" i.e. discrete volume segments
339    - `orientations` — `n_grains`x3x3 orientations (direction cosines)
340    - `fractions` — volume fractions of the grains relative to aggregate volume
341    - `strain_rate` — 3x3 dimensionless macroscopic strain-rate tensor
342    - `velocity_gradient` — 3x3 dimensionless macroscopic velocity gradient
343    - `deformation_gradient_spin` — 3x3 spin tensor defining the rate of rotation of the
344      finite strain ellipse
345    - `stress_exponent` — `p` in `dislocation_density ∝ shear_stress^p`
346    - `deformation_exponent` — `n` in `shear_stress ∝ |deformation_rate|^(1/n)`
347    - `nucleation_efficiency` — parameter controlling grain nucleation
348    - `gmb_mobility` — grain boundary mobility parameter
349    - `volume_fraction` — volume fraction of the mineral phase relative to other phases
350      (multiphase simulations)
351
352    Returns a tuple with the rotation rates and grain volume fraction changes.
353
354    """
355    if regime == DeformationRegime.min_viscosity:
356        # Do absolutely nothing, all derivatives are zero.
357        return (
358            np.repeat(np.eye(3), n_grains).reshape(3, 3, n_grains).transpose(),
359            np.zeros(n_grains),
360        )
361    elif regime == DeformationRegime.matrix_diffusion:
362        # Passive rotation based on macroscopic vorticity for diffusion creep?
363        # vorticity = 0.5 * (velocity_gradient - velocity_gradient.transpose())
364        # Passive rotation based on spin of F for diffusion creep.
365        vorticity = deformation_gradient_spin
366        # Or just don't change at all?
367        # vorticity = np.zeros((3, 3))
368        # This 💃 is because numba doesn't let us use np.tile or even np.array([a] * n).
369        return (
370            np.repeat(vorticity.transpose(), n_grains)
371            .reshape(3, 3, n_grains)
372            .transpose(),
373            np.zeros(n_grains),
374        )
375    elif regime == DeformationRegime.boundary_diffusion:
376        raise ValueError("this deformation mechanism is not yet supported.")
377    elif regime == DeformationRegime.sliding_diffusion:
378        # Miyazaki et al. 2013 wrote controversial Nature article proposing that CPO can
379        # develop in the diffGBS regime. However, Karato 2024 gives some convincing
380        # arguments in the Journal of Geodynamics for why their results are likely
381        # inapplicable to Earth's upper mantle.
382        raise ValueError("this deformation mechanism is not yet supported.")
383    elif regime == DeformationRegime.matrix_dislocation:
384        # Based on subroutine DERIV in original Fortran.
385        strain_energies = np.empty(n_grains)
386        orientations_diff = np.empty((n_grains, 3, 3))
387        for grain_index in range(n_grains):
388            orientation_change, strain_energy = _get_rotation_and_strain(
389                phase,
390                fabric,
391                orientations[grain_index],
392                strain_rate,
393                velocity_gradient,
394                stress_exponent,
395                deformation_exponent,
396                nucleation_efficiency,
397            )
398            orientations_diff[grain_index] = orientation_change
399            strain_energies[grain_index] = strain_energy
400        # Volume average mean strain energy.
401        mean_energy = np.sum(fractions * strain_energies)
402        # Strain energy residual.
403        strain_residuals = mean_energy - strain_energies
404        fractions_diff = volume_fraction * gbm_mobility * fractions * strain_residuals
405        return orientations_diff, fractions_diff
406    elif regime == DeformationRegime.sliding_dislocation:
407        raise ValueError("this deformation mechanism is not yet supported.")
408    elif regime == DeformationRegime.frictional_yielding:
409        # For now, the same as matrix_dislocation, but we smooth the strain energy
410        # distribution and the orientation changes, since some energy is lost to
411        # micro-fracturing. Also increase the GBS threshold, dislocations will tend to
412        # pile up at grain boundaries.
413        # TODO: Maybe modify the stress/deformation exponents?
414        # TODO: Reduce nucleation efficiency?
415        strain_energies = np.empty(n_grains)
416        orientations_diff = np.empty((n_grains, 3, 3))
417        for grain_index in range(n_grains):
418            orientation_change, strain_energy = _get_rotation_and_strain(
419                phase,
420                fabric,
421                orientations[grain_index],
422                strain_rate,
423                velocity_gradient,
424                stress_exponent,
425                deformation_exponent,
426                nucleation_efficiency,
427            )
428            orientations_diff[grain_index] = 0.3 * orientation_change
429            strain_energies[grain_index] = strain_energy
430        # Volume average mean strain energy.
431        mean_energy = np.sum(fractions * strain_energies)
432        # Strain energy residuals, minus the energy lost to micro-fracturing.
433        strain_residuals = 0.3 * (mean_energy - strain_energies)
434        fractions_diff = volume_fraction * gbm_mobility * fractions * strain_residuals
435        return orientations_diff, fractions_diff
436    elif regime == DeformationRegime.max_viscosity:
437        # Do absolutely nothing, all derivatives are zero.
438        return (
439            np.repeat(np.eye(3), n_grains).reshape(3, 3, n_grains).transpose(),
440            np.zeros(n_grains),
441        )
442    else:
443        raise ValueError(f"regime must be a valid `DeformationRegime`, not {regime}")

Get derivatives of orientation and volume distribution.

  • regime — ordinal number of the local deformation mechanism
  • phase — ordinal number of the mineral phase
  • fabric — ordinal number of the fabric type
  • n_grains — number of "grains" i.e. discrete volume segments
  • orientationsn_grainsx3x3 orientations (direction cosines)
  • fractions — volume fractions of the grains relative to aggregate volume
  • strain_rate — 3x3 dimensionless macroscopic strain-rate tensor
  • velocity_gradient — 3x3 dimensionless macroscopic velocity gradient
  • deformation_gradient_spin — 3x3 spin tensor defining the rate of rotation of the finite strain ellipse
  • stress_exponentp in dislocation_density ∝ shear_stress^p
  • deformation_exponentn in shear_stress ∝ |deformation_rate|^(1/n)
  • nucleation_efficiency — parameter controlling grain nucleation
  • gmb_mobility — grain boundary mobility parameter
  • volume_fraction — volume fraction of the mineral phase relative to other phases (multiphase simulations)

Returns a tuple with the rotation rates and grain volume fraction changes.