Source code for ogstools.propertylib.tensor_math

# Copyright (c) 2012-2024, OpenGeoSys Community (http://www.opengeosys.org)
#            Distributed under a Modified BSD License.
#            See accompanying file LICENSE.txt or
#            http://www.opengeosys.org/project/license
#

"""Common tensor transformation operations.

They can be used as they are, but are also part of
:py:obj:`ogstools.propertylib.matrix.Matrix` properties.
All input arrays are expected to be in vector notation of symmetric tensors in
the form of:

[xx, yy, zz, xy] for 2D and

[xx, yy, zz, xy, yz, xz] for 3D.

This notation style is the default output of OGS:

<https://www.opengeosys.org/docs/userguide/basics/conventions/#a-namesymmetric-tensorsa--symmetric-tensors-and-kelvin-mapping>

A better overview for the theoretical background of the equations can be found
here, for example:

<https://en.wikipedia.org/wiki/Cauchy_stress_tensor#Cauchy's_stress_theorem%E2%80%94stress_tensor>
"""

from typing import Optional, TypeVar, Union

import numpy as np
from numpy import linalg as LA
from pint.facets.plain import PlainQuantity, PlainUnit

from .unit_registry import u_reg

ValType = Union[PlainQuantity, np.ndarray]


T = TypeVar("T")


def _split_quantity(values: ValType) -> tuple[np.ndarray, Optional[PlainUnit]]:
    if isinstance(values, PlainQuantity):
        return values.magnitude, values.units
    return values, None


def _to_quantity(
    values: np.ndarray, unit: Optional[PlainUnit]
) -> Union[np.ndarray, PlainQuantity]:
    return values if unit is None else u_reg.Quantity(values, unit)


[docs] def identity(vals: T) -> T: ":returns: The input values." return vals
[docs] def sym_tensor_to_mat(vals: np.ndarray) -> np.ndarray: "Convert an symmetric tensor to a 3x3 matrix." assert np.shape(vals)[-1] in [4, 6] shape = list(np.shape(vals))[:-1] + [3, 3] mat = np.zeros(shape) idx = np.asarray([[0, 0], [1, 1], [2, 2], [0, 1], [1, 2], [0, 2]]) for i in range(np.shape(vals)[-1]): mat[..., idx[i, 0], idx[i, 1]] = vals[..., i] return mat
[docs] def trace(values: ValType) -> ValType: """Return the trace. :math:`tr(\\mathbf{\\sigma}) = \\sum\\limits_{i=1}^3 \\sigma_{ii}` """ vals, unit = _split_quantity(values) return _to_quantity(np.sum(vals[..., :3], axis=-1), unit)
[docs] def eigenvalues(values: ValType) -> ValType: "Return the eigenvalues." vals, unit = _split_quantity(values) eigvals: np.ndarray = LA.eigvals(sym_tensor_to_mat(vals)) eigvals.sort(axis=-1) assert np.all(eigvals[..., 0] <= eigvals[..., 1]) assert np.all(eigvals[..., 1] <= eigvals[..., 2]) return _to_quantity(eigvals, unit)
[docs] def eigenvectors(values: ValType) -> ValType: "Return the eigenvectors." vals, unit = _split_quantity(values) eigvals, eigvecs = LA.eig(sym_tensor_to_mat(vals)) ids = eigvals.argsort(axis=-1) eigvals = np.take_along_axis(eigvals, ids, axis=-1) eigvecs = np.take_along_axis(eigvecs, ids[:, np.newaxis], axis=-1) assert np.all(eigvals[..., 0] <= eigvals[..., 1]) assert np.all(eigvals[..., 1] <= eigvals[..., 2]) return _to_quantity(eigvecs, unit)
[docs] def det(values: ValType) -> ValType: "Return the determinants." vals, unit = _split_quantity(values) return _to_quantity(np.linalg.det(sym_tensor_to_mat(vals)), unit)
[docs] def frobenius_norm(values: ValType) -> ValType: """Return the Frobenius norm. :math:`||\\mathbf{\\sigma}||_F = \\sqrt{ \\sum\\limits_{i=1}^m \\sum\\limits_{j=1}^n |\\sigma_{ij}|^2 }` """ vals, unit = _split_quantity(values) return _to_quantity( np.linalg.norm(sym_tensor_to_mat(vals), axis=(-2, -1)), unit )
[docs] def invariant_1(values: ValType) -> ValType: """Return the first invariant. :math:`I1 = tr(\\mathbf{\\sigma})` """ return trace(values)
[docs] def invariant_2(values: ValType) -> ValType: """Return the second invariant. :math:`I2 = \\frac{1}{2} \\left[(tr(\\mathbf{\\sigma}))^2 - tr(\\mathbf{\\sigma}^2) \\right]` """ return 0.5 * (trace(values) ** 2 - trace(values**2))
[docs] def invariant_3(values: ValType) -> ValType: """Return the third invariant. :math:`I3 = det(\\mathbf{\\sigma})` """ return det(values)
[docs] def mean(values: ValType) -> ValType: """Return the mean value. Also called hydrostatic component or octahedral normal component. :math:`\\pi = \\frac{1}{3} I1` """ return (1.0 / 3.0) * invariant_1(values)
[docs] def effective_pressure(values: ValType) -> ValType: """Return the effective pressure. :math:`\\pi = -\\frac{1}{3} I1` """ return -mean(values)
[docs] def hydrostatic_component(values: ValType) -> ValType: """Return the hydrostatic component. :math:`p_{ij} = \\pi \\delta_{ij}` """ vals, unit = _split_quantity(values) result = vals * 0.0 result[..., :3] = _split_quantity(mean(vals))[0][..., np.newaxis] return _to_quantity(result, unit)
[docs] def deviator(values: ValType) -> ValType: """Return the deviator. :math:`s_{ij} = \\sigma_{ij} - \\pi \\delta_{ij}` """ return values - hydrostatic_component(values)
[docs] def deviator_invariant_1(values: ValType) -> ValType: """Return the first invariant of the deviator. :math:`J1 = 0` """ return trace(deviator(values))
[docs] def deviator_invariant_2(values: ValType) -> ValType: """Return the second invariant of the deviator. :math:`J2 = \\frac{1}{2} tr(\\mathbf{s}^2)` """ return 0.5 * trace(deviator(values) ** 2)
[docs] def deviator_invariant_3(values: ValType) -> ValType: """Return the third invariant of the deviator. :math:`J3 = \\frac{1}{3} tr(\\mathbf{s}^3)` """ return (1.0 / 3.0) * trace(deviator(values) ** 3)
[docs] def octahedral_shear(values: ValType) -> ValType: """Return the octahedral shear value. :math:`\\tau_{oct} = \\sqrt{\\frac{2}{3} J2}` """ return np.sqrt((2.0 / 3.0) * deviator_invariant_2(values))
[docs] def von_mises(values: ValType) -> ValType: """Return the von Mises stress. :math:`\\sigma_{Mises} = \\sqrt{3 J2}` """ return np.sqrt(3.0 * deviator_invariant_2(values))
[docs] def qp_ratio(values: ValType) -> ValType: """Return the qp ratio (von Mises stress / effective pressure). :math:`qp = \\sigma_{Mises} / \\pi` """ return von_mises(values) / effective_pressure(values)