Mondaic

Materials 3: Oriented materials

So far, we’ve discussed different symmetry classes of elastic and acoustic materials. These symmetry classes are defined by specific mathematical transformations (e.g. rotations, reflections) that leave the material properties unchanged. For example, a hexagonal (transversely isotropic) material doesn’t change its properties when rotated around the axis of symmetry.

In Salvus material, by default, the axes of the symmetry classes are aligned with the coordinate axes. However, in many cases, an actual material may be oriented arbitrarily in space, or even spatially heterogeneously oriented.:

  1. Geological formations - Layered sedimentary rock formations frequently have tilted or folded layers
  2. Composite materials - Engineered materials like fiber composites have specific fiber orientations
  3. Crystalline structures - Natural crystals and minerals have preferred orientations
  4. Fluid-filled fractures - Aligned fluid-filled cracks or fractures create directional anisotropy

Being able to specify material orientations allows modelers to represent these real-world scenarios more accurately, leading to more precise simulation results.

Material transformations and symmetry classes

The material module in Salvus allows for the orientation of materials to be specified. One very important note however is that if a material like hexagonal is oriented such that the symmetry axis or planes do not anymore align with the coordinate axes, the material is no longer hexagonal in the global coordinate system. This means that after applying an orientation, materials in general will be triclinic (elastic) or orthotropic (acoustic).

For example, if we take a VTI (Vertically Transverse Isotropic) material with its symmetry axis aligned with the z-axis, and tilt it by some angle, the resulting material will need to be represented in a lower symmetry class (triclinic) when expressed in the global coordinate system.

A big caveat of using oriented materials is that the module does not easily support orienting materials for 2 dimensions. If this is a feature you are interested in, please reach out to support to get you underway on a per-case basis, and to indicate there is interest in this feature being part of the module.

from salvus.material import elastic, orientation
import numpy as np
from salvus.material._details.material import to_solver_form

np.set_printoptions(precision=2)

Creating orientations

Salvus provides multiple ways to specify the orientation of a material. These different methods allow you to describe the orientation in terms that make the most sense for your particular application or dataset.

AzimuthDip

The AzimuthDip orientation is one of the more common ways to specify material orientation. It uses:

  • Azimuth: The angle (in degrees) measured from the first global axis (the x-axis) in the horizontal plane.
  • Dip: The angle (in degrees) measured downward from the horizontal plane.

This parameterization is particularly useful in geophysics and structural geology where field measurements are often reported in these terms.

orientation_1 = orientation.AzimuthDip.from_params(azimuth=0, dip=22.5)

orientation_1
salvus.material.orientation.AzimuthDip

With the following properties:

FieldTypeValue (or range)
AZIMUTHconstant0
DIPconstant22
salvus.material.orientation.AzimuthDip

All orientations know how to generate a rotation matrix that can be used to rotate a material tensor (which is done internally in Salvus later):

orientation_1.generate_rotation_matrix()
array([[[ 0.92,  0.  ,  0.38],
        [ 0.  ,  1.  ,  0.  ],
        [-0.38,  0.  ,  0.92]]])

The visualization shows how the local coordinate system is oriented relative to the global coordinate system. The local z-axis (blue) is tilted at 22.5° from the global z-axis, towards the global x-axis (red).

_ = orientation.visualize_local_bases(orientation_1)

We can also specify spatially varying orientations by providing arrays for azimuth and dip:


orientation_2 = orientation.AzimuthDip.from_params(
    azimuth=0, dip=np.array([0, 11.25, 22.5, 33.75, 45.0])
)

# The semi-colon at the end of the line suppresses the output in Jupyter,
# such that the figure is not printed twice.
_ = orientation.visualize_local_bases(orientation_2)

That was different dip angles in the global x-z plane. Specifying varying azimuths would look like this:


orientation_3 = orientation.AzimuthDip.from_params(
    azimuth=np.array([0, 30, 60, 90]), dip=33
)

_ = orientation.visualize_local_bases(orientation_3)

AxesDips

Another way to specify orientation is using the AxesDips class, which allows you to specify:

  • dip_x: The dip angle of the x-axis from the horizontal plane
  • dip_y: The dip angle of the y-axis from the horizontal plane

This parameterization can be more intuitive when working with materials where you know the orientation of specific planes or axes relative to the global coordinate system.

orientation_4 = orientation.AxesDips.from_params(dip_x=22.5, dip_y=12.5)

orientation_4
salvus.material.orientation.AxesDips

With the following properties:

FieldTypeValue (or range)
DIP_Xconstant22
DIP_Yconstant12
salvus.material.orientation.AxesDips

What’s important to note is that the dip_x and dip_y angles don’t specify the total dip of the local horizontal plane, except when one of them is 0:

orientation_5 = orientation.AxesDips.from_params(dip_x=22.5, dip_y=0)
np.allclose(
    orientation_1.generate_rotation_matrix(),
    orientation_5.generate_rotation_matrix(),
)
True
# But when dip_x and dip_y are both non-zero:
np.allclose(
    orientation_1.generate_rotation_matrix(),
    orientation_4.generate_rotation_matrix(),
)
False

This effect can be very deceptive. For relatively steep dipping planes, the visual difference is small:

orientation_5a = orientation.AxesDips.from_params(dip_x=33, dip_y=33)
orientation_5a
salvus.material.orientation.AxesDips

With the following properties:

FieldTypeValue (or range)
DIP_Xconstant33
DIP_Yconstant33
salvus.material.orientation.AxesDips
orientation_5b = orientation.AzimuthDip.from_params(azimuth=45, dip=33)
orientation_5b
salvus.material.orientation.AzimuthDip

With the following properties:

FieldTypeValue (or range)
AZIMUTHconstant45
DIPconstant33
salvus.material.orientation.AzimuthDip
orientation.visualize_local_bases(orientation_5a)
orientation.visualize_local_bases(orientation_5b)

This unapparent difference is an effect of the projection on the coordinate planes. When directly comparing the two orientations’ rotation matrices, we see that they are indeed different:

np.allclose(
    orientation_5a.generate_rotation_matrix(),
    orientation_5b.generate_rotation_matrix(),
)
False

A final test is to convert one of the orientations into the other’s system:

orientation.AzimuthDip.from_material(orientation_5a).DIP.p
42.564398528805846
orientation_5b.DIP.p
33.0

Ambiguous orientations

The AxesDips and AzimuthDip orientations are unfortunately not always sufficient to describe the orientation of a material. For example, if we have a material that is not hexagonal, but orthotropic, monoclinic or triclinic, we need to also keep track of the orientation of the material’s horizontal rotation about the local z-axis.

Both the aforementioned orientations are not able to do this, and will simply ambiguously transform local_x and local_y to lie in the dipping plane.

AzimuthDipRake

A further way to specify orientation is using the AzimuthDipRake orientation, which has the same definition as AzimuthDip, but adds a rake angle. The rake angle is defined as an angle measured from the horizontal on the dipping plane (strike) for local y, or as the angle between the down direction on the dipping plane and the local x-axis. This way, when “facing” in the down direction on the dipping plane, local x is pointing in your view direction, and local y is pointing to your left. The rake angle is measured in the plane of the dip.

This is a common way to specify the orientation of faults or fractures in geophysics, where the rake angle describes the direction of slip or movement along the fault plane.

orientation_6 = orientation.AzimuthDipRake.from_params(
    azimuth=0,  # Dip towards the x-axis
    dip=22.5,
    # Local x as to make these angles with the local down, in-plane.
    rake=np.linspace(0, 30, 5),
)

orientation.visualize_local_bases(orientation_6)

DirectOrthonormalBasis

Finally, we can also specify the orientation using a DirectOrthonormalBasis class. This class allows you to specify the local coordinate system using three orthonormal basis vectors. This is a more general way to specify orientation and can be useful when you have a specific coordinate system in mind or when the other parameterizations are not sufficient.

orientation_7 = orientation.DirectOrthonormalBasis.from_basis_vectors(
    local_x=np.array([0, 1.0, 0]),
    local_y=np.array([-1.0, 0.0, 0]),
    local_z=np.array([0, 0, 1.0]),
)

orientation.visualize_local_bases(orientation_7)

Note that this orientation also applies the Gram-Schmidt process to ensure that the basis vectors are orthonormal. This means that the vectors are guaranteed to be orthogonal and have unit length, but the input vectors are not necessarily respected.

orientation_8 = orientation.DirectOrthonormalBasis.from_basis_vectors(
    local_x=np.array([0, 1.0, 0]),
    local_y=np.array([-1.0, 0.0, 0]),
    local_z=np.array([0.2, 0.2, 1.0]),
    skip_orthonormalization=False,
)
orientation.visualize_local_bases(orientation_8)

orientation_8.get_basis_vectors()
(array([[ 0.  ,  0.98, -0.19]]),
 array([[-0.98,  0.04,  0.19]]),
 array([[0.2 , 0.19, 0.96]]))

Applying orientations to materials

Now that we’ve explored how to create different orientations, let’s see how to apply them to materials. When we apply an orientation to a material, we’re essentially rotating the material’s properties to align with our specified orientation.

VTI materials and dipping planes

Here, we’ll start with a VTI (Vertically Transverse Isotropic) material defined using Thomsen parameters. Thomsen parameters are commonly used in exploration geophysics to describe weak anisotropy:

  • epsilon (ε): Controls the difference between horizontal and vertical P-wave velocities
  • delta (δ): Controls the angular dependence of P-wave velocity
  • gamma (γ): Controls the difference between horizontal and vertical S-wave velocities

First, let’s create our VTI material:

elastic_vti_material = elastic.transversely_isotropic.Thomsen.from_params(
    rho=2500, vp=5000, vs=3500, epsilon=0.23, delta=0.4, gamma=0.2
)
elastic_vti_material
salvus.material.elastic.hexagonal.Thomsen

With the following properties:

FieldTypeValue (or range)
RHOconstant2.5e+03
VPconstant5e+03
VSconstant3.5e+03
DELTAconstant0.4
GAMMAconstant0.2
EPSILONconstant0.23
salvus.material.elastic.hexagonal.Thomsen
elastic_vti_material.to_tensor_components()
salvus.material.elastic.hexagonal.TensorComponents

With the following properties:

FieldTypeValue (or range)
RHOconstant2.5e+03
C11constant9.1e+10
C12constant5.5e+09
C13constant2e+10
C33constant6.2e+10
C44constant3.1e+10

The averages of the stiffness tensor are the following, using the symmetry definition of this material to fill in implied positions. Empty positions are zero but not stored in memory for this material:

Cij 1 2 3 4 5 6
19.1e+105.5e+092e+10---
25.5e+099.1e+102e+10---
32e+102e+106.2e+10---
4---3.1e+10--
5----3.1e+10-
6-----4.3e+10
salvus.material.elastic.hexagonal.TensorComponents

When we apply an orientation to our VTI material, we’re essentially tilting its symmetry axis. In this case, we’re using our AzimuthDip orientation to tilt the symmetry axis by 22.5 degrees from the vertical:

orientation_1 = orientation.AzimuthDip.from_params(azimuth=0, dip=22.5)
elastic_vti_material_oriented = elastic_vti_material.with_orientation(
    orientation_1
)

elastic_vti_material_oriented
abc.salvus.material.orientation.elastic.hexagonal.Thomsen

With the following properties:

FieldTypeValue (or range)
RHOconstant2.5e+03
VPconstant5e+03
VSconstant3.5e+03
DELTAconstant0.4
GAMMAconstant0.2
EPSILONconstant0.23
AZIMUTHconstant0
DIPconstant22
abc.salvus.material.orientation.elastic.hexagonal.Thomsen

Now let’s convert our oriented material to a solver-compatible form. Salvus requires materials to be in one of its supported forms for simulation. When we apply an orientation to a material, we need to convert it to a form that the solver can understand - in this case, a triclinic tensor components material.

# to_solver_form(m=elastic_vti_material) # Won't work, nothing to do!

elastic_vti_material_oriented_sf = to_solver_form(
    m=elastic_vti_material_oriented
)

elastic_vti_material_oriented_sf
salvus.material.elastic.triclinic.TensorComponents

With the following properties:

FieldTypeValue (or range)
RHOconstant2.5e+03
C11constant8.8e+10
C12constant7.7e+09
C13constant1.9e+10
C14constant0
C15constant-3.9e+09
C16constant0
C22constant9.1e+10
C23constant1.8e+10
C24constant0
C25constant5.3e+09
C26constant0
C33constant6.8e+10
C34constant0
C35constant-6.3e+09
C36constant0
C44constant3.2e+10
C45constant0
C46constant-4.3e+09
C55constant2.9e+10
C56constant0
C66constant4.1e+10

The averages of the stiffness tensor are the following, using the symmetry definition of this material to fill in implied positions. Empty positions are zero but not stored in memory for this material:

Cij 1 2 3 4 5 6
18.8e+107.7e+091.9e+100-3.9e+090
27.7e+099.1e+101.8e+1005.3e+090
31.9e+101.8e+106.8e+100-6.3e+090
40003.2e+100-4.3e+09
5-3.9e+095.3e+09-6.3e+0902.9e+100
6000-4.3e+0904.1e+10
salvus.material.elastic.triclinic.TensorComponents
elastic_vti_material_oriented_sf.to_wavelength_oracle()
Constant parameter
Spatially constant
Value: 3.4e+03
isinstance(
    elastic_vti_material_oriented_sf, elastic.triclinic.TensorComponents
)
True

Rotating in the symmetry plane does not break the symmetry of the tensor, but will downcast the material to triclinic. Downcasting means that the material is no longer in the original symmetry class (VTI) but is now represented in a lower symmetry class.


orientation_rotation_vti = (
    orientation.DirectOrthonormalBasis.from_basis_vectors(
        local_x=np.array([0, 1.0, 0]),
        local_y=np.array([-1.0, 0, 0]),
        local_z=np.array([0, 0, 1.0]),
    )
)

to_solver_form(elastic_vti_material.with_orientation(orientation_rotation_vti))
salvus.material.elastic.triclinic.TensorComponents

With the following properties:

FieldTypeValue (or range)
RHOconstant2.5e+03
C11constant9.1e+10
C12constant5.5e+09
C13constant2e+10
C14constant0
C15constant0
C16constant0
C22constant9.1e+10
C23constant2e+10
C24constant0
C25constant0
C26constant0
C33constant6.2e+10
C34constant0
C35constant0
C36constant0
C44constant3.1e+10
C45constant0
C46constant0
C55constant3.1e+10
C56constant0
C66constant4.3e+10

The averages of the stiffness tensor are the following, using the symmetry definition of this material to fill in implied positions. Empty positions are zero but not stored in memory for this material:

Cij 1 2 3 4 5 6
19.1e+105.5e+092e+10000
25.5e+099.1e+102e+10000
32e+102e+106.2e+10000
40003.1e+1000
500003.1e+100
6000004.3e+10
salvus.material.elastic.triclinic.TensorComponents

Orthotropic materials and rake angles

Now let’s create an orthotropic material using the AzimuthDipRake orientation. This material has different properties in three orthogonal directions, and we can specify its orientation using the azimuth, dip, and rake angles.

# Completely fictitious material properties
orthotropic_material = elastic.orthotropic.EngineeringConstants.from_params(
    rho=2500,
    e1=50e9,
    e2=30e9,
    e3=20e9,
    v12=0.2,
    v13=0.1,
    v23=0.3,
    g12=10e9,
    g13=5e9,
    g23=2e9,
)

orthotropic_material
salvus.material.elastic.orthotropic.EngineeringConstants

With the following properties:

FieldTypeValue (or range)
RHOconstant2.5e+03
E1constant5e+10
E2constant3e+10
E3constant2e+10
G12constant1e+10
G13constant5e+09
G23constant2e+09
V12constant0.2
V13constant0.1
V23constant0.3
salvus.material.elastic.orthotropic.EngineeringConstants
# Let's visualize the entries of the stiffness tensor as well:
orthotropic_material.to_tensor_components()
salvus.material.elastic.orthotropic.TensorComponents

With the following properties:

FieldTypeValue (or range)
RHOconstant2.5e+03
C11constant5.2e+10
C12constant7.3e+09
C13constant3.5e+09
C22constant3.3e+10
C23constant6.9e+09
C33constant2.2e+10
C44constant2e+09
C55constant5e+09
C66constant1e+10

The averages of the stiffness tensor are the following, using the symmetry definition of this material to fill in implied positions. Empty positions are zero but not stored in memory for this material:

Cij 1 2 3 4 5 6
15.2e+107.3e+093.5e+09---
27.3e+093.3e+106.9e+09---
33.5e+096.9e+092.2e+10---
4---2e+09--
5----5e+09-
6-----1e+10
salvus.material.elastic.orthotropic.TensorComponents

We now first start off easy: a material with simply a 90 degree rotation about the (local) z-axis. Let’s see what this does to the stiffness tensor:

orientation_orthotropic_1 = orientation.AzimuthDipRake.from_params(
    azimuth=0, dip=0, rake=90
)

orthotropic_material_oriented = orthotropic_material.with_orientation(
    orientation_orthotropic_1
)

to_solver_form(orthotropic_material_oriented)
salvus.material.elastic.triclinic.TensorComponents

With the following properties:

FieldTypeValue (or range)
RHOconstant2.5e+03
C11constant3.3e+10
C12constant7.3e+09
C13constant6.9e+09
C14constant0
C15constant0
C16constant-3.5e-07
C22constant5.2e+10
C23constant3.5e+09
C24constant0
C25constant0
C26constant1.5e-06
C33constant2.2e+10
C34constant0
C35constant0
C36constant-2.1e-07
C44constant5e+09
C45constant1.8e-07
C46constant0
C55constant2e+09
C56constant0
C66constant1e+10

The averages of the stiffness tensor are the following, using the symmetry definition of this material to fill in implied positions. Empty positions are zero but not stored in memory for this material:

Cij 1 2 3 4 5 6
13.3e+107.3e+096.9e+0900-3.5e-07
27.3e+095.2e+103.5e+09001.5e-06
36.9e+093.5e+092.2e+1000-2.1e-07
40005e+091.8e-070
50001.8e-072e+090
6-3.5e-071.5e-06-2.1e-07001e+10
salvus.material.elastic.triclinic.TensorComponents

This resulting material has the entries for C11 and C22 swapped, C13 and C23 swapped, and C44 and C55 swapped. Additionally, some numerical noise has entered some of the off-diagonal terms. This is not an issue when simulating with Salvus, but can be manually muted if desired.

To map this material back to an orthotropic form (and mute numerical noise), Salvus will require you to forcefully discard information, as strictly speaking, the material is now triclinic.

elastic.orthotropic.EngineeringConstants.from_material(
    to_solver_form(orthotropic_material_oriented),
    reduction_method="remove-components",
)
salvus.material.elastic.orthotropic.EngineeringConstants

With the following properties:

FieldTypeValue (or range)
RHOconstant2.5e+03
E1constant3e+10
E2constant5e+10
E3constant2e+10
G12constant1e+10
G13constant2e+09
G23constant5e+09
V12constant0.12
V13constant0.3
V23constant0.1
salvus.material.elastic.orthotropic.EngineeringConstants

Also note that remove-components only works if the entries in the to-be removed components are actually close to zero. This is apparent when we try the same approach with a less than 90 degree rotation, under which the material doesn’t remain orthotropic in the global coordinate system:

orthotropic_material_oriented_45 = orthotropic_material.with_orientation(
    orientation.AzimuthDipRake.from_params(azimuth=0, dip=0, rake=45)
)
to_solver_form(orthotropic_material_oriented_45)
salvus.material.elastic.triclinic.TensorComponents

With the following properties:

FieldTypeValue (or range)
RHOconstant2.5e+03
C11constant3.5e+10
C12constant1.5e+10
C13constant5.2e+09
C14constant0
C15constant0
C16constant4.7e+09
C22constant3.5e+10
C23constant5.2e+09
C24constant0
C25constant0
C26constant4.7e+09
C33constant2.2e+10
C34constant0
C35constant0
C36constant-1.7e+09
C44constant3.5e+09
C45constant1.5e+09
C46constant0
C55constant3.5e+09
C56constant0
C66constant1.8e+10

The averages of the stiffness tensor are the following, using the symmetry definition of this material to fill in implied positions. Empty positions are zero but not stored in memory for this material:

Cij 1 2 3 4 5 6
13.5e+101.5e+105.2e+09004.7e+09
21.5e+103.5e+105.2e+09004.7e+09
35.2e+095.2e+092.2e+1000-1.7e+09
40003.5e+091.5e+090
50001.5e+093.5e+090
64.7e+094.7e+09-1.7e+09001.8e+10
salvus.material.elastic.triclinic.TensorComponents
try:
    elastic.orthotropic.EngineeringConstants.from_material(
        to_solver_form(orthotropic_material_oriented_45),
        reduction_method="remove-components",
    )
except TypeError as e:
    print(e)
Passed material of type <class 'salvus.material.elastic.triclinic.TensorComponents'> can't be treated as <class 'salvus.material.elastic.orthotropic.TensorComponents'>, as it does not respect the symmetry. The component C16 should be zero, but it is not.

The “force” method will work, but information will be lost and the material will be altered:

elastic.orthotropic.EngineeringConstants.from_material(
    to_solver_form(orthotropic_material_oriented_45), reduction_method="force"
)
salvus.material.elastic.orthotropic.EngineeringConstants

With the following properties:

FieldTypeValue (or range)
RHOconstant2.5e+03
E1constant2.8e+10
E2constant2.8e+10
E3constant2e+10
G12constant1.8e+10
G13constant3.5e+09
G23constant3.5e+09
V12constant0.4
V13constant0.14
V23constant0.14
salvus.material.elastic.orthotropic.EngineeringConstants

Finally, let’s see how an orthotropic material can map into a fully triclinic material.

orthotropic_material_oriented_dramatic = orthotropic_material.with_orientation(
    orientation.AzimuthDipRake.from_params(azimuth=22.5, dip=30, rake=45)
)
to_solver_form(orthotropic_material_oriented_dramatic)
salvus.material.elastic.triclinic.TensorComponents

With the following properties:

FieldTypeValue (or range)
RHOconstant2.5e+03
C11constant2.5e+10
C12constant9.5e+09
C13constant1.1e+10
C14constant1.6e+09
C15constant-5.6e+09
C16constant-1.6e+09
C22constant4.2e+10
C23constant7.6e+09
C24constant-8.2e+09
C25constant-3e+09
C26constant3.6e+09
C33constant1.9e+10
C34constant-7.8e+08
C35constant9.5e+08
C36constant7.6e+08
C44constant8.8e+09
C45constant2.3e+09
C46constant-4.2e+09
C55constant7.7e+09
C56constant-6.4e+08
C66constant1.1e+10

The averages of the stiffness tensor are the following, using the symmetry definition of this material to fill in implied positions. Empty positions are zero but not stored in memory for this material:

Cij 1 2 3 4 5 6
12.5e+109.5e+091.1e+101.6e+09-5.6e+09-1.6e+09
29.5e+094.2e+107.6e+09-8.2e+09-3e+093.6e+09
31.1e+107.6e+091.9e+10-7.8e+089.5e+087.6e+08
41.6e+09-8.2e+09-7.8e+088.8e+092.3e+09-4.2e+09
5-5.6e+09-3e+099.5e+082.3e+097.7e+09-6.4e+08
6-1.6e+093.6e+097.6e+08-4.2e+09-6.4e+081.1e+10
salvus.material.elastic.triclinic.TensorComponents

This even produces negative moduli in the off-diagonal terms!

In the final tutorial of this series, we will explore how to set up a simulation using the materials we’ve created.