Version:

Materials 2: Anisotropy and elliptic coefficients

The Salvus materials module also supports anisotropic materials, i.e. materials that have different properties in different directions. This is a common occurrence in waveform physics, as many materials are anisotropic due to their crystalline structure or defect-induced microstructure.
The materials module supports a variety of symmetry classes. A symmetry class is a set of symmetries that are equivalent under certain transformations. For example, a material that is isotropic in the horizontal plane and but has different properties in the vertical direction is said to have a transversely isotropic symmetry class.
A slightly more rigorous definition of symmetry classes can be made using crystalline symmetry groups. The following symmetry classes are defined in the materials module:
Elastic symmetry classes:
  • Isotropic
  • Cubic
  • Hexagonal (= Transversely isotropic)
  • Orthotropic (= Orthorhombic)
  • Monoclinic
  • Triclinic (= generally anisotropic)
Acoustic symmetry classes:
  • Isotropic
  • Cubic
  • Hexagonal (= Transversely isotropic)
  • Orthotropic (= Orthorhombic)
The materials module of Salvus allows all kinds of transformations between these symmetry classes. The only restriction is that the symmetry class of the source material must be compatible with the symmetry class of the target material. For example, an isotropic material can be transformed to any symmetry class, but a cubic material can only be transformed to a cubic or less symmetric material.
The exact rules for compatibility are based on a partial ordering of the symmetry classes, which follows the direction of the lists above, except for the elastic hexagonal and cubic materials. These materials are not (generally) convertible into each other, but they are convertible into the orthotropic symmetry class.
Copy
# Import Salvus' material module
from salvus import material
from salvus.material import elastic, acoustic
import numpy as np
import textwrap

Elliptic coefficients and tensors

The easiest way to inspect all anisotropic materials and relate the different symmetry classes is to discuss the (in the elastic case) stiffness tensor from the anisotropic wave equation. The stiffness tensor is a rank 4 tensor that is commonly written as a symmetric 6x6 matrix using Voigt notation in the most general anisotropic case. Effectively, they act as the elliptic coefficients of the anisotropic elastic wave equation:
σ=C:uρt2u=σ+f\begin{align} {\pmb \sigma} &= {\pmb C} : \nabla {\pmb u} \\ \rho \, \partial_t^2 \, {\pmb u} &= \nabla \cdot {\pmb \sigma} + {\pmb f} \end{align}
The symbols involved are the following:
ttime [s]gradient operator (nabla)σSecond-order stress tensor [Pa or N/m2]CFourth-order stiffness tensor [Pa or N/m2]uDisplacement [m]ρDensity [kg/m3]fExternal forcing [N/m3]\begin{align} t &\quad \text{time [s]} \\ \nabla &\quad \text{gradient operator (nabla)} \\ {\pmb \sigma} &\quad \text{Second-order stress tensor [$\text{Pa}$ or $N/m^2$]} \\ {\pmb C} &\quad \text{Fourth-order stiffness tensor [$\text{Pa}$ or $N/m^2$]} \\ {\pmb u} &\quad \text{Displacement [$m$]} \\ \rho &\quad \text{Density [$kg/m^3$]} \\ {\pmb f} &\quad \text{External forcing [$N/m^3$]} \end{align}
In the isotropic elastic case, the stiffness tensor reduces to the following form in Voigt notation:
C=(λ+2μλλ000λλ+2μλ000λλλ+2μ000000μ000000μ000000μ).\pmb C = \begin{pmatrix} \lambda+2\mu & \lambda & \lambda & 0 & 0 & 0 \\ \lambda & \lambda+2\mu & \lambda & 0 & 0 & 0 \\ \lambda & \lambda & \lambda+2\mu & 0 & 0 & 0 \\ 0 & 0 & 0 & \mu& 0 & 0 \\ 0 & 0 & 0 & 0 & \mu& 0 \\ 0 & 0 & 0 & 0 & 0 & \mu \end{pmatrix}.
Note that to fully describe this material, we still also need to provide the density of the material. This is sometimes referred to as the scalar parameter in the wave equation.
The Salvus material module is able to convert any material from any symmetry class into this form. This ensures that any material can be simulated, no matter its level of anisotropy. An important point is however, that the materials won't store all independent components of the generic anisotropic stiffness tensor, but only those required for the specific symmetry class.
In the isotropic case, this means that the stiffness tensor parameterization will only store the two independent elliptical coefficients C11 and C12, as well as the density.
material_tc_isotropic = material.from_params(
    vp=5000, vs=3500, rho=2500
).to_tensor_components()  # All Salvus materials have this method

material_tc_isotropic
<IPython.core.display.HTML object>
salvus.material.elastic.isotropic.TensorComponents
If we compare the values in the stiffness tensor above with the computed Lame parameters, we can see that the definition for isotropic media above holds.
material_tc_isotropic_lame = elastic.isotropic.LameParameters.from_material(
    material_tc_isotropic
)

L, M = material_tc_isotropic_lame.LAM.p, material_tc_isotropic_lame.MU.p

print(f"L + 2M: {L + 2 * M:.1e}, L: {L:.1e}, M: {M:.1e}")
L + 2M: 6.2e+10, L: 1.2e+09, M: 3.1e+10
We can also inspect the symmetries present in the entries of the stiffness tensor. For example, we can see which entries should be zero for an isotropic material:
material_tc_isotropic.zero_components()
['C14',
 'C15',
 'C16',
 'C24',
 'C25',
 'C26',
 'C34',
 'C35',
 'C36',
 'C45',
 'C46',
 'C56']
We can also see which entries are equal to each other:
material_tc_isotropic.equal_components()
{'C22': 'C11',
 'C33': 'C11',
 'C23': 'C12',
 'C13': 'C12',
 'C44': 'halfC11minC12',
 'C55': 'halfC11minC12',
 'C66': 'halfC11minC12'}
Finally, when one wants to perform numerical computations on the stiffness tensor itself, it is often useful to have the tensor in a more low level container, like a NumPy array:
np.set_printoptions(precision=2, suppress=True)  # To avoid long outputs
material.utils.extract_tensor(material_tc_isotropic)
array([[6.25e+10, 1.25e+09, 1.25e+09, 0.00e+00, 0.00e+00, 0.00e+00],
       [1.25e+09, 6.25e+10, 1.25e+09, 0.00e+00, 0.00e+00, 0.00e+00],
       [1.25e+09, 1.25e+09, 6.25e+10, 0.00e+00, 0.00e+00, 0.00e+00],
       [0.00e+00, 0.00e+00, 0.00e+00, 3.06e+10, 0.00e+00, 0.00e+00],
       [0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 3.06e+10, 0.00e+00],
       [0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 3.06e+10]])
This method can be directly applied to any material (even those not directly parameterized by tensor components!), as well as on higher dimensional parameter structures:
material_isotropic = material.from_params(
    vp=5000, vs=np.array([[3100, 3100], [3050, 3050], [3070, 3070]]), rho=2500
)

stiffness_tensor_per_point = material.utils.extract_tensor(material_isotropic)

stiffness_tensor_per_point.shape
(3, 2, 6, 6)
Acoustic materials (those that don't sustain shear forces) can also be anisotropic. In this case, SalvusCompute also uses a tensor to describe the mechanical properties of the material. This tensor is a 3x3 matrix also containing the elliptic coefficients of the material. However, the one scalar parameter in the acoustic case is not the density, but the M0M_0 parameter.
The acoustic tensor is typically denoted with symbol D\pmb D and its entries as DijD_{ij}.
material_tc_acoustic = material.from_params(
    vp=5000, rho=2500
).to_tensor_components()

material_tc_acoustic
<IPython.core.display.HTML object>
salvus.material.acoustic.isotropic.TensorComponents
material_tc_acoustic.zero_components()
['D12', 'D13', 'D23']
material_tc_acoustic.equal_components()
{'D22': 'D11', 'D33': 'D11'}

Converting an acoustic material to an elastic one

SalvusMaterial doesn't allow direct conversion from an acoustic material to an elastic one, as information would need to be injected into the material. To convert an acoustic material to an elastic one, one must manually add missing parameters.
material_v_acoustic = acoustic.Velocity.from_material(
    # Because the M0/Dij parameterization is not understood in elastic
    # materials, we need to convert the material to velocities first
    material_tc_acoustic
)

# We need a way to generate the shear wave velocity from the P-wave velocity
vp_vs_ratio = 1.4

elastic.isotropic.Velocity.from_params(
    vp=material_v_acoustic.VP,
    rho=material_v_acoustic.RHO,
    # And we add on the missing parameters
    vs=material_v_acoustic.VP / vp_vs_ratio,
)
<IPython.core.display.HTML object>
salvus.material.elastic.isotropic.Velocity
The hexagonal symmetry class is a special case for Salvus: it is the only symmetry class that is separately supported by the solver. In Geophysics, often subsurface materials are described by horizontal and vertical velocities, defining a material that is isotropic in the horizontal plane but differs from those properties in the perpendicular direction.
This type of material occurs often in other fields as well -- it is a natural result of many thin layers of differing materials, see e.g. this work. Because it is such a common type of material, we will highlight converting this material from isotropic materials, and to fully anisotropic (triclinic) materials.
material_vti = material.from_params(
    vpv=5500,
    vph=5000,
    # Typically if VPV > VPH, then the reverse is true for S waves: VSH > VSV
    vsv=3200,
    vsh=3500,
    eta=0.1,  # Controls ellipticity of the wavefront
    rho=2500,
)

material_vti
<IPython.core.display.HTML object>
salvus.material.elastic.hexagonal.Velocity
This material can be used as-is in the layered mesher, and sent off to the solver. However, it is also possible to convert this material to a few other hexagonal materials (which are not yet supported by the solver).
print(elastic.hexagonal.__all__)
['EngineeringConstants', 'Velocity', 'TensorComponents', 'Thomsen']
We can now also see that the stiffness tensor for VTI materials decouples some of the equal components compared to the isotropic case:
material_tc_hexagonal = material_vti.to_tensor_components()

material_tc_hexagonal
<IPython.core.display.HTML object>
salvus.material.elastic.hexagonal.TensorComponents
Compared to the isotropic case, C33C_{33} has decoupled from C11C_{11} and C22C_{22}, C44C_{44} and C55C_{55} have decoupled from C66(=C12)C_{66}(=C_{12}), and C13C_{13} and C23C_{23} have decoupled from C12C_{12}. This leads to 3 new independent parameters, matching 3 (from isotropic) + 3 (new) = 6 independent parameters in total.
material_tc_hexagonal.equal_components()
{'C22': 'C11', 'C23': 'C13', 'C55': 'C44', 'C66': 'halfC11minC12'}
Because these materials are so common in earth sciences, we also provide a reduction method to convert an elastic hexagonal material an acoustic one. Note that is a reduction, not a conversion: the resulting material will contain less information than the original.
material_vti.to_acoustic()
<IPython.core.display.HTML object>
salvus.material.acoustic.hexagonal.Velocity
Another variation on hexagonal materials is the engineering constants form. This is a material described by the Young's modulus, Poisson's ratio and shear modulus, but under hexagonal anisotropic conditions. What this exactly means is given in the docstring of the class:
# To print all methods and docstrings, use:
help(elastic.hexagonal.EngineeringConstants)
# To print just the main docstring, use:
# print(elastic.hexagonal.EngineeringConstants.__doc__)
Help on class EngineeringConstants in module salvus.material.elastic.hexagonal:

class EngineeringConstants(_Hexagonal)
 |  EngineeringConstants(RHO: '_pd.FC', E1: '_pd.FC', E3: '_pd.FC', G13: '_pd.FC', V12: '_pd.FC', V13: '_pd.FC') -> None
 |  
 |  An hexagonal material parametrized with engineering constants.
 |  
 |  An hexagonal material has two planes of symmetry and therefore 6
 |  independent parameters plus the density.
 |  
 |  The shear moduli here are assumed to be in the engineering convention:
 |  twice the normal shear moduli.
 |  
 |  The following equalities hold compared to the full orthotropic case:
 |  - `E2 = E1`
 |  - `V32 = V31`
 |  - `V23 = V13`
 |  - `V31 / E3 = V13 / E1`
 |  - `V21 = V12`
 |  - `G21 = E2 / (2 * (1 + V21))`
 |  
 |  The normal and reverse Poisson's ratios are also named the major and minor
 |  Poisson's ratio, depending on which has the larger magnitude. However, this
 |  material always defines it by first and second axes, i.e. `v12` and `v13`.
 |  
 |  This material is rotationally symmetric around its vertical, the dimensions
 |  described in these constants by the index 3. This means that:
 |  - the Young's modulus in the 1 and 2 dimensions are equal.
 |  - The two Poisson's ratio between the third and the two other dimensions
 |    are equal, as are their reverse Poisson's ratio.
 |  - The Poisson's ratio between dimension 1 and dimension 2 is the same as
 |    its reverse.
 |  
 |  Args:
 |      RHO: The density in kg / m^3.
 |      E1: Young's modulus in Pa.
 |      E3: Young's modulus in Pa.
 |      G13: Shear modulus in Pa, engineering convention.
 |      V12: Poisson's ratio.
 |      V13: Poisson's ratio.
 |  
 |  Method resolution order:
 |      EngineeringConstants
 |      _Hexagonal
 |      salvus.material.base_materials.AllowsOrientation
 |      salvus.material.elastic._ElasticMaterial
 |      salvus.material.base_materials.PhysicalMaterial
 |      salvus.material.base_materials.Material
 |      salvus.flow.utils.serialization_helpers.SerializationMixin
 |      salvus.utils.dataclass_utils.MappableDataclass
 |      salvus.material.base_materials.AllowsAttenuation
 |      salvus.utils.dataclass_utils.DataclassInstance
 |      typing_extensions.Protocol
 |      typing.Protocol
 |      abc.ABC
 |      typing.Generic
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  __delattr__(self, name)
 |      Implement delattr(self, name).
 |  
 |  __eq__(self, other)
 |      Return self==value.
 |  
 |  __hash__(self)
 |      Return hash(self).
 |  
 |  __init__(self, RHO: '_pd.FC', E1: '_pd.FC', E3: '_pd.FC', G13: '_pd.FC', V12: '_pd.FC', V13: '_pd.FC') -> None
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  
 |  __setattr__(self, name, value)
 |      Implement setattr(self, name, value).
 |  
 |  __subclasshook__ = _proto_hook(other) from typing.Protocol.__init_subclass__.<locals>
 |      # Set (or override) the protocol subclass hook.
 |  
 |  ----------------------------------------------------------------------
 |  Class methods defined here:
 |  
 |  from_params(*, rho: '_pd.R', e1: '_pd.R', e3: '_pd.R', g13: '_pd.R', v12: '_pd.R', v13: '_pd.R') -> '_H'
 |      Construct an hexagonal material from its engineering constants.
 |      
 |      The shear moduli here are assumed to be in the engineering convention:
 |      twice the normal shear moduli.
 |      
 |      Args:
 |          rho: The density in kg / m^3.
 |          e1: Young's modulus in Pa.
 |          e3: Young's modulus in Pa.
 |          g13: Shear modulus in Pa, engineering convention.
 |          v12: Poisson's ratio.
 |          v13: Poisson's ratio.
 |  
 |  from_tensor_components(m: 'GenericTensorComponents[_pd.F]', *, reduction_method: "typing.Literal['remove-components', 'force'] | None" = None) -> '_H'
 |      Create material from tensor components acoustic parameter material.
 |      
 |      Overwrite this method if your material can not be constructed from
 |      acoustic constants.
 |      
 |      A class method to create a anisotropic material of a desired symmetry
 |      class from a canonical TC material. The method will automatically check
 |      if the canonical material that is passed meets the symmetry
 |      requirements of the desired materials. If it does not, a TypeError will
 |      be raised.
 |      
 |      Args:
 |          m: The material in tensor components parametrization to be used to
 |              construct the new material.
 |          reduction_method: Method to move between incompatible symmetry
 |              classes. None will only move to symmetries that are equal or
 |              more permissive, while `remove-components` will drop components
 |              that are found to match the new material's constraints as
 |              necessary, and thus leading to loss of free parameters but not
 |              of information. The option "force" will take all information
 |              necessary to construct the new parameter set without
 |              verification, leading to loss of information.
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |  
 |  __abstractmethods__ = frozenset()
 |  
 |  __annotations__ = {'E1': '_pd.FC', 'E3': '_pd.FC', 'G13': '_pd.FC', 'R...
 |  
 |  __dataclass_fields__ = {'E1': Field(name='E1',type='_pd.FC',default=<d...
 |  
 |  __dataclass_params__ = _DataclassParams(init=True,repr=False,eq=True,o...
 |  
 |  __match_args__ = ('RHO', 'E1', 'E3', 'G13', 'V12', 'V13')
 |  
 |  __orig_bases__ = (salvus.material.elastic.hexagonal._Hexagonal[~F],)
 |  
 |  __parameters__ = (~F,)
 |  
 |  ----------------------------------------------------------------------
 |  Class methods inherited from _Hexagonal:
 |  
 |  from_material(m: 'Material', *, reduction_method: "typing.Literal['remove-components', 'force'] | None" = None) -> '_Hexagonal'
 |      Construct this material from another within the same physical system.
 |      
 |      Args:
 |          m: Material to transform.
 |          reduction_method: Method to move between incompatible symmetry
 |              classes. None will only move to symmetries that are equal or
 |              more permissive, while `remove-components` will drop components
 |              that are found to match the new material's constraints as
 |              necessary, and thus leading to loss of free parameters but not
 |              of information. The option "force" will take all information
 |              necessary to construct the new parameter set without
 |              verification, leading to loss of information.
 |      
 |      Returns:
 |          The transformed material.
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from salvus.material.base_materials.AllowsOrientation:
 |  
 |  with_orientation(self, orientation: 'Material | None') -> 'Material'
 |      Experimental way to add orientation to a material.
 |      
 |      Args:
 |          orientation: The orientation.
 |      
 |      Returns:
 |          The object with an orientation material attached.
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes inherited from salvus.material.base_materials.AllowsOrientation:
 |  
 |  __protocol_attrs__ = {'__dataclass_fields__', 'orientation', 'with_ori...
 |  
 |  orientation = None
 |  
 |  ----------------------------------------------------------------------
 |  Class methods inherited from salvus.material.elastic._ElasticMaterial:
 |  
 |  material_system() -> 'type[PhysicalMaterial]'
 |      Get the material system of the material.
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from salvus.material.base_materials.PhysicalMaterial:
 |  
 |  to_tensor_components(self, *, expand_symmetries: 'bool' = False) -> 'MaterialDict | GenericTensorComponents'
 |      Generate a tensor component representation of the material.
 |      
 |      This method ensures compatibility with solver and other symmetries.
 |      
 |      Args:
 |          expand_symmetries: boolean determining if to return a expanded
 |              canonical parameters instead of the TensorComponents object in
 |              the relevant symmetry system. Defaults to False.
 |      
 |      Returns:
 |          The material in tensor component form.
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from salvus.material.base_materials.Material:
 |  
 |  __getattribute__(self, name)
 |      Allow for direct access to a materials parameters.
 |  
 |  __post_init__(self)
 |      Try to fix type errors by casting to a parameter type.
 |  
 |  __repr__(self) -> 'str'
 |  
 |  __str__(self) -> 'str'
 |      Return a string representation of the material.
 |  
 |  map_realized_parameters(self, *, f_constant: 'typing.Callable[[str, _pd.RealizedConstantParameter], _pd.RealizedConstantParameter]' = <cyfunction _map_realized_default at 0x70efff046bc0>, f_discrete: 'typing.Callable[[str, _pd.RealizedDiscreteParameter], _pd.RealizedDiscreteParameter]' = <cyfunction _map_realized_default at 0x70efff046bc0>, f_analytic: 'typing.Callable[[str, _pd.RealizedAnalyticParameter], _pd.RealizedAnalyticParameter]' = <cyfunction _map_realized_default at 0x70efff046bc0>) -> 'Self'
 |      Apply functions to each parameter individually, distinguishing _pd.
 |      
 |      Useful when one wants to transform each parameter type separately. For
 |      instance, transformations of discrete parameters often require more
 |      associated logic than their constant equivalents. This function
 |      abstracts away the boilerplate of check for each parameter type, and
 |      subsequently transforming it with some function, as well as ensuring
 |      that the parameters are indeed of the correct realized type.
 |      
 |      The signatures of each transformation function should take the
 |      parameter's name and value as two distinct inputs, and return the
 |      (potentially modified) parameter value.
 |      
 |      Args:
 |          f_constant: The function to apply to constant parameters. Defaults
 |              to returning the parameter as-is.
 |          f_discrete: The function to apply to discrete parameters. Defaults
 |              to returning the parameter as-is.
 |          f_analytic: The function to apply to analytic parameters. Defaults
 |              to returning the parameter as-is.
 |  
 |  to_wavelength_oracle(self, n_dim: 'typing.Literal[2, 3] | None' = None) -> '_pd.FC'
 |      The wavelength oracle.
 |      
 |      Args:
 |          n_dim: Dimension to return the oracle for, deprecated.
 |  
 |  ----------------------------------------------------------------------
 |  Class methods inherited from salvus.material.base_materials.Material:
 |  
 |  from_dataset(ds: 'xr.Dataset') -> 'Material[_pd.F]'
 |      Construct a material from an xarray Dataset.
 |      
 |      Args:
 |          ds: The dataset to construct the material from.
 |  
 |  ----------------------------------------------------------------------
 |  Readonly properties inherited from salvus.material.base_materials.Material:
 |  
 |  ds
 |      Material's xarray representation.
 |  
 |  flatten
 |      Get all parameters as a dict.
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from salvus.flow.utils.serialization_helpers.SerializationMixin:
 |  
 |  __ne__(self, other) -> 'bool'
 |  
 |  to_json(self, external_file_hash: 'typing.Optional[str]' = None) -> 'dict'
 |      Serialize the object to dictionary that can be written to JSON.
 |      
 |      Args:
 |          external_file_hash: Hash of any external files associated with
 |              this object. Can be passed here in which case it will be
 |              stored in a centralized location in the JSON file.
 |  
 |  ----------------------------------------------------------------------
 |  Class methods inherited from salvus.flow.utils.serialization_helpers.SerializationMixin:
 |  
 |  from_json(d: 'dict') -> 'typing.Any'
 |      Recreate the object from a dictionary serialization of its
 |      initialization parameters.
 |      
 |      Args:
 |          d: Dictionary containing its init parameters and a few other
 |              things.
 |  
 |  ----------------------------------------------------------------------
 |  Static methods inherited from salvus.flow.utils.serialization_helpers.SerializationMixin:
 |  
 |  __new__(cls, *args, **kwargs)
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from salvus.flow.utils.serialization_helpers.SerializationMixin:
 |  
 |  __dict__
 |      dictionary for instance variables
 |  
 |  __weakref__
 |      list of weak references to the object
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from salvus.utils.dataclass_utils.MappableDataclass:
 |  
 |  map(self: 'typing_extensions.Self', f: 'typing.Callable[[str, typing.Any], tuple[str, typing.Any]]') -> 'typing_extensions.Self'
 |      Generic map for dataclass instances.
 |      
 |      `f` should be a function taking two parameters: the name of the
 |      dataclass member and its value, and it should return a tuple containing
 |      the same quantities. If a member is not to be transformed, `f` should
 |      just return a tuple of the input member name and value, unchanged. Both
 |      names and values can be transformed, with the semantics following those
 |      of `dataclasses.replace`.
 |      
 |      In Salvus we primarily treat dataclasses as containers offering
 |      semantics similar to typed dictionaries. Deriving from this protocol
 |      allows any relevant dataclass to additionally be treated functorially.
 |      This allows for the generic un- and re-wrapping of value held in
 |      dataclasses, and essentially replaces the following imperative code:
 |      
 |      ```python
 |      @dataclass
 |      class A:
 |          member: int
 |      
 |      # Before
 |      my_a = A(member=1)
 |      my_a_new = dataclasses.replace(my_a, member=2 * my_a.member)
 |      
 |      # After
 |      my_a_new = A(val=1).map(lambda key, val: (key, 2 * val))
 |      ```
 |      
 |      As with many functional patterns, the perceived benefits for simple
 |      demonstrative purposes is minimal. The scalability of this pattern
 |      becomes apparent, however, when parsing deeply nested abstractions, as
 |      the transformation logic can be factored out into independent
 |      functions. This is used extensively, for example, in the realization
 |      logic of the layered mesher, where generic materials can have generic
 |      parameters, etc.
 |      
 |      Args:
 |          f: The function to map over the dataclass.
 |      
 |      Returns:
 |          A new, potentially transformed, dataclass.
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from salvus.material.base_materials.AllowsAttenuation:
 |  
 |  with_attenuation(self, attenuation: 'Material | None') -> 'Self'
 |      Add attenuation to an object.
 |      
 |      Args:
 |          attenuation: The attenuation material.
 |      
 |      Returns:
 |          The object with an attenuation material attached.
 |  
 |  ----------------------------------------------------------------------
 |  Readonly properties inherited from salvus.material.base_materials.AllowsAttenuation:
 |  
 |  viscosity
 |      Get the optional attenuation.
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes inherited from salvus.material.base_materials.AllowsAttenuation:
 |  
 |  attenuation = None
 |  
 |  ----------------------------------------------------------------------
 |  Class methods inherited from typing_extensions.Protocol:
 |  
 |  __init_subclass__(*args, **kwargs)
 |      This method is called when a class is subclassed.
 |      
 |      The default implementation does nothing. It may be
 |      overridden to extend subclasses.
 |  
 |  ----------------------------------------------------------------------
 |  Class methods inherited from typing.Generic:
 |  
 |  __class_getitem__(params)
 |      Parameterizes a generic class.
 |      
 |      At least, parameterizing a generic class is the *main* thing this method
 |      does. For example, for some generic class `Foo`, this is called when we
 |      do `Foo[int]` - there, with `cls=Foo` and `params=int`.
 |      
 |      However, note that this method is also called when defining generic
 |      classes in the first place with `class Foo(Generic[T]): ...`.

Let's see what the engineering constants are for our hexagonal material:
elastic.hexagonal.EngineeringConstants.from_material(material_tc_hexagonal)
<IPython.core.display.HTML object>
salvus.material.elastic.hexagonal.EngineeringConstants
Other elastic symmetry classes (except triclinic) are not directly supported by the solver, but can be interesting as well to defined your materials:
print(f"Cubic materials: {elastic.cubic.__all__}")
print(f"Orthotropic materials: {elastic.orthotropic.__all__}")
print(f"Monoclinic materials: {elastic.monoclinic.__all__}")
Cubic materials: ['TensorComponents']
Orthotropic materials: ['EngineeringConstants', 'TensorComponents']
Monoclinic materials: ['TensorComponents']
For acoustic, the following symmetry classes are supported:
print(f"Isotropic materials: {acoustic.isotropic.__all__}")
print(
    f"Hexagonal elliptical materials: {acoustic.elliptical_hexagonal.__all__}"
)
print(f"Hexagonal materials: {acoustic.hexagonal.__all__}")
print(f"Orthotropic materials: {acoustic.orthotropic.__all__}")
Isotropic materials: ['BulkModulus', 'LinearCoefficients', 'Velocity']
Hexagonal elliptical materials: ['Velocity', '_TensorComponents2D', 'TensorComponents']
Hexagonal materials: ['Thomsen', 'Velocity', '_TensorComponents2D', 'TensorComponents']
Orthotropic materials: ['TensorComponents']
The most general equivalence class in the entire set of anisotropic classes is the triclinic class. This class has 21 + 1 (density) = 22 independent parameters, and allows for the most generic waveform propagation. It is the last of the three equivalence classes for which a parameterization is supported by the solver, namely that of directly the elliptic coefficients, i.e. the stiffness tensor in Voigt notation:
material_tc_triclinic = elastic.triclinic.TensorComponents.from_params(
    **{
        # fmt: off
        "rho": 2500,

        "c11": 6.25e10,
        "c12": 1.25e9,
        "c13": 1.13e9,
        "c14": 0.7e9,
        "c15": 0.8e9,
        "c16": 0.9e9,

        "c22": 7.2e10,
        "c23": 1.15e9,
        "c24": 0.8e9,
        "c25": 0.9e9,
        "c26": 1.0e9,
        
        "c33": 7.8e10,
        "c34": 1.2e9,
        "c35": 1.3e9,
        "c36": 1.4e9,

        "c44": 2.7e10,
        "c45": 1.2e9,
        "c46": 1.2e9,
        
        "c55": 2.56e10,
        "c56": 1.3e9,

        "c66": 2.3e10,
        # fmt: on
    }
)

material_tc_triclinic
<IPython.core.display.HTML object>
salvus.material.elastic.triclinic.TensorComponents
Although it is quite hard to interpret the material in this form, we can still extract the wavelength at 1 Hz for this material. Internally, Salvus solves the eigenvalue problem for the stiffness tensor, and uses the slowest wave speed to compute the wavelength.
wavelenght = material_tc_triclinic.to_wavelength_oracle().p
print(f"Wavelength at 1 Hz: {wavelenght:.1f} m")
Wavelength at 1 Hz: 2990.4 m
Salvus will typically produce an error if you try to convert a material that is incompatible with the target material.
# Going against the symmetry class ordering isn't possible
try:
    elastic.isotropic.Velocity.from_material(material_tc_triclinic)
except TypeError as e:
    print(textwrap.fill(f"Error: {e}", width=80))
Error: The desired material type <class
'salvus.material.elastic.isotropic.TensorComponents'> can't accommodate all
parameters in the passed material of type <class
'salvus.material.elastic.triclinic.TensorComponents'>. If you think the material
does have the symmetry although it is not in the right symmetry form, retry with
`reduction_method="remove-components"`.
# Elastic to acoustic (or vice versa) conversion is not supported generally
try:
    acoustic.isotropic.Velocity.from_material(material_tc_triclinic)
except TypeError as e:
    print(textwrap.fill(f"Error: {e}", width=80))
Error: Materials of type <class
'salvus.material.elastic.triclinic.TensorComponents'> is not compatible with
materials in the system (<class 'salvus.material.acoustic._AcousticMaterial'>,).
PAGE CONTENTS