tests.test_core

PyDRex: Tests for core D-Rex routines.

  1"""> PyDRex: Tests for core D-Rex routines."""
  2
  3import contextlib as cl
  4
  5import numpy as np
  6import pytest
  7from numpy import testing as nt
  8from pydrex import core as _core
  9from pydrex import io as _io
 10from pydrex import logger as _log
 11from pydrex import minerals as _minerals
 12from pydrex import utils as _utils
 13from pydrex import visualisation as _vis
 14from scipy.spatial.transform import Rotation
 15
 16# Subdirectory of `outdir` used to store outputs from these tests.
 17SUBDIR = "core"
 18
 19
 20class TestDislocationCreepOPX:
 21    """Single-grain orthopyroxene crystallographic rotation rate tests."""
 22
 23    class_id = "dislocation_creep_OPX"
 24
 25    @pytest.mark.skipif(_utils.in_ci("win32"), reason="Not equal to tolerance")
 26    def test_shear_dudz(self, outdir):
 27        test_id = "dudz"
 28        optional_logging = cl.nullcontext()
 29        if outdir is not None:
 30            optional_logging = _io.logfile_enable(
 31                f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.log"
 32            )
 33        with optional_logging:
 34            for θ in np.mgrid[0 : 2 * np.pi : 360j]:
 35                _log.debug("θ (°): %s", np.rad2deg(θ))
 36                orientations_diff, fractions_diff = _core.derivatives(
 37                    regime=_core.DeformationRegime.matrix_dislocation,
 38                    phase=_core.MineralPhase.enstatite,
 39                    fabric=_core.MineralFabric.enstatite_AB,
 40                    n_grains=1,
 41                    orientations=Rotation.from_rotvec([[0, θ, 0]]).as_matrix(),
 42                    fractions=np.array([1.0]),
 43                    strain_rate=np.array([[0, 0, 1], [0, 0, 0], [1, 0, 0]]),
 44                    velocity_gradient=np.array([[0, 0, 2], [0, 0, 0], [0, 0, 0]]),
 45                    deformation_gradient_spin=np.full((3, 3), np.nan),
 46                    stress_exponent=1.5,
 47                    deformation_exponent=3.5,
 48                    nucleation_efficiency=5,
 49                    gbm_mobility=0,
 50                    volume_fraction=1.0,
 51                )
 52                cosθ = np.cos(θ)
 53                cos2θ = np.cos(2 * θ)
 54                sinθ = np.sin(θ)
 55                target_orientations_diff = (1 + cos2θ) * np.array(
 56                    [[sinθ, 0, -cosθ], [0, 0, 0], [cosθ, 0, sinθ]]
 57                )
 58                np.testing.assert_allclose(
 59                    orientations_diff[0], target_orientations_diff
 60                )
 61                assert np.isclose(np.sum(fractions_diff), 0.0)
 62
 63    def test_shear_dvdx(self, outdir):
 64        test_id = "dvdx"
 65        optional_logging = cl.nullcontext()
 66        if outdir is not None:
 67            optional_logging = _io.logfile_enable(
 68                f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.log"
 69            )
 70        with optional_logging:
 71            for θ in np.linspace(0, 2 * np.pi, 361):
 72                _log.debug("θ (°): %s", np.rad2deg(θ))
 73                orientations = Rotation.from_rotvec([[0, 0, θ]]).as_matrix()
 74                deformation_rate = _core._get_deformation_rate(
 75                    _core.MineralPhase.enstatite,
 76                    orientations[0],
 77                    np.array([0, 0, 0, 0]),
 78                )
 79                np.testing.assert_allclose(deformation_rate.flatten(), np.zeros(9))
 80                orientations_diff, fractions_diff = _core.derivatives(
 81                    regime=_core.DeformationRegime.matrix_dislocation,
 82                    phase=_core.MineralPhase.enstatite,
 83                    fabric=_core.MineralFabric.enstatite_AB,
 84                    n_grains=1,
 85                    orientations=orientations,
 86                    fractions=np.array([1.0]),
 87                    strain_rate=np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]]),
 88                    velocity_gradient=np.array([[0, 0, 0], [2, 0, 0], [0, 0, 0]]),
 89                    deformation_gradient_spin=np.full((3, 3), np.nan),
 90                    stress_exponent=1.5,
 91                    deformation_exponent=3.5,
 92                    nucleation_efficiency=5,
 93                    gbm_mobility=0,
 94                    volume_fraction=1.0,
 95                )
 96                # Can't activate the (100)[001] slip system, no plastic deformation.
 97                # Only passive (rigid-body) rotation due to the velocity gradient.
 98                sinθ = np.sin(θ)
 99                cosθ = np.cos(θ)
100                target_orientations_diff = np.array(
101                    [[sinθ, cosθ, 0], [-cosθ, sinθ, 0], [0, 0, 0]]
102                )
103                np.testing.assert_allclose(
104                    orientations_diff[0], target_orientations_diff, atol=1e-15
105                )
106                assert np.isclose(np.sum(fractions_diff), 0.0)
107
108
109class TestDislocationCreepOlivineA:
110    """Single-grain A-type olivine analytical rotation rate tests."""
111
112    class_id = "dislocation_creep_OlA"
113
114    @pytest.mark.skipif(_utils.in_ci("win32"), reason="Not equal to tolerance")
115    def test_shear_dvdx_slip_010_100(self, outdir):
116        r"""Single grain of A-type olivine, slip on (010)[100].
117
118        Velocity gradient:
119        $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 2 & 0 & 0 \cr 0 & 0 & 0 \end{bmatrix}$$
120
121        """
122        test_id = "dvdx_010_100"
123        nondim_velocity_gradient = np.array([[0, 0, 0], [2, 0, 0], [0, 0, 0]])
124        # Strain rate is 0.5*(L + Lᵀ).
125        nondim_strain_rate = np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]])
126
127        optional_logging = cl.nullcontext()
128        if outdir is not None:
129            optional_logging = _io.logfile_enable(
130                f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.log"
131            )
132            initial_angles = []
133            rotation_rates = []
134            target_rotation_rates = []
135
136        with optional_logging:
137            for θ in np.mgrid[0 : 2 * np.pi : 3600j]:
138                _log.debug("θ (°): %s", np.rad2deg(θ))
139                # Initial grain rotations around Z (anti-clockwise).
140                initial_orientations = Rotation.from_rotvec([[0, 0, θ]])
141                orientation_matrix = initial_orientations[0].as_matrix()
142                slip_invariants = _core._get_slip_invariants(
143                    nondim_strain_rate, orientation_matrix
144                )
145                _log.debug("slip invariants: %s", slip_invariants)
146
147                crss = _core.get_crss(
148                    _core.MineralPhase.olivine,
149                    _core.MineralFabric.olivine_A,
150                )
151                slip_indices = np.argsort(np.abs(slip_invariants / crss))
152                slip_system = _minerals.OLIVINE_SLIP_SYSTEMS[slip_indices[-1]]
153                assert slip_system == ([0, 1, 0], [1, 0, 0])
154
155                slip_rates = _core._get_slip_rates_olivine(
156                    slip_invariants, slip_indices, crss, 3.5
157                )
158                _log.debug("slip rates: %s", slip_rates)
159
160                deformation_rate = _core._get_deformation_rate(
161                    _core.MineralPhase.olivine,
162                    orientation_matrix,
163                    slip_rates,
164                )
165                _log.debug("deformation rate:\n%s", deformation_rate)
166
167                orientations_diff, fractions_diff = _core.derivatives(
168                    regime=_core.DeformationRegime.matrix_dislocation,
169                    phase=_core.MineralPhase.olivine,
170                    fabric=_core.MineralFabric.olivine_A,
171                    n_grains=1,
172                    orientations=initial_orientations.as_matrix(),
173                    fractions=np.array([1.0]),
174                    strain_rate=nondim_strain_rate,
175                    velocity_gradient=nondim_velocity_gradient,
176                    deformation_gradient_spin=np.full((3, 3), np.nan),
177                    stress_exponent=1.5,
178                    deformation_exponent=3.5,
179                    nucleation_efficiency=5,
180                    gbm_mobility=0,
181                    volume_fraction=1.0,
182                )
183                cosθ = np.cos(θ)
184                cos2θ = np.cos(2 * θ)
185                sinθ = np.sin(θ)
186                target_orientations_diff = np.array(
187                    [
188                        [sinθ * (1 + cos2θ), cosθ * (1 + cos2θ), 0],
189                        [-cosθ * (1 + cos2θ), sinθ * (1 + cos2θ), 0],
190                        [0, 0, 0],
191                    ]
192                )
193                np.testing.assert_allclose(
194                    orientations_diff[0], target_orientations_diff
195                )
196                if outdir is not None:
197                    initial_angles.append(np.rad2deg(θ))
198                    rotation_rates.append(
199                        np.sqrt(
200                            orientations_diff[0][0, 0] ** 2
201                            + orientations_diff[0][0, 1] ** 2
202                        )
203                    )
204                    target_rotation_rates.append(1 + cos2θ)
205                assert np.isclose(np.sum(fractions_diff), 0.0)
206
207        if outdir is not None:
208            fig = _vis.figure(figscale=(1, 2 / 3))
209            axs = fig.subplot_mosaic(  # Put the legend on a special axes.
210                [["legend"], ["spin"]], gridspec_kw={"height_ratios": [0.1, 1]}
211            )
212            ax = axs["spin"]
213            fig, ax, colors = _vis.spin(
214                ax,
215                initial_angles[::25],
216                rotation_rates[::25],
217                initial_angles,
218                target_rotation_rates,
219                shear_axis=90,
220            )
221            _utils.redraw_legend(
222                ax,
223                legendax=axs["legend"],
224                loc="upper center",
225                ncols=3,
226                mode="expand",
227            )
228            fig.savefig(
229                _io.resolve_path(f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.pdf")
230            )
231
232    def test_shear_dudz_slip_001_100(self, outdir):
233        r"""Single grain of A-type olivine, slip on (001)[100].
234
235        Velocity gradient:
236        $$\bm{L} = \begin{bmatrix} 0 & 0 & 2 \cr 0 & 0 & 0 \cr 0 & 0 & 0 \end{bmatrix}$$
237
238        """
239        test_id = "dudz_001_100"
240        nondim_velocity_gradient = np.array([[0, 0, 2], [0, 0, 0], [0, 0, 0]])
241        # Strain rate is 0.5*(L + Lᵀ).
242        nondim_strain_rate = np.array([[0, 0, 1], [0, 0, 0], [1, 0, 0]])
243
244        optional_logging = cl.nullcontext()
245        if outdir is not None:
246            optional_logging = _io.logfile_enable(
247                f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.log"
248            )
249            initial_angles = []
250            rotation_rates = []
251            target_rotation_rates = []
252
253        with optional_logging:
254            for θ in np.mgrid[0 : 2 * np.pi : 360j]:
255                _log.debug("θ (°): %s", np.rad2deg(θ))
256                # Initial grain rotations around Y (anti-clockwise).
257                initial_orientations = Rotation.from_rotvec([[0, θ, 0]])
258                orientation_matrix = initial_orientations[0].as_matrix()
259                slip_invariants = _core._get_slip_invariants(
260                    nondim_strain_rate, orientation_matrix
261                )
262                _log.debug("slip invariants: %s", slip_invariants)
263
264                crss = _core.get_crss(
265                    _core.MineralPhase.olivine,
266                    _core.MineralFabric.olivine_A,
267                )
268                slip_indices = np.argsort(np.abs(slip_invariants / crss))
269                slip_system = _minerals.OLIVINE_SLIP_SYSTEMS[slip_indices[-1]]
270                assert slip_system == ([0, 0, 1], [1, 0, 0])
271
272                slip_rates = _core._get_slip_rates_olivine(
273                    slip_invariants, slip_indices, crss, 3.5
274                )
275                _log.debug("slip rates: %s", slip_rates)
276
277                deformation_rate = _core._get_deformation_rate(
278                    _core.MineralPhase.olivine,
279                    orientation_matrix,
280                    slip_rates,
281                )
282                _log.debug("deformation rate:\n%s", deformation_rate)
283
284                orientations_diff, fractions_diff = _core.derivatives(
285                    regime=_core.DeformationRegime.matrix_dislocation,
286                    phase=_core.MineralPhase.olivine,
287                    fabric=_core.MineralFabric.olivine_A,
288                    n_grains=1,
289                    orientations=initial_orientations.as_matrix(),
290                    fractions=np.array([1.0]),
291                    strain_rate=nondim_strain_rate,
292                    velocity_gradient=nondim_velocity_gradient,
293                    deformation_gradient_spin=np.full((3, 3), np.nan),
294                    stress_exponent=1.5,
295                    deformation_exponent=3.5,
296                    nucleation_efficiency=5,
297                    gbm_mobility=0,
298                    volume_fraction=1.0,
299                )
300                cosθ = np.cos(θ)
301                cos2θ = np.cos(2 * θ)
302                sinθ = np.sin(θ)
303                target_orientations_diff = np.array(
304                    [
305                        [-sinθ * (cos2θ - 1), 0, cosθ * (cos2θ - 1)],
306                        [0, 0, 0],
307                        [cosθ * (1 - cos2θ), 0, sinθ * (1 - cos2θ)],
308                    ]
309                )
310                np.testing.assert_allclose(
311                    orientations_diff[0], target_orientations_diff
312                )
313                if outdir is not None:
314                    initial_angles.append(np.rad2deg(θ))
315                    rotation_rates.append(
316                        np.sqrt(
317                            orientations_diff[0][0, 0] ** 2
318                            + orientations_diff[0][0, 2] ** 2
319                        )
320                    )
321                    target_rotation_rates.append(1 - cos2θ)
322                assert np.isclose(np.sum(fractions_diff), 0.0)
323
324        if outdir is not None:
325            fig = _vis.figure(figscale=(1, 2 / 3))
326            axs = fig.subplot_mosaic(  # Put the legend on a special axes.
327                [["legend"], ["spin"]], gridspec_kw={"height_ratios": [0.1, 1]}
328            )
329            ax = axs["spin"]
330            fig, ax, colors = _vis.spin(
331                ax,
332                initial_angles[::5],
333                rotation_rates[::5],
334                initial_angles,
335                target_rotation_rates,
336                shear_axis=0,
337            )
338            _utils.redraw_legend(
339                ax,
340                legendax=axs["legend"],
341                loc="upper center",
342                ncols=3,
343                mode="expand",
344            )
345            fig.savefig(
346                _io.resolve_path(f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.pdf")
347            )
348
349    @pytest.mark.skipif(_utils.in_ci("win32"), reason="Not equal to tolerance")
350    def test_shear_dwdx_slip_001_100(self, outdir):
351        r"""Single grain of A-type olivine, slip on (001)[100].
352
353        Velocity gradient:
354        $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 0 & 0 & 0 \cr 2 & 0 & 0 \end{bmatrix}$$
355
356        """
357        test_id = "dwdx_001_100"
358        nondim_velocity_gradient = np.array([[0, 0, 0], [0, 0, 0], [2, 0, 0]])
359        # Strain rate is 0.5*(L + Lᵀ).
360        nondim_strain_rate = np.array([[0, 0, 1], [0, 0, 0], [1, 0, 0]])
361
362        optional_logging = cl.nullcontext()
363        if outdir is not None:
364            optional_logging = _io.logfile_enable(
365                f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.log"
366            )
367            initial_angles = []
368            rotation_rates = []
369            target_rotation_rates = []
370
371        with optional_logging:
372            for θ in np.mgrid[0 : 2 * np.pi : 360j]:
373                _log.debug("θ (°): %s", np.rad2deg(θ))
374                # Initial grain rotations around Y (anti-clockwise).
375                initial_orientations = Rotation.from_rotvec([[0, θ, 0]])
376                orientation_matrix = initial_orientations[0].as_matrix()
377                slip_invariants = _core._get_slip_invariants(
378                    nondim_strain_rate, orientation_matrix
379                )
380                _log.debug("slip invariants: %s", slip_invariants)
381
382                crss = _core.get_crss(
383                    _core.MineralPhase.olivine,
384                    _core.MineralFabric.olivine_A,
385                )
386                slip_indices = np.argsort(np.abs(slip_invariants / crss))
387                slip_system = _minerals.OLIVINE_SLIP_SYSTEMS[slip_indices[-1]]
388                assert slip_system == ([0, 0, 1], [1, 0, 0])
389
390                slip_rates = _core._get_slip_rates_olivine(
391                    slip_invariants, slip_indices, crss, 3.5
392                )
393                _log.debug("slip rates: %s", slip_rates)
394
395                deformation_rate = _core._get_deformation_rate(
396                    _core.MineralPhase.olivine,
397                    orientation_matrix,
398                    slip_rates,
399                )
400                _log.debug("deformation rate:\n%s", deformation_rate)
401
402                orientations_diff, fractions_diff = _core.derivatives(
403                    regime=_core.DeformationRegime.matrix_dislocation,
404                    phase=_core.MineralPhase.olivine,
405                    fabric=_core.MineralFabric.olivine_A,
406                    n_grains=1,
407                    orientations=initial_orientations.as_matrix(),
408                    fractions=np.array([1.0]),
409                    strain_rate=nondim_strain_rate,
410                    velocity_gradient=nondim_velocity_gradient,
411                    deformation_gradient_spin=np.full((3, 3), np.nan),
412                    stress_exponent=1.5,
413                    deformation_exponent=3.5,
414                    nucleation_efficiency=5,
415                    gbm_mobility=0,
416                    volume_fraction=1.0,
417                )
418                cosθ = np.cos(θ)
419                cos2θ = np.cos(2 * θ)
420                sinθ = np.sin(θ)
421                target_orientations_diff = np.array(
422                    [
423                        [-sinθ * (1 + cos2θ), 0, cosθ * (1 + cos2θ)],
424                        [0, 0, 0],
425                        [-cosθ * (1 + cos2θ), 0, -sinθ * (1 + cos2θ)],
426                    ]
427                )
428                np.testing.assert_allclose(
429                    orientations_diff[0], target_orientations_diff
430                )
431                if outdir is not None:
432                    initial_angles.append(np.rad2deg(θ))
433                    rotation_rates.append(
434                        np.sqrt(
435                            orientations_diff[0][0, 0] ** 2
436                            + orientations_diff[0][0, 2] ** 2
437                        )
438                    )
439                    target_rotation_rates.append(1 + cos2θ)
440                assert np.isclose(np.sum(fractions_diff), 0.0)
441
442        if outdir is not None:
443            fig = _vis.figure(figscale=(1, 2 / 3))
444            axs = fig.subplot_mosaic(  # Put the legend on a special axes.
445                [["legend"], ["spin"]], gridspec_kw={"height_ratios": [0.1, 1]}
446            )
447            ax = axs["spin"]
448            fig, ax, colors = _vis.spin(
449                ax,
450                initial_angles[::5],
451                rotation_rates[::5],
452                initial_angles,
453                target_rotation_rates,
454                shear_axis=90,
455            )
456            _utils.redraw_legend(
457                ax,
458                legendax=axs["legend"],
459                loc="upper center",
460                ncols=3,
461                mode="expand",
462            )
463            fig.savefig(
464                _io.resolve_path(f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.pdf")
465            )
466
467    @pytest.mark.skipif(_utils.in_ci("win32"), reason="Not equal to tolerance")
468    def test_shear_dvdz_slip_010_001(self, outdir):
469        r"""Single grain of A-type olivine, slip on (010)[001].
470
471        Velocity gradient:
472        $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 0 & 0 & 2 \cr 0 & 0 & 0 \end{bmatrix}$$
473
474        """
475        test_id = "dvdz_010_001"
476        nondim_velocity_gradient = np.array([[0, 0, 0], [0, 0, 2], [0, 0, 0]])
477        # Strain rate is 0.5*(L + Lᵀ).
478        nondim_strain_rate = np.array([[0, 0, 0], [0, 0, 1], [0, 1, 0]])
479
480        optional_logging = cl.nullcontext()
481        if outdir is not None:
482            optional_logging = _io.logfile_enable(
483                f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.log"
484            )
485            initial_angles = []
486            rotation_rates = []
487            target_rotation_rates = []
488
489        with optional_logging:
490            for θ in np.mgrid[0 : 2 * np.pi : 360j]:
491                _log.debug("θ (°): %s", np.rad2deg(θ))
492                # Initial grain rotations around X (anti-clockwise).
493                initial_orientations = Rotation.from_rotvec([[θ, 0, 0]])
494                orientation_matrix = initial_orientations[0].as_matrix()
495                slip_invariants = _core._get_slip_invariants(
496                    nondim_strain_rate, orientation_matrix
497                )
498                _log.debug("slip invariants: %s", slip_invariants)
499
500                crss = _core.get_crss(
501                    _core.MineralPhase.olivine,
502                    _core.MineralFabric.olivine_A,
503                )
504                slip_indices = np.argsort(np.abs(slip_invariants / crss))
505                slip_system = _minerals.OLIVINE_SLIP_SYSTEMS[slip_indices[-1]]
506                assert slip_system == ([0, 1, 0], [0, 0, 1])
507
508                slip_rates = _core._get_slip_rates_olivine(
509                    slip_invariants, slip_indices, crss, 3.5
510                )
511                _log.debug("slip rates: %s", slip_rates)
512
513                deformation_rate = _core._get_deformation_rate(
514                    _core.MineralPhase.olivine,
515                    orientation_matrix,
516                    slip_rates,
517                )
518                _log.debug("deformation rate:\n%s", deformation_rate)
519
520                orientations_diff, fractions_diff = _core.derivatives(
521                    regime=_core.DeformationRegime.matrix_dislocation,
522                    phase=_core.MineralPhase.olivine,
523                    fabric=_core.MineralFabric.olivine_A,
524                    n_grains=1,
525                    orientations=initial_orientations.as_matrix(),
526                    fractions=np.array([1.0]),
527                    strain_rate=nondim_strain_rate,
528                    velocity_gradient=nondim_velocity_gradient,
529                    deformation_gradient_spin=np.full((3, 3), np.nan),
530                    stress_exponent=1.5,
531                    deformation_exponent=3.5,
532                    nucleation_efficiency=5,
533                    gbm_mobility=0,
534                    volume_fraction=1.0,
535                )
536                cosθ = np.cos(θ)
537                cos2θ = np.cos(2 * θ)
538                sinθ = np.sin(θ)
539                target_orientations_diff = np.array(
540                    [
541                        [0, 0, 0],
542                        [0, -sinθ * (1 + cos2θ), -cosθ * (1 + cos2θ)],
543                        [0, cosθ * (1 + cos2θ), -sinθ * (1 + cos2θ)],
544                    ]
545                )
546                np.testing.assert_allclose(
547                    orientations_diff[0], target_orientations_diff
548                )
549                if outdir is not None:
550                    initial_angles.append(np.rad2deg(θ))
551                    rotation_rates.append(
552                        np.sqrt(
553                            orientations_diff[0][1, 1] ** 2
554                            + orientations_diff[0][1, 2] ** 2
555                        )
556                    )
557                    target_rotation_rates.append(1 + cos2θ)
558                assert np.isclose(np.sum(fractions_diff), 0.0)
559
560        if outdir is not None:
561            fig = _vis.figure(figscale=(1, 2 / 3))
562            axs = fig.subplot_mosaic(  # Put the legend on a special axes.
563                [["legend"], ["spin"]], gridspec_kw={"height_ratios": [0.1, 1]}
564            )
565            ax = axs["spin"]
566            fig, ax, colors = _vis.spin(
567                ax,
568                initial_angles[::5],
569                rotation_rates[::5],
570                initial_angles,
571                target_rotation_rates,
572                shear_axis=90,
573            )
574            _utils.redraw_legend(
575                ax,
576                legendax=axs["legend"],
577                loc="upper center",
578                ncols=3,
579                mode="expand",
580            )
581            fig.savefig(
582                _io.resolve_path(f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.pdf")
583            )
584
585
586class TestRecrystallisation2D:
587    """Basic recrystallisation tests for 2D simple shear."""
588
589    class_id = "recrystallisation_2D"
590
591    def test_shear_dvdx_circle_inplane(self, outdir):
592        r"""360000 grains of A-type olivine with uniform spread of a-axes on a circle.
593
594        Grain growth rates are compared to analytical calculations.
595        The a-axes are distributed in the YX plane (i.e.\ rotated around Z).
596
597        Velocity gradient:
598        $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 2 & 0 & 0 \cr 0 & 0 & 0 \end{bmatrix}$$
599
600        """
601        test_id = "dvdx_circle_inplane"
602
603        optional_logging = cl.nullcontext()
604        # Initial uniform distribution of orientations on a 2D circle.
605        initial_angles = np.mgrid[0 : 2 * np.pi : 360000j]
606        cos2θ = np.cos(2 * initial_angles)
607        if outdir is not None:
608            out_basepath = f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}"
609            optional_logging = _io.logfile_enable(f"{out_basepath}.log")
610
611        with optional_logging:
612            initial_orientations = Rotation.from_rotvec(
613                [[0, 0, θ] for θ in initial_angles]
614            )
615            orientations_diff, fractions_diff = _core.derivatives(
616                regime=_core.DeformationRegime.matrix_dislocation,
617                phase=_core.MineralPhase.olivine,
618                fabric=_core.MineralFabric.olivine_A,
619                n_grains=360000,
620                orientations=initial_orientations.as_matrix(),
621                fractions=np.full(360000, 1 / 360000),
622                strain_rate=np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]]),
623                velocity_gradient=np.array([[0, 0, 0], [2, 0, 0], [0, 0, 0]]),
624                deformation_gradient_spin=np.full((3, 3), np.nan),
625                stress_exponent=1.5,
626                deformation_exponent=3.5,
627                nucleation_efficiency=5,
628                gbm_mobility=125,
629                volume_fraction=1.0,
630            )
631            ρ = np.abs(cos2θ) ** (1.5 / 3.5)
632            # No need to sum over slip systems, only (010)[100] can activate in this
633            # 2D simple shear deformation geometry (ρₛ=ρ).
634            target_strain_energies = ρ * np.exp(-5 * ρ**2)
635            target_fractions_diff = np.array(
636                [  # df/dt* = - M* f (E - E_mean)
637                    -125 * 1 / 360000 * (E - np.mean(target_strain_energies))
638                    for E in target_strain_energies
639                ]
640            )
641
642        if outdir is not None:
643            fig = _vis.figure(figscale=(1, 4 / 3))
644            axs = fig.subplot_mosaic(  # Put the legend on a special axes.
645                [["legend"], ["spin"], ["growth"]],
646                gridspec_kw={"height_ratios": [0.2, 1, 1]},
647                sharex=True,
648            )
649            ax = axs["spin"]
650            xvals = np.rad2deg(initial_angles)
651            fig, ax, colors = _vis.spin(
652                ax,
653                xvals[::2500],
654                np.sqrt(
655                    [
656                        o[0, 0] ** 2 + o[0, 1] ** 2 + o[0, 2] ** 2
657                        for o in orientations_diff
658                    ][::2500]
659                ),
660                xvals,
661                1 + cos2θ,
662                shear_axis=90,
663            )
664            ax.get_legend().remove()
665            ax.label_outer()
666            ax2 = axs["growth"]
667            fig, ax2, colors = _vis.growth(
668                ax2,
669                xvals[::2500],
670                fractions_diff[::2500],
671                xvals,
672                target_fractions_diff,
673                shear_axis=90,
674            )
675            _utils.redraw_legend(
676                ax2,
677                legendax=axs["legend"],
678                loc="upper center",
679                ncols=3,
680                mode="expand",
681            )
682            _utils.add_subplot_labels(axs, labelmap={"spin": "a)", "growth": "b)"})
683            fig.savefig(f"{out_basepath}.pdf")
684
685        nt.assert_allclose(fractions_diff, target_fractions_diff, atol=1e-15, rtol=0)
686
687    def test_shear_dvdx_circle_shearplane(self, outdir):
688        r"""360000 grains of A-type olivine with uniform spread of a-axes on a circle.
689
690        Unlike `test_shear_dvdx_circle_inplane`, two slip systems are active here,
691        with cyclical variety in which one is dominant depending on grain orientation.
692        The a-axes are distributed in the YZ plane
693        (i.e.\ extrinsic rotation around Z by 90° and then around X).
694
695        Velocity gradient:
696        $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 2 & 0 & 0 \cr 0 & 0 & 0 \end{bmatrix}$$
697
698        """
699        test_id = "dvdx_circle_shearplane"
700
701        optional_logging = cl.nullcontext()
702        # Initial uniform distribution of orientations on a 2D circle.
703        initial_angles = np.mgrid[0 : 2 * np.pi : 360000j]
704        if outdir is not None:
705            out_basepath = f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}"
706            optional_logging = _io.logfile_enable(f"{out_basepath}.log")
707
708        with optional_logging:
709            initial_orientations = Rotation.from_euler(
710                "zx", [[np.pi / 2, θ] for θ in initial_angles]
711            )
712            orientations_diff, fractions_diff = _core.derivatives(
713                regime=_core.DeformationRegime.matrix_dislocation,
714                phase=_core.MineralPhase.olivine,
715                fabric=_core.MineralFabric.olivine_A,
716                n_grains=360000,
717                orientations=initial_orientations.as_matrix(),
718                fractions=np.full(360000, 1 / 360000),
719                strain_rate=np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]]),
720                velocity_gradient=np.array([[0, 0, 0], [2, 0, 0], [0, 0, 0]]),
721                deformation_gradient_spin=np.full((3, 3), np.nan),
722                stress_exponent=1.5,
723                deformation_exponent=3.5,
724                nucleation_efficiency=5,
725                gbm_mobility=125,
726                volume_fraction=1.0,
727            )
728
729        if outdir is not None:
730            fig = _vis.figure(figscale=(1, 4 / 3))
731            axs = fig.subplot_mosaic([["spin"], ["growth"]], sharex=True)
732            ax = axs["spin"]
733            xvals = np.rad2deg(initial_angles)
734            fig, ax, colors = _vis.spin(
735                ax,
736                xvals[::2500],
737                np.sqrt(
738                    [
739                        o[0, 0] ** 2 + o[0, 1] ** 2 + o[0, 2] ** 2
740                        for o in orientations_diff
741                    ][::2500]
742                ),
743            )
744            ax.get_legend().remove()
745            ax.label_outer()
746            ax2 = axs["growth"]
747            fig, ax2, colors = _vis.growth(ax2, xvals[::2500], fractions_diff[::2500])
748            _utils.add_subplot_labels(axs, labelmap={"spin": "a)", "growth": "b)"})
749            fig.savefig(f"{out_basepath}.pdf")
750
751        # Check dominant slip system every 1°.
752        for θ in initial_angles[::1000]:
753            slip_invariants = _core._get_slip_invariants(
754                np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]]),
755                Rotation.from_euler("zx", [np.pi / 2, θ]).as_matrix(),
756            )
757            θ = np.rad2deg(θ)
758            crss = _core.get_crss(
759                _core.MineralPhase.olivine,
760                _core.MineralFabric.olivine_A,
761            )
762            slip_indices = np.argsort(np.abs(slip_invariants / crss))
763            slip_system = _minerals.OLIVINE_SLIP_SYSTEMS[slip_indices[-1]]
764
765            if 0 <= θ < 64:
766                assert slip_system == ([0, 1, 0], [1, 0, 0])
767            elif 64 <= θ < 117:
768                assert slip_system == ([0, 0, 1], [1, 0, 0])
769            elif 117 <= θ < 244:
770                assert slip_system == ([0, 1, 0], [1, 0, 0])
771            elif 244 <= θ < 297:
772                assert slip_system == ([0, 0, 1], [1, 0, 0])
773            else:
774                assert slip_system == ([0, 1, 0], [1, 0, 0])
SUBDIR = 'core'
class TestDislocationCreepOPX:
 21class TestDislocationCreepOPX:
 22    """Single-grain orthopyroxene crystallographic rotation rate tests."""
 23
 24    class_id = "dislocation_creep_OPX"
 25
 26    @pytest.mark.skipif(_utils.in_ci("win32"), reason="Not equal to tolerance")
 27    def test_shear_dudz(self, outdir):
 28        test_id = "dudz"
 29        optional_logging = cl.nullcontext()
 30        if outdir is not None:
 31            optional_logging = _io.logfile_enable(
 32                f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.log"
 33            )
 34        with optional_logging:
 35            for θ in np.mgrid[0 : 2 * np.pi : 360j]:
 36                _log.debug("θ (°): %s", np.rad2deg(θ))
 37                orientations_diff, fractions_diff = _core.derivatives(
 38                    regime=_core.DeformationRegime.matrix_dislocation,
 39                    phase=_core.MineralPhase.enstatite,
 40                    fabric=_core.MineralFabric.enstatite_AB,
 41                    n_grains=1,
 42                    orientations=Rotation.from_rotvec([[0, θ, 0]]).as_matrix(),
 43                    fractions=np.array([1.0]),
 44                    strain_rate=np.array([[0, 0, 1], [0, 0, 0], [1, 0, 0]]),
 45                    velocity_gradient=np.array([[0, 0, 2], [0, 0, 0], [0, 0, 0]]),
 46                    deformation_gradient_spin=np.full((3, 3), np.nan),
 47                    stress_exponent=1.5,
 48                    deformation_exponent=3.5,
 49                    nucleation_efficiency=5,
 50                    gbm_mobility=0,
 51                    volume_fraction=1.0,
 52                )
 53                cosθ = np.cos(θ)
 54                cos2θ = np.cos(2 * θ)
 55                sinθ = np.sin(θ)
 56                target_orientations_diff = (1 + cos2θ) * np.array(
 57                    [[sinθ, 0, -cosθ], [0, 0, 0], [cosθ, 0, sinθ]]
 58                )
 59                np.testing.assert_allclose(
 60                    orientations_diff[0], target_orientations_diff
 61                )
 62                assert np.isclose(np.sum(fractions_diff), 0.0)
 63
 64    def test_shear_dvdx(self, outdir):
 65        test_id = "dvdx"
 66        optional_logging = cl.nullcontext()
 67        if outdir is not None:
 68            optional_logging = _io.logfile_enable(
 69                f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.log"
 70            )
 71        with optional_logging:
 72            for θ in np.linspace(0, 2 * np.pi, 361):
 73                _log.debug("θ (°): %s", np.rad2deg(θ))
 74                orientations = Rotation.from_rotvec([[0, 0, θ]]).as_matrix()
 75                deformation_rate = _core._get_deformation_rate(
 76                    _core.MineralPhase.enstatite,
 77                    orientations[0],
 78                    np.array([0, 0, 0, 0]),
 79                )
 80                np.testing.assert_allclose(deformation_rate.flatten(), np.zeros(9))
 81                orientations_diff, fractions_diff = _core.derivatives(
 82                    regime=_core.DeformationRegime.matrix_dislocation,
 83                    phase=_core.MineralPhase.enstatite,
 84                    fabric=_core.MineralFabric.enstatite_AB,
 85                    n_grains=1,
 86                    orientations=orientations,
 87                    fractions=np.array([1.0]),
 88                    strain_rate=np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]]),
 89                    velocity_gradient=np.array([[0, 0, 0], [2, 0, 0], [0, 0, 0]]),
 90                    deformation_gradient_spin=np.full((3, 3), np.nan),
 91                    stress_exponent=1.5,
 92                    deformation_exponent=3.5,
 93                    nucleation_efficiency=5,
 94                    gbm_mobility=0,
 95                    volume_fraction=1.0,
 96                )
 97                # Can't activate the (100)[001] slip system, no plastic deformation.
 98                # Only passive (rigid-body) rotation due to the velocity gradient.
 99                sinθ = np.sin(θ)
100                cosθ = np.cos(θ)
101                target_orientations_diff = np.array(
102                    [[sinθ, cosθ, 0], [-cosθ, sinθ, 0], [0, 0, 0]]
103                )
104                np.testing.assert_allclose(
105                    orientations_diff[0], target_orientations_diff, atol=1e-15
106                )
107                assert np.isclose(np.sum(fractions_diff), 0.0)

Single-grain orthopyroxene crystallographic rotation rate tests.

class_id = 'dislocation_creep_OPX'
@pytest.mark.skipif(_utils.in_ci('win32'), reason='Not equal to tolerance')
def test_shear_dudz(self, outdir):
26    @pytest.mark.skipif(_utils.in_ci("win32"), reason="Not equal to tolerance")
27    def test_shear_dudz(self, outdir):
28        test_id = "dudz"
29        optional_logging = cl.nullcontext()
30        if outdir is not None:
31            optional_logging = _io.logfile_enable(
32                f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.log"
33            )
34        with optional_logging:
35            for θ in np.mgrid[0 : 2 * np.pi : 360j]:
36                _log.debug("θ (°): %s", np.rad2deg(θ))
37                orientations_diff, fractions_diff = _core.derivatives(
38                    regime=_core.DeformationRegime.matrix_dislocation,
39                    phase=_core.MineralPhase.enstatite,
40                    fabric=_core.MineralFabric.enstatite_AB,
41                    n_grains=1,
42                    orientations=Rotation.from_rotvec([[0, θ, 0]]).as_matrix(),
43                    fractions=np.array([1.0]),
44                    strain_rate=np.array([[0, 0, 1], [0, 0, 0], [1, 0, 0]]),
45                    velocity_gradient=np.array([[0, 0, 2], [0, 0, 0], [0, 0, 0]]),
46                    deformation_gradient_spin=np.full((3, 3), np.nan),
47                    stress_exponent=1.5,
48                    deformation_exponent=3.5,
49                    nucleation_efficiency=5,
50                    gbm_mobility=0,
51                    volume_fraction=1.0,
52                )
53                cosθ = np.cos(θ)
54                cos2θ = np.cos(2 * θ)
55                sinθ = np.sin(θ)
56                target_orientations_diff = (1 + cos2θ) * np.array(
57                    [[sinθ, 0, -cosθ], [0, 0, 0], [cosθ, 0, sinθ]]
58                )
59                np.testing.assert_allclose(
60                    orientations_diff[0], target_orientations_diff
61                )
62                assert np.isclose(np.sum(fractions_diff), 0.0)
def test_shear_dvdx(self, outdir):
 64    def test_shear_dvdx(self, outdir):
 65        test_id = "dvdx"
 66        optional_logging = cl.nullcontext()
 67        if outdir is not None:
 68            optional_logging = _io.logfile_enable(
 69                f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.log"
 70            )
 71        with optional_logging:
 72            for θ in np.linspace(0, 2 * np.pi, 361):
 73                _log.debug("θ (°): %s", np.rad2deg(θ))
 74                orientations = Rotation.from_rotvec([[0, 0, θ]]).as_matrix()
 75                deformation_rate = _core._get_deformation_rate(
 76                    _core.MineralPhase.enstatite,
 77                    orientations[0],
 78                    np.array([0, 0, 0, 0]),
 79                )
 80                np.testing.assert_allclose(deformation_rate.flatten(), np.zeros(9))
 81                orientations_diff, fractions_diff = _core.derivatives(
 82                    regime=_core.DeformationRegime.matrix_dislocation,
 83                    phase=_core.MineralPhase.enstatite,
 84                    fabric=_core.MineralFabric.enstatite_AB,
 85                    n_grains=1,
 86                    orientations=orientations,
 87                    fractions=np.array([1.0]),
 88                    strain_rate=np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]]),
 89                    velocity_gradient=np.array([[0, 0, 0], [2, 0, 0], [0, 0, 0]]),
 90                    deformation_gradient_spin=np.full((3, 3), np.nan),
 91                    stress_exponent=1.5,
 92                    deformation_exponent=3.5,
 93                    nucleation_efficiency=5,
 94                    gbm_mobility=0,
 95                    volume_fraction=1.0,
 96                )
 97                # Can't activate the (100)[001] slip system, no plastic deformation.
 98                # Only passive (rigid-body) rotation due to the velocity gradient.
 99                sinθ = np.sin(θ)
100                cosθ = np.cos(θ)
101                target_orientations_diff = np.array(
102                    [[sinθ, cosθ, 0], [-cosθ, sinθ, 0], [0, 0, 0]]
103                )
104                np.testing.assert_allclose(
105                    orientations_diff[0], target_orientations_diff, atol=1e-15
106                )
107                assert np.isclose(np.sum(fractions_diff), 0.0)
class TestDislocationCreepOlivineA:
110class TestDislocationCreepOlivineA:
111    """Single-grain A-type olivine analytical rotation rate tests."""
112
113    class_id = "dislocation_creep_OlA"
114
115    @pytest.mark.skipif(_utils.in_ci("win32"), reason="Not equal to tolerance")
116    def test_shear_dvdx_slip_010_100(self, outdir):
117        r"""Single grain of A-type olivine, slip on (010)[100].
118
119        Velocity gradient:
120        $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 2 & 0 & 0 \cr 0 & 0 & 0 \end{bmatrix}$$
121
122        """
123        test_id = "dvdx_010_100"
124        nondim_velocity_gradient = np.array([[0, 0, 0], [2, 0, 0], [0, 0, 0]])
125        # Strain rate is 0.5*(L + Lᵀ).
126        nondim_strain_rate = np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]])
127
128        optional_logging = cl.nullcontext()
129        if outdir is not None:
130            optional_logging = _io.logfile_enable(
131                f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.log"
132            )
133            initial_angles = []
134            rotation_rates = []
135            target_rotation_rates = []
136
137        with optional_logging:
138            for θ in np.mgrid[0 : 2 * np.pi : 3600j]:
139                _log.debug("θ (°): %s", np.rad2deg(θ))
140                # Initial grain rotations around Z (anti-clockwise).
141                initial_orientations = Rotation.from_rotvec([[0, 0, θ]])
142                orientation_matrix = initial_orientations[0].as_matrix()
143                slip_invariants = _core._get_slip_invariants(
144                    nondim_strain_rate, orientation_matrix
145                )
146                _log.debug("slip invariants: %s", slip_invariants)
147
148                crss = _core.get_crss(
149                    _core.MineralPhase.olivine,
150                    _core.MineralFabric.olivine_A,
151                )
152                slip_indices = np.argsort(np.abs(slip_invariants / crss))
153                slip_system = _minerals.OLIVINE_SLIP_SYSTEMS[slip_indices[-1]]
154                assert slip_system == ([0, 1, 0], [1, 0, 0])
155
156                slip_rates = _core._get_slip_rates_olivine(
157                    slip_invariants, slip_indices, crss, 3.5
158                )
159                _log.debug("slip rates: %s", slip_rates)
160
161                deformation_rate = _core._get_deformation_rate(
162                    _core.MineralPhase.olivine,
163                    orientation_matrix,
164                    slip_rates,
165                )
166                _log.debug("deformation rate:\n%s", deformation_rate)
167
168                orientations_diff, fractions_diff = _core.derivatives(
169                    regime=_core.DeformationRegime.matrix_dislocation,
170                    phase=_core.MineralPhase.olivine,
171                    fabric=_core.MineralFabric.olivine_A,
172                    n_grains=1,
173                    orientations=initial_orientations.as_matrix(),
174                    fractions=np.array([1.0]),
175                    strain_rate=nondim_strain_rate,
176                    velocity_gradient=nondim_velocity_gradient,
177                    deformation_gradient_spin=np.full((3, 3), np.nan),
178                    stress_exponent=1.5,
179                    deformation_exponent=3.5,
180                    nucleation_efficiency=5,
181                    gbm_mobility=0,
182                    volume_fraction=1.0,
183                )
184                cosθ = np.cos(θ)
185                cos2θ = np.cos(2 * θ)
186                sinθ = np.sin(θ)
187                target_orientations_diff = np.array(
188                    [
189                        [sinθ * (1 + cos2θ), cosθ * (1 + cos2θ), 0],
190                        [-cosθ * (1 + cos2θ), sinθ * (1 + cos2θ), 0],
191                        [0, 0, 0],
192                    ]
193                )
194                np.testing.assert_allclose(
195                    orientations_diff[0], target_orientations_diff
196                )
197                if outdir is not None:
198                    initial_angles.append(np.rad2deg(θ))
199                    rotation_rates.append(
200                        np.sqrt(
201                            orientations_diff[0][0, 0] ** 2
202                            + orientations_diff[0][0, 1] ** 2
203                        )
204                    )
205                    target_rotation_rates.append(1 + cos2θ)
206                assert np.isclose(np.sum(fractions_diff), 0.0)
207
208        if outdir is not None:
209            fig = _vis.figure(figscale=(1, 2 / 3))
210            axs = fig.subplot_mosaic(  # Put the legend on a special axes.
211                [["legend"], ["spin"]], gridspec_kw={"height_ratios": [0.1, 1]}
212            )
213            ax = axs["spin"]
214            fig, ax, colors = _vis.spin(
215                ax,
216                initial_angles[::25],
217                rotation_rates[::25],
218                initial_angles,
219                target_rotation_rates,
220                shear_axis=90,
221            )
222            _utils.redraw_legend(
223                ax,
224                legendax=axs["legend"],
225                loc="upper center",
226                ncols=3,
227                mode="expand",
228            )
229            fig.savefig(
230                _io.resolve_path(f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.pdf")
231            )
232
233    def test_shear_dudz_slip_001_100(self, outdir):
234        r"""Single grain of A-type olivine, slip on (001)[100].
235
236        Velocity gradient:
237        $$\bm{L} = \begin{bmatrix} 0 & 0 & 2 \cr 0 & 0 & 0 \cr 0 & 0 & 0 \end{bmatrix}$$
238
239        """
240        test_id = "dudz_001_100"
241        nondim_velocity_gradient = np.array([[0, 0, 2], [0, 0, 0], [0, 0, 0]])
242        # Strain rate is 0.5*(L + Lᵀ).
243        nondim_strain_rate = np.array([[0, 0, 1], [0, 0, 0], [1, 0, 0]])
244
245        optional_logging = cl.nullcontext()
246        if outdir is not None:
247            optional_logging = _io.logfile_enable(
248                f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.log"
249            )
250            initial_angles = []
251            rotation_rates = []
252            target_rotation_rates = []
253
254        with optional_logging:
255            for θ in np.mgrid[0 : 2 * np.pi : 360j]:
256                _log.debug("θ (°): %s", np.rad2deg(θ))
257                # Initial grain rotations around Y (anti-clockwise).
258                initial_orientations = Rotation.from_rotvec([[0, θ, 0]])
259                orientation_matrix = initial_orientations[0].as_matrix()
260                slip_invariants = _core._get_slip_invariants(
261                    nondim_strain_rate, orientation_matrix
262                )
263                _log.debug("slip invariants: %s", slip_invariants)
264
265                crss = _core.get_crss(
266                    _core.MineralPhase.olivine,
267                    _core.MineralFabric.olivine_A,
268                )
269                slip_indices = np.argsort(np.abs(slip_invariants / crss))
270                slip_system = _minerals.OLIVINE_SLIP_SYSTEMS[slip_indices[-1]]
271                assert slip_system == ([0, 0, 1], [1, 0, 0])
272
273                slip_rates = _core._get_slip_rates_olivine(
274                    slip_invariants, slip_indices, crss, 3.5
275                )
276                _log.debug("slip rates: %s", slip_rates)
277
278                deformation_rate = _core._get_deformation_rate(
279                    _core.MineralPhase.olivine,
280                    orientation_matrix,
281                    slip_rates,
282                )
283                _log.debug("deformation rate:\n%s", deformation_rate)
284
285                orientations_diff, fractions_diff = _core.derivatives(
286                    regime=_core.DeformationRegime.matrix_dislocation,
287                    phase=_core.MineralPhase.olivine,
288                    fabric=_core.MineralFabric.olivine_A,
289                    n_grains=1,
290                    orientations=initial_orientations.as_matrix(),
291                    fractions=np.array([1.0]),
292                    strain_rate=nondim_strain_rate,
293                    velocity_gradient=nondim_velocity_gradient,
294                    deformation_gradient_spin=np.full((3, 3), np.nan),
295                    stress_exponent=1.5,
296                    deformation_exponent=3.5,
297                    nucleation_efficiency=5,
298                    gbm_mobility=0,
299                    volume_fraction=1.0,
300                )
301                cosθ = np.cos(θ)
302                cos2θ = np.cos(2 * θ)
303                sinθ = np.sin(θ)
304                target_orientations_diff = np.array(
305                    [
306                        [-sinθ * (cos2θ - 1), 0, cosθ * (cos2θ - 1)],
307                        [0, 0, 0],
308                        [cosθ * (1 - cos2θ), 0, sinθ * (1 - cos2θ)],
309                    ]
310                )
311                np.testing.assert_allclose(
312                    orientations_diff[0], target_orientations_diff
313                )
314                if outdir is not None:
315                    initial_angles.append(np.rad2deg(θ))
316                    rotation_rates.append(
317                        np.sqrt(
318                            orientations_diff[0][0, 0] ** 2
319                            + orientations_diff[0][0, 2] ** 2
320                        )
321                    )
322                    target_rotation_rates.append(1 - cos2θ)
323                assert np.isclose(np.sum(fractions_diff), 0.0)
324
325        if outdir is not None:
326            fig = _vis.figure(figscale=(1, 2 / 3))
327            axs = fig.subplot_mosaic(  # Put the legend on a special axes.
328                [["legend"], ["spin"]], gridspec_kw={"height_ratios": [0.1, 1]}
329            )
330            ax = axs["spin"]
331            fig, ax, colors = _vis.spin(
332                ax,
333                initial_angles[::5],
334                rotation_rates[::5],
335                initial_angles,
336                target_rotation_rates,
337                shear_axis=0,
338            )
339            _utils.redraw_legend(
340                ax,
341                legendax=axs["legend"],
342                loc="upper center",
343                ncols=3,
344                mode="expand",
345            )
346            fig.savefig(
347                _io.resolve_path(f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.pdf")
348            )
349
350    @pytest.mark.skipif(_utils.in_ci("win32"), reason="Not equal to tolerance")
351    def test_shear_dwdx_slip_001_100(self, outdir):
352        r"""Single grain of A-type olivine, slip on (001)[100].
353
354        Velocity gradient:
355        $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 0 & 0 & 0 \cr 2 & 0 & 0 \end{bmatrix}$$
356
357        """
358        test_id = "dwdx_001_100"
359        nondim_velocity_gradient = np.array([[0, 0, 0], [0, 0, 0], [2, 0, 0]])
360        # Strain rate is 0.5*(L + Lᵀ).
361        nondim_strain_rate = np.array([[0, 0, 1], [0, 0, 0], [1, 0, 0]])
362
363        optional_logging = cl.nullcontext()
364        if outdir is not None:
365            optional_logging = _io.logfile_enable(
366                f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.log"
367            )
368            initial_angles = []
369            rotation_rates = []
370            target_rotation_rates = []
371
372        with optional_logging:
373            for θ in np.mgrid[0 : 2 * np.pi : 360j]:
374                _log.debug("θ (°): %s", np.rad2deg(θ))
375                # Initial grain rotations around Y (anti-clockwise).
376                initial_orientations = Rotation.from_rotvec([[0, θ, 0]])
377                orientation_matrix = initial_orientations[0].as_matrix()
378                slip_invariants = _core._get_slip_invariants(
379                    nondim_strain_rate, orientation_matrix
380                )
381                _log.debug("slip invariants: %s", slip_invariants)
382
383                crss = _core.get_crss(
384                    _core.MineralPhase.olivine,
385                    _core.MineralFabric.olivine_A,
386                )
387                slip_indices = np.argsort(np.abs(slip_invariants / crss))
388                slip_system = _minerals.OLIVINE_SLIP_SYSTEMS[slip_indices[-1]]
389                assert slip_system == ([0, 0, 1], [1, 0, 0])
390
391                slip_rates = _core._get_slip_rates_olivine(
392                    slip_invariants, slip_indices, crss, 3.5
393                )
394                _log.debug("slip rates: %s", slip_rates)
395
396                deformation_rate = _core._get_deformation_rate(
397                    _core.MineralPhase.olivine,
398                    orientation_matrix,
399                    slip_rates,
400                )
401                _log.debug("deformation rate:\n%s", deformation_rate)
402
403                orientations_diff, fractions_diff = _core.derivatives(
404                    regime=_core.DeformationRegime.matrix_dislocation,
405                    phase=_core.MineralPhase.olivine,
406                    fabric=_core.MineralFabric.olivine_A,
407                    n_grains=1,
408                    orientations=initial_orientations.as_matrix(),
409                    fractions=np.array([1.0]),
410                    strain_rate=nondim_strain_rate,
411                    velocity_gradient=nondim_velocity_gradient,
412                    deformation_gradient_spin=np.full((3, 3), np.nan),
413                    stress_exponent=1.5,
414                    deformation_exponent=3.5,
415                    nucleation_efficiency=5,
416                    gbm_mobility=0,
417                    volume_fraction=1.0,
418                )
419                cosθ = np.cos(θ)
420                cos2θ = np.cos(2 * θ)
421                sinθ = np.sin(θ)
422                target_orientations_diff = np.array(
423                    [
424                        [-sinθ * (1 + cos2θ), 0, cosθ * (1 + cos2θ)],
425                        [0, 0, 0],
426                        [-cosθ * (1 + cos2θ), 0, -sinθ * (1 + cos2θ)],
427                    ]
428                )
429                np.testing.assert_allclose(
430                    orientations_diff[0], target_orientations_diff
431                )
432                if outdir is not None:
433                    initial_angles.append(np.rad2deg(θ))
434                    rotation_rates.append(
435                        np.sqrt(
436                            orientations_diff[0][0, 0] ** 2
437                            + orientations_diff[0][0, 2] ** 2
438                        )
439                    )
440                    target_rotation_rates.append(1 + cos2θ)
441                assert np.isclose(np.sum(fractions_diff), 0.0)
442
443        if outdir is not None:
444            fig = _vis.figure(figscale=(1, 2 / 3))
445            axs = fig.subplot_mosaic(  # Put the legend on a special axes.
446                [["legend"], ["spin"]], gridspec_kw={"height_ratios": [0.1, 1]}
447            )
448            ax = axs["spin"]
449            fig, ax, colors = _vis.spin(
450                ax,
451                initial_angles[::5],
452                rotation_rates[::5],
453                initial_angles,
454                target_rotation_rates,
455                shear_axis=90,
456            )
457            _utils.redraw_legend(
458                ax,
459                legendax=axs["legend"],
460                loc="upper center",
461                ncols=3,
462                mode="expand",
463            )
464            fig.savefig(
465                _io.resolve_path(f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.pdf")
466            )
467
468    @pytest.mark.skipif(_utils.in_ci("win32"), reason="Not equal to tolerance")
469    def test_shear_dvdz_slip_010_001(self, outdir):
470        r"""Single grain of A-type olivine, slip on (010)[001].
471
472        Velocity gradient:
473        $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 0 & 0 & 2 \cr 0 & 0 & 0 \end{bmatrix}$$
474
475        """
476        test_id = "dvdz_010_001"
477        nondim_velocity_gradient = np.array([[0, 0, 0], [0, 0, 2], [0, 0, 0]])
478        # Strain rate is 0.5*(L + Lᵀ).
479        nondim_strain_rate = np.array([[0, 0, 0], [0, 0, 1], [0, 1, 0]])
480
481        optional_logging = cl.nullcontext()
482        if outdir is not None:
483            optional_logging = _io.logfile_enable(
484                f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.log"
485            )
486            initial_angles = []
487            rotation_rates = []
488            target_rotation_rates = []
489
490        with optional_logging:
491            for θ in np.mgrid[0 : 2 * np.pi : 360j]:
492                _log.debug("θ (°): %s", np.rad2deg(θ))
493                # Initial grain rotations around X (anti-clockwise).
494                initial_orientations = Rotation.from_rotvec([[θ, 0, 0]])
495                orientation_matrix = initial_orientations[0].as_matrix()
496                slip_invariants = _core._get_slip_invariants(
497                    nondim_strain_rate, orientation_matrix
498                )
499                _log.debug("slip invariants: %s", slip_invariants)
500
501                crss = _core.get_crss(
502                    _core.MineralPhase.olivine,
503                    _core.MineralFabric.olivine_A,
504                )
505                slip_indices = np.argsort(np.abs(slip_invariants / crss))
506                slip_system = _minerals.OLIVINE_SLIP_SYSTEMS[slip_indices[-1]]
507                assert slip_system == ([0, 1, 0], [0, 0, 1])
508
509                slip_rates = _core._get_slip_rates_olivine(
510                    slip_invariants, slip_indices, crss, 3.5
511                )
512                _log.debug("slip rates: %s", slip_rates)
513
514                deformation_rate = _core._get_deformation_rate(
515                    _core.MineralPhase.olivine,
516                    orientation_matrix,
517                    slip_rates,
518                )
519                _log.debug("deformation rate:\n%s", deformation_rate)
520
521                orientations_diff, fractions_diff = _core.derivatives(
522                    regime=_core.DeformationRegime.matrix_dislocation,
523                    phase=_core.MineralPhase.olivine,
524                    fabric=_core.MineralFabric.olivine_A,
525                    n_grains=1,
526                    orientations=initial_orientations.as_matrix(),
527                    fractions=np.array([1.0]),
528                    strain_rate=nondim_strain_rate,
529                    velocity_gradient=nondim_velocity_gradient,
530                    deformation_gradient_spin=np.full((3, 3), np.nan),
531                    stress_exponent=1.5,
532                    deformation_exponent=3.5,
533                    nucleation_efficiency=5,
534                    gbm_mobility=0,
535                    volume_fraction=1.0,
536                )
537                cosθ = np.cos(θ)
538                cos2θ = np.cos(2 * θ)
539                sinθ = np.sin(θ)
540                target_orientations_diff = np.array(
541                    [
542                        [0, 0, 0],
543                        [0, -sinθ * (1 + cos2θ), -cosθ * (1 + cos2θ)],
544                        [0, cosθ * (1 + cos2θ), -sinθ * (1 + cos2θ)],
545                    ]
546                )
547                np.testing.assert_allclose(
548                    orientations_diff[0], target_orientations_diff
549                )
550                if outdir is not None:
551                    initial_angles.append(np.rad2deg(θ))
552                    rotation_rates.append(
553                        np.sqrt(
554                            orientations_diff[0][1, 1] ** 2
555                            + orientations_diff[0][1, 2] ** 2
556                        )
557                    )
558                    target_rotation_rates.append(1 + cos2θ)
559                assert np.isclose(np.sum(fractions_diff), 0.0)
560
561        if outdir is not None:
562            fig = _vis.figure(figscale=(1, 2 / 3))
563            axs = fig.subplot_mosaic(  # Put the legend on a special axes.
564                [["legend"], ["spin"]], gridspec_kw={"height_ratios": [0.1, 1]}
565            )
566            ax = axs["spin"]
567            fig, ax, colors = _vis.spin(
568                ax,
569                initial_angles[::5],
570                rotation_rates[::5],
571                initial_angles,
572                target_rotation_rates,
573                shear_axis=90,
574            )
575            _utils.redraw_legend(
576                ax,
577                legendax=axs["legend"],
578                loc="upper center",
579                ncols=3,
580                mode="expand",
581            )
582            fig.savefig(
583                _io.resolve_path(f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.pdf")
584            )

Single-grain A-type olivine analytical rotation rate tests.

class_id = 'dislocation_creep_OlA'
@pytest.mark.skipif(_utils.in_ci('win32'), reason='Not equal to tolerance')
def test_shear_dvdx_slip_010_100(self, outdir):
115    @pytest.mark.skipif(_utils.in_ci("win32"), reason="Not equal to tolerance")
116    def test_shear_dvdx_slip_010_100(self, outdir):
117        r"""Single grain of A-type olivine, slip on (010)[100].
118
119        Velocity gradient:
120        $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 2 & 0 & 0 \cr 0 & 0 & 0 \end{bmatrix}$$
121
122        """
123        test_id = "dvdx_010_100"
124        nondim_velocity_gradient = np.array([[0, 0, 0], [2, 0, 0], [0, 0, 0]])
125        # Strain rate is 0.5*(L + Lᵀ).
126        nondim_strain_rate = np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]])
127
128        optional_logging = cl.nullcontext()
129        if outdir is not None:
130            optional_logging = _io.logfile_enable(
131                f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.log"
132            )
133            initial_angles = []
134            rotation_rates = []
135            target_rotation_rates = []
136
137        with optional_logging:
138            for θ in np.mgrid[0 : 2 * np.pi : 3600j]:
139                _log.debug("θ (°): %s", np.rad2deg(θ))
140                # Initial grain rotations around Z (anti-clockwise).
141                initial_orientations = Rotation.from_rotvec([[0, 0, θ]])
142                orientation_matrix = initial_orientations[0].as_matrix()
143                slip_invariants = _core._get_slip_invariants(
144                    nondim_strain_rate, orientation_matrix
145                )
146                _log.debug("slip invariants: %s", slip_invariants)
147
148                crss = _core.get_crss(
149                    _core.MineralPhase.olivine,
150                    _core.MineralFabric.olivine_A,
151                )
152                slip_indices = np.argsort(np.abs(slip_invariants / crss))
153                slip_system = _minerals.OLIVINE_SLIP_SYSTEMS[slip_indices[-1]]
154                assert slip_system == ([0, 1, 0], [1, 0, 0])
155
156                slip_rates = _core._get_slip_rates_olivine(
157                    slip_invariants, slip_indices, crss, 3.5
158                )
159                _log.debug("slip rates: %s", slip_rates)
160
161                deformation_rate = _core._get_deformation_rate(
162                    _core.MineralPhase.olivine,
163                    orientation_matrix,
164                    slip_rates,
165                )
166                _log.debug("deformation rate:\n%s", deformation_rate)
167
168                orientations_diff, fractions_diff = _core.derivatives(
169                    regime=_core.DeformationRegime.matrix_dislocation,
170                    phase=_core.MineralPhase.olivine,
171                    fabric=_core.MineralFabric.olivine_A,
172                    n_grains=1,
173                    orientations=initial_orientations.as_matrix(),
174                    fractions=np.array([1.0]),
175                    strain_rate=nondim_strain_rate,
176                    velocity_gradient=nondim_velocity_gradient,
177                    deformation_gradient_spin=np.full((3, 3), np.nan),
178                    stress_exponent=1.5,
179                    deformation_exponent=3.5,
180                    nucleation_efficiency=5,
181                    gbm_mobility=0,
182                    volume_fraction=1.0,
183                )
184                cosθ = np.cos(θ)
185                cos2θ = np.cos(2 * θ)
186                sinθ = np.sin(θ)
187                target_orientations_diff = np.array(
188                    [
189                        [sinθ * (1 + cos2θ), cosθ * (1 + cos2θ), 0],
190                        [-cosθ * (1 + cos2θ), sinθ * (1 + cos2θ), 0],
191                        [0, 0, 0],
192                    ]
193                )
194                np.testing.assert_allclose(
195                    orientations_diff[0], target_orientations_diff
196                )
197                if outdir is not None:
198                    initial_angles.append(np.rad2deg(θ))
199                    rotation_rates.append(
200                        np.sqrt(
201                            orientations_diff[0][0, 0] ** 2
202                            + orientations_diff[0][0, 1] ** 2
203                        )
204                    )
205                    target_rotation_rates.append(1 + cos2θ)
206                assert np.isclose(np.sum(fractions_diff), 0.0)
207
208        if outdir is not None:
209            fig = _vis.figure(figscale=(1, 2 / 3))
210            axs = fig.subplot_mosaic(  # Put the legend on a special axes.
211                [["legend"], ["spin"]], gridspec_kw={"height_ratios": [0.1, 1]}
212            )
213            ax = axs["spin"]
214            fig, ax, colors = _vis.spin(
215                ax,
216                initial_angles[::25],
217                rotation_rates[::25],
218                initial_angles,
219                target_rotation_rates,
220                shear_axis=90,
221            )
222            _utils.redraw_legend(
223                ax,
224                legendax=axs["legend"],
225                loc="upper center",
226                ncols=3,
227                mode="expand",
228            )
229            fig.savefig(
230                _io.resolve_path(f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.pdf")
231            )

Single grain of A-type olivine, slip on (010)[100].

Velocity gradient: $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 2 & 0 & 0 \cr 0 & 0 & 0 \end{bmatrix}$$

def test_shear_dudz_slip_001_100(self, outdir):
233    def test_shear_dudz_slip_001_100(self, outdir):
234        r"""Single grain of A-type olivine, slip on (001)[100].
235
236        Velocity gradient:
237        $$\bm{L} = \begin{bmatrix} 0 & 0 & 2 \cr 0 & 0 & 0 \cr 0 & 0 & 0 \end{bmatrix}$$
238
239        """
240        test_id = "dudz_001_100"
241        nondim_velocity_gradient = np.array([[0, 0, 2], [0, 0, 0], [0, 0, 0]])
242        # Strain rate is 0.5*(L + Lᵀ).
243        nondim_strain_rate = np.array([[0, 0, 1], [0, 0, 0], [1, 0, 0]])
244
245        optional_logging = cl.nullcontext()
246        if outdir is not None:
247            optional_logging = _io.logfile_enable(
248                f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.log"
249            )
250            initial_angles = []
251            rotation_rates = []
252            target_rotation_rates = []
253
254        with optional_logging:
255            for θ in np.mgrid[0 : 2 * np.pi : 360j]:
256                _log.debug("θ (°): %s", np.rad2deg(θ))
257                # Initial grain rotations around Y (anti-clockwise).
258                initial_orientations = Rotation.from_rotvec([[0, θ, 0]])
259                orientation_matrix = initial_orientations[0].as_matrix()
260                slip_invariants = _core._get_slip_invariants(
261                    nondim_strain_rate, orientation_matrix
262                )
263                _log.debug("slip invariants: %s", slip_invariants)
264
265                crss = _core.get_crss(
266                    _core.MineralPhase.olivine,
267                    _core.MineralFabric.olivine_A,
268                )
269                slip_indices = np.argsort(np.abs(slip_invariants / crss))
270                slip_system = _minerals.OLIVINE_SLIP_SYSTEMS[slip_indices[-1]]
271                assert slip_system == ([0, 0, 1], [1, 0, 0])
272
273                slip_rates = _core._get_slip_rates_olivine(
274                    slip_invariants, slip_indices, crss, 3.5
275                )
276                _log.debug("slip rates: %s", slip_rates)
277
278                deformation_rate = _core._get_deformation_rate(
279                    _core.MineralPhase.olivine,
280                    orientation_matrix,
281                    slip_rates,
282                )
283                _log.debug("deformation rate:\n%s", deformation_rate)
284
285                orientations_diff, fractions_diff = _core.derivatives(
286                    regime=_core.DeformationRegime.matrix_dislocation,
287                    phase=_core.MineralPhase.olivine,
288                    fabric=_core.MineralFabric.olivine_A,
289                    n_grains=1,
290                    orientations=initial_orientations.as_matrix(),
291                    fractions=np.array([1.0]),
292                    strain_rate=nondim_strain_rate,
293                    velocity_gradient=nondim_velocity_gradient,
294                    deformation_gradient_spin=np.full((3, 3), np.nan),
295                    stress_exponent=1.5,
296                    deformation_exponent=3.5,
297                    nucleation_efficiency=5,
298                    gbm_mobility=0,
299                    volume_fraction=1.0,
300                )
301                cosθ = np.cos(θ)
302                cos2θ = np.cos(2 * θ)
303                sinθ = np.sin(θ)
304                target_orientations_diff = np.array(
305                    [
306                        [-sinθ * (cos2θ - 1), 0, cosθ * (cos2θ - 1)],
307                        [0, 0, 0],
308                        [cosθ * (1 - cos2θ), 0, sinθ * (1 - cos2θ)],
309                    ]
310                )
311                np.testing.assert_allclose(
312                    orientations_diff[0], target_orientations_diff
313                )
314                if outdir is not None:
315                    initial_angles.append(np.rad2deg(θ))
316                    rotation_rates.append(
317                        np.sqrt(
318                            orientations_diff[0][0, 0] ** 2
319                            + orientations_diff[0][0, 2] ** 2
320                        )
321                    )
322                    target_rotation_rates.append(1 - cos2θ)
323                assert np.isclose(np.sum(fractions_diff), 0.0)
324
325        if outdir is not None:
326            fig = _vis.figure(figscale=(1, 2 / 3))
327            axs = fig.subplot_mosaic(  # Put the legend on a special axes.
328                [["legend"], ["spin"]], gridspec_kw={"height_ratios": [0.1, 1]}
329            )
330            ax = axs["spin"]
331            fig, ax, colors = _vis.spin(
332                ax,
333                initial_angles[::5],
334                rotation_rates[::5],
335                initial_angles,
336                target_rotation_rates,
337                shear_axis=0,
338            )
339            _utils.redraw_legend(
340                ax,
341                legendax=axs["legend"],
342                loc="upper center",
343                ncols=3,
344                mode="expand",
345            )
346            fig.savefig(
347                _io.resolve_path(f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.pdf")
348            )

Single grain of A-type olivine, slip on (001)[100].

Velocity gradient: $$\bm{L} = \begin{bmatrix} 0 & 0 & 2 \cr 0 & 0 & 0 \cr 0 & 0 & 0 \end{bmatrix}$$

@pytest.mark.skipif(_utils.in_ci('win32'), reason='Not equal to tolerance')
def test_shear_dwdx_slip_001_100(self, outdir):
350    @pytest.mark.skipif(_utils.in_ci("win32"), reason="Not equal to tolerance")
351    def test_shear_dwdx_slip_001_100(self, outdir):
352        r"""Single grain of A-type olivine, slip on (001)[100].
353
354        Velocity gradient:
355        $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 0 & 0 & 0 \cr 2 & 0 & 0 \end{bmatrix}$$
356
357        """
358        test_id = "dwdx_001_100"
359        nondim_velocity_gradient = np.array([[0, 0, 0], [0, 0, 0], [2, 0, 0]])
360        # Strain rate is 0.5*(L + Lᵀ).
361        nondim_strain_rate = np.array([[0, 0, 1], [0, 0, 0], [1, 0, 0]])
362
363        optional_logging = cl.nullcontext()
364        if outdir is not None:
365            optional_logging = _io.logfile_enable(
366                f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.log"
367            )
368            initial_angles = []
369            rotation_rates = []
370            target_rotation_rates = []
371
372        with optional_logging:
373            for θ in np.mgrid[0 : 2 * np.pi : 360j]:
374                _log.debug("θ (°): %s", np.rad2deg(θ))
375                # Initial grain rotations around Y (anti-clockwise).
376                initial_orientations = Rotation.from_rotvec([[0, θ, 0]])
377                orientation_matrix = initial_orientations[0].as_matrix()
378                slip_invariants = _core._get_slip_invariants(
379                    nondim_strain_rate, orientation_matrix
380                )
381                _log.debug("slip invariants: %s", slip_invariants)
382
383                crss = _core.get_crss(
384                    _core.MineralPhase.olivine,
385                    _core.MineralFabric.olivine_A,
386                )
387                slip_indices = np.argsort(np.abs(slip_invariants / crss))
388                slip_system = _minerals.OLIVINE_SLIP_SYSTEMS[slip_indices[-1]]
389                assert slip_system == ([0, 0, 1], [1, 0, 0])
390
391                slip_rates = _core._get_slip_rates_olivine(
392                    slip_invariants, slip_indices, crss, 3.5
393                )
394                _log.debug("slip rates: %s", slip_rates)
395
396                deformation_rate = _core._get_deformation_rate(
397                    _core.MineralPhase.olivine,
398                    orientation_matrix,
399                    slip_rates,
400                )
401                _log.debug("deformation rate:\n%s", deformation_rate)
402
403                orientations_diff, fractions_diff = _core.derivatives(
404                    regime=_core.DeformationRegime.matrix_dislocation,
405                    phase=_core.MineralPhase.olivine,
406                    fabric=_core.MineralFabric.olivine_A,
407                    n_grains=1,
408                    orientations=initial_orientations.as_matrix(),
409                    fractions=np.array([1.0]),
410                    strain_rate=nondim_strain_rate,
411                    velocity_gradient=nondim_velocity_gradient,
412                    deformation_gradient_spin=np.full((3, 3), np.nan),
413                    stress_exponent=1.5,
414                    deformation_exponent=3.5,
415                    nucleation_efficiency=5,
416                    gbm_mobility=0,
417                    volume_fraction=1.0,
418                )
419                cosθ = np.cos(θ)
420                cos2θ = np.cos(2 * θ)
421                sinθ = np.sin(θ)
422                target_orientations_diff = np.array(
423                    [
424                        [-sinθ * (1 + cos2θ), 0, cosθ * (1 + cos2θ)],
425                        [0, 0, 0],
426                        [-cosθ * (1 + cos2θ), 0, -sinθ * (1 + cos2θ)],
427                    ]
428                )
429                np.testing.assert_allclose(
430                    orientations_diff[0], target_orientations_diff
431                )
432                if outdir is not None:
433                    initial_angles.append(np.rad2deg(θ))
434                    rotation_rates.append(
435                        np.sqrt(
436                            orientations_diff[0][0, 0] ** 2
437                            + orientations_diff[0][0, 2] ** 2
438                        )
439                    )
440                    target_rotation_rates.append(1 + cos2θ)
441                assert np.isclose(np.sum(fractions_diff), 0.0)
442
443        if outdir is not None:
444            fig = _vis.figure(figscale=(1, 2 / 3))
445            axs = fig.subplot_mosaic(  # Put the legend on a special axes.
446                [["legend"], ["spin"]], gridspec_kw={"height_ratios": [0.1, 1]}
447            )
448            ax = axs["spin"]
449            fig, ax, colors = _vis.spin(
450                ax,
451                initial_angles[::5],
452                rotation_rates[::5],
453                initial_angles,
454                target_rotation_rates,
455                shear_axis=90,
456            )
457            _utils.redraw_legend(
458                ax,
459                legendax=axs["legend"],
460                loc="upper center",
461                ncols=3,
462                mode="expand",
463            )
464            fig.savefig(
465                _io.resolve_path(f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.pdf")
466            )

Single grain of A-type olivine, slip on (001)[100].

Velocity gradient: $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 0 & 0 & 0 \cr 2 & 0 & 0 \end{bmatrix}$$

@pytest.mark.skipif(_utils.in_ci('win32'), reason='Not equal to tolerance')
def test_shear_dvdz_slip_010_001(self, outdir):
468    @pytest.mark.skipif(_utils.in_ci("win32"), reason="Not equal to tolerance")
469    def test_shear_dvdz_slip_010_001(self, outdir):
470        r"""Single grain of A-type olivine, slip on (010)[001].
471
472        Velocity gradient:
473        $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 0 & 0 & 2 \cr 0 & 0 & 0 \end{bmatrix}$$
474
475        """
476        test_id = "dvdz_010_001"
477        nondim_velocity_gradient = np.array([[0, 0, 0], [0, 0, 2], [0, 0, 0]])
478        # Strain rate is 0.5*(L + Lᵀ).
479        nondim_strain_rate = np.array([[0, 0, 0], [0, 0, 1], [0, 1, 0]])
480
481        optional_logging = cl.nullcontext()
482        if outdir is not None:
483            optional_logging = _io.logfile_enable(
484                f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.log"
485            )
486            initial_angles = []
487            rotation_rates = []
488            target_rotation_rates = []
489
490        with optional_logging:
491            for θ in np.mgrid[0 : 2 * np.pi : 360j]:
492                _log.debug("θ (°): %s", np.rad2deg(θ))
493                # Initial grain rotations around X (anti-clockwise).
494                initial_orientations = Rotation.from_rotvec([[θ, 0, 0]])
495                orientation_matrix = initial_orientations[0].as_matrix()
496                slip_invariants = _core._get_slip_invariants(
497                    nondim_strain_rate, orientation_matrix
498                )
499                _log.debug("slip invariants: %s", slip_invariants)
500
501                crss = _core.get_crss(
502                    _core.MineralPhase.olivine,
503                    _core.MineralFabric.olivine_A,
504                )
505                slip_indices = np.argsort(np.abs(slip_invariants / crss))
506                slip_system = _minerals.OLIVINE_SLIP_SYSTEMS[slip_indices[-1]]
507                assert slip_system == ([0, 1, 0], [0, 0, 1])
508
509                slip_rates = _core._get_slip_rates_olivine(
510                    slip_invariants, slip_indices, crss, 3.5
511                )
512                _log.debug("slip rates: %s", slip_rates)
513
514                deformation_rate = _core._get_deformation_rate(
515                    _core.MineralPhase.olivine,
516                    orientation_matrix,
517                    slip_rates,
518                )
519                _log.debug("deformation rate:\n%s", deformation_rate)
520
521                orientations_diff, fractions_diff = _core.derivatives(
522                    regime=_core.DeformationRegime.matrix_dislocation,
523                    phase=_core.MineralPhase.olivine,
524                    fabric=_core.MineralFabric.olivine_A,
525                    n_grains=1,
526                    orientations=initial_orientations.as_matrix(),
527                    fractions=np.array([1.0]),
528                    strain_rate=nondim_strain_rate,
529                    velocity_gradient=nondim_velocity_gradient,
530                    deformation_gradient_spin=np.full((3, 3), np.nan),
531                    stress_exponent=1.5,
532                    deformation_exponent=3.5,
533                    nucleation_efficiency=5,
534                    gbm_mobility=0,
535                    volume_fraction=1.0,
536                )
537                cosθ = np.cos(θ)
538                cos2θ = np.cos(2 * θ)
539                sinθ = np.sin(θ)
540                target_orientations_diff = np.array(
541                    [
542                        [0, 0, 0],
543                        [0, -sinθ * (1 + cos2θ), -cosθ * (1 + cos2θ)],
544                        [0, cosθ * (1 + cos2θ), -sinθ * (1 + cos2θ)],
545                    ]
546                )
547                np.testing.assert_allclose(
548                    orientations_diff[0], target_orientations_diff
549                )
550                if outdir is not None:
551                    initial_angles.append(np.rad2deg(θ))
552                    rotation_rates.append(
553                        np.sqrt(
554                            orientations_diff[0][1, 1] ** 2
555                            + orientations_diff[0][1, 2] ** 2
556                        )
557                    )
558                    target_rotation_rates.append(1 + cos2θ)
559                assert np.isclose(np.sum(fractions_diff), 0.0)
560
561        if outdir is not None:
562            fig = _vis.figure(figscale=(1, 2 / 3))
563            axs = fig.subplot_mosaic(  # Put the legend on a special axes.
564                [["legend"], ["spin"]], gridspec_kw={"height_ratios": [0.1, 1]}
565            )
566            ax = axs["spin"]
567            fig, ax, colors = _vis.spin(
568                ax,
569                initial_angles[::5],
570                rotation_rates[::5],
571                initial_angles,
572                target_rotation_rates,
573                shear_axis=90,
574            )
575            _utils.redraw_legend(
576                ax,
577                legendax=axs["legend"],
578                loc="upper center",
579                ncols=3,
580                mode="expand",
581            )
582            fig.savefig(
583                _io.resolve_path(f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}.pdf")
584            )

Single grain of A-type olivine, slip on (010)[001].

Velocity gradient: $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 0 & 0 & 2 \cr 0 & 0 & 0 \end{bmatrix}$$

class TestRecrystallisation2D:
587class TestRecrystallisation2D:
588    """Basic recrystallisation tests for 2D simple shear."""
589
590    class_id = "recrystallisation_2D"
591
592    def test_shear_dvdx_circle_inplane(self, outdir):
593        r"""360000 grains of A-type olivine with uniform spread of a-axes on a circle.
594
595        Grain growth rates are compared to analytical calculations.
596        The a-axes are distributed in the YX plane (i.e.\ rotated around Z).
597
598        Velocity gradient:
599        $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 2 & 0 & 0 \cr 0 & 0 & 0 \end{bmatrix}$$
600
601        """
602        test_id = "dvdx_circle_inplane"
603
604        optional_logging = cl.nullcontext()
605        # Initial uniform distribution of orientations on a 2D circle.
606        initial_angles = np.mgrid[0 : 2 * np.pi : 360000j]
607        cos2θ = np.cos(2 * initial_angles)
608        if outdir is not None:
609            out_basepath = f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}"
610            optional_logging = _io.logfile_enable(f"{out_basepath}.log")
611
612        with optional_logging:
613            initial_orientations = Rotation.from_rotvec(
614                [[0, 0, θ] for θ in initial_angles]
615            )
616            orientations_diff, fractions_diff = _core.derivatives(
617                regime=_core.DeformationRegime.matrix_dislocation,
618                phase=_core.MineralPhase.olivine,
619                fabric=_core.MineralFabric.olivine_A,
620                n_grains=360000,
621                orientations=initial_orientations.as_matrix(),
622                fractions=np.full(360000, 1 / 360000),
623                strain_rate=np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]]),
624                velocity_gradient=np.array([[0, 0, 0], [2, 0, 0], [0, 0, 0]]),
625                deformation_gradient_spin=np.full((3, 3), np.nan),
626                stress_exponent=1.5,
627                deformation_exponent=3.5,
628                nucleation_efficiency=5,
629                gbm_mobility=125,
630                volume_fraction=1.0,
631            )
632            ρ = np.abs(cos2θ) ** (1.5 / 3.5)
633            # No need to sum over slip systems, only (010)[100] can activate in this
634            # 2D simple shear deformation geometry (ρₛ=ρ).
635            target_strain_energies = ρ * np.exp(-5 * ρ**2)
636            target_fractions_diff = np.array(
637                [  # df/dt* = - M* f (E - E_mean)
638                    -125 * 1 / 360000 * (E - np.mean(target_strain_energies))
639                    for E in target_strain_energies
640                ]
641            )
642
643        if outdir is not None:
644            fig = _vis.figure(figscale=(1, 4 / 3))
645            axs = fig.subplot_mosaic(  # Put the legend on a special axes.
646                [["legend"], ["spin"], ["growth"]],
647                gridspec_kw={"height_ratios": [0.2, 1, 1]},
648                sharex=True,
649            )
650            ax = axs["spin"]
651            xvals = np.rad2deg(initial_angles)
652            fig, ax, colors = _vis.spin(
653                ax,
654                xvals[::2500],
655                np.sqrt(
656                    [
657                        o[0, 0] ** 2 + o[0, 1] ** 2 + o[0, 2] ** 2
658                        for o in orientations_diff
659                    ][::2500]
660                ),
661                xvals,
662                1 + cos2θ,
663                shear_axis=90,
664            )
665            ax.get_legend().remove()
666            ax.label_outer()
667            ax2 = axs["growth"]
668            fig, ax2, colors = _vis.growth(
669                ax2,
670                xvals[::2500],
671                fractions_diff[::2500],
672                xvals,
673                target_fractions_diff,
674                shear_axis=90,
675            )
676            _utils.redraw_legend(
677                ax2,
678                legendax=axs["legend"],
679                loc="upper center",
680                ncols=3,
681                mode="expand",
682            )
683            _utils.add_subplot_labels(axs, labelmap={"spin": "a)", "growth": "b)"})
684            fig.savefig(f"{out_basepath}.pdf")
685
686        nt.assert_allclose(fractions_diff, target_fractions_diff, atol=1e-15, rtol=0)
687
688    def test_shear_dvdx_circle_shearplane(self, outdir):
689        r"""360000 grains of A-type olivine with uniform spread of a-axes on a circle.
690
691        Unlike `test_shear_dvdx_circle_inplane`, two slip systems are active here,
692        with cyclical variety in which one is dominant depending on grain orientation.
693        The a-axes are distributed in the YZ plane
694        (i.e.\ extrinsic rotation around Z by 90° and then around X).
695
696        Velocity gradient:
697        $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 2 & 0 & 0 \cr 0 & 0 & 0 \end{bmatrix}$$
698
699        """
700        test_id = "dvdx_circle_shearplane"
701
702        optional_logging = cl.nullcontext()
703        # Initial uniform distribution of orientations on a 2D circle.
704        initial_angles = np.mgrid[0 : 2 * np.pi : 360000j]
705        if outdir is not None:
706            out_basepath = f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}"
707            optional_logging = _io.logfile_enable(f"{out_basepath}.log")
708
709        with optional_logging:
710            initial_orientations = Rotation.from_euler(
711                "zx", [[np.pi / 2, θ] for θ in initial_angles]
712            )
713            orientations_diff, fractions_diff = _core.derivatives(
714                regime=_core.DeformationRegime.matrix_dislocation,
715                phase=_core.MineralPhase.olivine,
716                fabric=_core.MineralFabric.olivine_A,
717                n_grains=360000,
718                orientations=initial_orientations.as_matrix(),
719                fractions=np.full(360000, 1 / 360000),
720                strain_rate=np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]]),
721                velocity_gradient=np.array([[0, 0, 0], [2, 0, 0], [0, 0, 0]]),
722                deformation_gradient_spin=np.full((3, 3), np.nan),
723                stress_exponent=1.5,
724                deformation_exponent=3.5,
725                nucleation_efficiency=5,
726                gbm_mobility=125,
727                volume_fraction=1.0,
728            )
729
730        if outdir is not None:
731            fig = _vis.figure(figscale=(1, 4 / 3))
732            axs = fig.subplot_mosaic([["spin"], ["growth"]], sharex=True)
733            ax = axs["spin"]
734            xvals = np.rad2deg(initial_angles)
735            fig, ax, colors = _vis.spin(
736                ax,
737                xvals[::2500],
738                np.sqrt(
739                    [
740                        o[0, 0] ** 2 + o[0, 1] ** 2 + o[0, 2] ** 2
741                        for o in orientations_diff
742                    ][::2500]
743                ),
744            )
745            ax.get_legend().remove()
746            ax.label_outer()
747            ax2 = axs["growth"]
748            fig, ax2, colors = _vis.growth(ax2, xvals[::2500], fractions_diff[::2500])
749            _utils.add_subplot_labels(axs, labelmap={"spin": "a)", "growth": "b)"})
750            fig.savefig(f"{out_basepath}.pdf")
751
752        # Check dominant slip system every 1°.
753        for θ in initial_angles[::1000]:
754            slip_invariants = _core._get_slip_invariants(
755                np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]]),
756                Rotation.from_euler("zx", [np.pi / 2, θ]).as_matrix(),
757            )
758            θ = np.rad2deg(θ)
759            crss = _core.get_crss(
760                _core.MineralPhase.olivine,
761                _core.MineralFabric.olivine_A,
762            )
763            slip_indices = np.argsort(np.abs(slip_invariants / crss))
764            slip_system = _minerals.OLIVINE_SLIP_SYSTEMS[slip_indices[-1]]
765
766            if 0 <= θ < 64:
767                assert slip_system == ([0, 1, 0], [1, 0, 0])
768            elif 64 <= θ < 117:
769                assert slip_system == ([0, 0, 1], [1, 0, 0])
770            elif 117 <= θ < 244:
771                assert slip_system == ([0, 1, 0], [1, 0, 0])
772            elif 244 <= θ < 297:
773                assert slip_system == ([0, 0, 1], [1, 0, 0])
774            else:
775                assert slip_system == ([0, 1, 0], [1, 0, 0])

Basic recrystallisation tests for 2D simple shear.

class_id = 'recrystallisation_2D'
def test_shear_dvdx_circle_inplane(self, outdir):
592    def test_shear_dvdx_circle_inplane(self, outdir):
593        r"""360000 grains of A-type olivine with uniform spread of a-axes on a circle.
594
595        Grain growth rates are compared to analytical calculations.
596        The a-axes are distributed in the YX plane (i.e.\ rotated around Z).
597
598        Velocity gradient:
599        $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 2 & 0 & 0 \cr 0 & 0 & 0 \end{bmatrix}$$
600
601        """
602        test_id = "dvdx_circle_inplane"
603
604        optional_logging = cl.nullcontext()
605        # Initial uniform distribution of orientations on a 2D circle.
606        initial_angles = np.mgrid[0 : 2 * np.pi : 360000j]
607        cos2θ = np.cos(2 * initial_angles)
608        if outdir is not None:
609            out_basepath = f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}"
610            optional_logging = _io.logfile_enable(f"{out_basepath}.log")
611
612        with optional_logging:
613            initial_orientations = Rotation.from_rotvec(
614                [[0, 0, θ] for θ in initial_angles]
615            )
616            orientations_diff, fractions_diff = _core.derivatives(
617                regime=_core.DeformationRegime.matrix_dislocation,
618                phase=_core.MineralPhase.olivine,
619                fabric=_core.MineralFabric.olivine_A,
620                n_grains=360000,
621                orientations=initial_orientations.as_matrix(),
622                fractions=np.full(360000, 1 / 360000),
623                strain_rate=np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]]),
624                velocity_gradient=np.array([[0, 0, 0], [2, 0, 0], [0, 0, 0]]),
625                deformation_gradient_spin=np.full((3, 3), np.nan),
626                stress_exponent=1.5,
627                deformation_exponent=3.5,
628                nucleation_efficiency=5,
629                gbm_mobility=125,
630                volume_fraction=1.0,
631            )
632            ρ = np.abs(cos2θ) ** (1.5 / 3.5)
633            # No need to sum over slip systems, only (010)[100] can activate in this
634            # 2D simple shear deformation geometry (ρₛ=ρ).
635            target_strain_energies = ρ * np.exp(-5 * ρ**2)
636            target_fractions_diff = np.array(
637                [  # df/dt* = - M* f (E - E_mean)
638                    -125 * 1 / 360000 * (E - np.mean(target_strain_energies))
639                    for E in target_strain_energies
640                ]
641            )
642
643        if outdir is not None:
644            fig = _vis.figure(figscale=(1, 4 / 3))
645            axs = fig.subplot_mosaic(  # Put the legend on a special axes.
646                [["legend"], ["spin"], ["growth"]],
647                gridspec_kw={"height_ratios": [0.2, 1, 1]},
648                sharex=True,
649            )
650            ax = axs["spin"]
651            xvals = np.rad2deg(initial_angles)
652            fig, ax, colors = _vis.spin(
653                ax,
654                xvals[::2500],
655                np.sqrt(
656                    [
657                        o[0, 0] ** 2 + o[0, 1] ** 2 + o[0, 2] ** 2
658                        for o in orientations_diff
659                    ][::2500]
660                ),
661                xvals,
662                1 + cos2θ,
663                shear_axis=90,
664            )
665            ax.get_legend().remove()
666            ax.label_outer()
667            ax2 = axs["growth"]
668            fig, ax2, colors = _vis.growth(
669                ax2,
670                xvals[::2500],
671                fractions_diff[::2500],
672                xvals,
673                target_fractions_diff,
674                shear_axis=90,
675            )
676            _utils.redraw_legend(
677                ax2,
678                legendax=axs["legend"],
679                loc="upper center",
680                ncols=3,
681                mode="expand",
682            )
683            _utils.add_subplot_labels(axs, labelmap={"spin": "a)", "growth": "b)"})
684            fig.savefig(f"{out_basepath}.pdf")
685
686        nt.assert_allclose(fractions_diff, target_fractions_diff, atol=1e-15, rtol=0)

360000 grains of A-type olivine with uniform spread of a-axes on a circle.

Grain growth rates are compared to analytical calculations. The a-axes are distributed in the YX plane (i.e.\ rotated around Z).

Velocity gradient: $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 2 & 0 & 0 \cr 0 & 0 & 0 \end{bmatrix}$$

def test_shear_dvdx_circle_shearplane(self, outdir):
688    def test_shear_dvdx_circle_shearplane(self, outdir):
689        r"""360000 grains of A-type olivine with uniform spread of a-axes on a circle.
690
691        Unlike `test_shear_dvdx_circle_inplane`, two slip systems are active here,
692        with cyclical variety in which one is dominant depending on grain orientation.
693        The a-axes are distributed in the YZ plane
694        (i.e.\ extrinsic rotation around Z by 90° and then around X).
695
696        Velocity gradient:
697        $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 2 & 0 & 0 \cr 0 & 0 & 0 \end{bmatrix}$$
698
699        """
700        test_id = "dvdx_circle_shearplane"
701
702        optional_logging = cl.nullcontext()
703        # Initial uniform distribution of orientations on a 2D circle.
704        initial_angles = np.mgrid[0 : 2 * np.pi : 360000j]
705        if outdir is not None:
706            out_basepath = f"{outdir}/{SUBDIR}/{self.class_id}_{test_id}"
707            optional_logging = _io.logfile_enable(f"{out_basepath}.log")
708
709        with optional_logging:
710            initial_orientations = Rotation.from_euler(
711                "zx", [[np.pi / 2, θ] for θ in initial_angles]
712            )
713            orientations_diff, fractions_diff = _core.derivatives(
714                regime=_core.DeformationRegime.matrix_dislocation,
715                phase=_core.MineralPhase.olivine,
716                fabric=_core.MineralFabric.olivine_A,
717                n_grains=360000,
718                orientations=initial_orientations.as_matrix(),
719                fractions=np.full(360000, 1 / 360000),
720                strain_rate=np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]]),
721                velocity_gradient=np.array([[0, 0, 0], [2, 0, 0], [0, 0, 0]]),
722                deformation_gradient_spin=np.full((3, 3), np.nan),
723                stress_exponent=1.5,
724                deformation_exponent=3.5,
725                nucleation_efficiency=5,
726                gbm_mobility=125,
727                volume_fraction=1.0,
728            )
729
730        if outdir is not None:
731            fig = _vis.figure(figscale=(1, 4 / 3))
732            axs = fig.subplot_mosaic([["spin"], ["growth"]], sharex=True)
733            ax = axs["spin"]
734            xvals = np.rad2deg(initial_angles)
735            fig, ax, colors = _vis.spin(
736                ax,
737                xvals[::2500],
738                np.sqrt(
739                    [
740                        o[0, 0] ** 2 + o[0, 1] ** 2 + o[0, 2] ** 2
741                        for o in orientations_diff
742                    ][::2500]
743                ),
744            )
745            ax.get_legend().remove()
746            ax.label_outer()
747            ax2 = axs["growth"]
748            fig, ax2, colors = _vis.growth(ax2, xvals[::2500], fractions_diff[::2500])
749            _utils.add_subplot_labels(axs, labelmap={"spin": "a)", "growth": "b)"})
750            fig.savefig(f"{out_basepath}.pdf")
751
752        # Check dominant slip system every 1°.
753        for θ in initial_angles[::1000]:
754            slip_invariants = _core._get_slip_invariants(
755                np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]]),
756                Rotation.from_euler("zx", [np.pi / 2, θ]).as_matrix(),
757            )
758            θ = np.rad2deg(θ)
759            crss = _core.get_crss(
760                _core.MineralPhase.olivine,
761                _core.MineralFabric.olivine_A,
762            )
763            slip_indices = np.argsort(np.abs(slip_invariants / crss))
764            slip_system = _minerals.OLIVINE_SLIP_SYSTEMS[slip_indices[-1]]
765
766            if 0 <= θ < 64:
767                assert slip_system == ([0, 1, 0], [1, 0, 0])
768            elif 64 <= θ < 117:
769                assert slip_system == ([0, 0, 1], [1, 0, 0])
770            elif 117 <= θ < 244:
771                assert slip_system == ([0, 1, 0], [1, 0, 0])
772            elif 244 <= θ < 297:
773                assert slip_system == ([0, 0, 1], [1, 0, 0])
774            else:
775                assert slip_system == ([0, 1, 0], [1, 0, 0])

360000 grains of A-type olivine with uniform spread of a-axes on a circle.

Unlike test_shear_dvdx_circle_inplane, two slip systems are active here, with cyclical variety in which one is dominant depending on grain orientation. The a-axes are distributed in the YZ plane (i.e.\ extrinsic rotation around Z by 90° and then around X).

Velocity gradient: $$\bm{L} = \begin{bmatrix} 0 & 0 & 0 \cr 2 & 0 & 0 \cr 0 & 0 & 0 \end{bmatrix}$$