You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			1575 lines
		
	
	
		
			61 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			1575 lines
		
	
	
		
			61 KiB
		
	
	
	
		
			Python
		
	
"""
 | 
						|
Interpolation inside triangular grids.
 | 
						|
"""
 | 
						|
 | 
						|
import numpy as np
 | 
						|
 | 
						|
from matplotlib import _api
 | 
						|
from matplotlib.tri import Triangulation
 | 
						|
from matplotlib.tri._trifinder import TriFinder
 | 
						|
from matplotlib.tri._tritools import TriAnalyzer
 | 
						|
 | 
						|
__all__ = ('TriInterpolator', 'LinearTriInterpolator', 'CubicTriInterpolator')
 | 
						|
 | 
						|
 | 
						|
class TriInterpolator:
 | 
						|
    """
 | 
						|
    Abstract base class for classes used to interpolate on a triangular grid.
 | 
						|
 | 
						|
    Derived classes implement the following methods:
 | 
						|
 | 
						|
    - ``__call__(x, y)``,
 | 
						|
      where x, y are array-like point coordinates of the same shape, and
 | 
						|
      that returns a masked array of the same shape containing the
 | 
						|
      interpolated z-values.
 | 
						|
 | 
						|
    - ``gradient(x, y)``,
 | 
						|
      where x, y are array-like point coordinates of the same
 | 
						|
      shape, and that returns a list of 2 masked arrays of the same shape
 | 
						|
      containing the 2 derivatives of the interpolator (derivatives of
 | 
						|
      interpolated z values with respect to x and y).
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, triangulation, z, trifinder=None):
 | 
						|
        _api.check_isinstance(Triangulation, triangulation=triangulation)
 | 
						|
        self._triangulation = triangulation
 | 
						|
 | 
						|
        self._z = np.asarray(z)
 | 
						|
        if self._z.shape != self._triangulation.x.shape:
 | 
						|
            raise ValueError("z array must have same length as triangulation x"
 | 
						|
                             " and y arrays")
 | 
						|
 | 
						|
        _api.check_isinstance((TriFinder, None), trifinder=trifinder)
 | 
						|
        self._trifinder = trifinder or self._triangulation.get_trifinder()
 | 
						|
 | 
						|
        # Default scaling factors : 1.0 (= no scaling)
 | 
						|
        # Scaling may be used for interpolations for which the order of
 | 
						|
        # magnitude of x, y has an impact on the interpolant definition.
 | 
						|
        # Please refer to :meth:`_interpolate_multikeys` for details.
 | 
						|
        self._unit_x = 1.0
 | 
						|
        self._unit_y = 1.0
 | 
						|
 | 
						|
        # Default triangle renumbering: None (= no renumbering)
 | 
						|
        # Renumbering may be used to avoid unnecessary computations
 | 
						|
        # if complex calculations are done inside the Interpolator.
 | 
						|
        # Please refer to :meth:`_interpolate_multikeys` for details.
 | 
						|
        self._tri_renum = None
 | 
						|
 | 
						|
    # __call__ and gradient docstrings are shared by all subclasses
 | 
						|
    # (except, if needed, relevant additions).
 | 
						|
    # However these methods are only implemented in subclasses to avoid
 | 
						|
    # confusion in the documentation.
 | 
						|
    _docstring__call__ = """
 | 
						|
        Returns a masked array containing interpolated values at the specified
 | 
						|
        (x, y) points.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        x, y : array-like
 | 
						|
            x and y coordinates of the same shape and any number of
 | 
						|
            dimensions.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        np.ma.array
 | 
						|
            Masked array of the same shape as *x* and *y*; values corresponding
 | 
						|
            to (*x*, *y*) points outside of the triangulation are masked out.
 | 
						|
 | 
						|
        """
 | 
						|
 | 
						|
    _docstringgradient = r"""
 | 
						|
        Returns a list of 2 masked arrays containing interpolated derivatives
 | 
						|
        at the specified (x, y) points.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        x, y : array-like
 | 
						|
            x and y coordinates of the same shape and any number of
 | 
						|
            dimensions.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        dzdx, dzdy : np.ma.array
 | 
						|
            2 masked arrays of the same shape as *x* and *y*; values
 | 
						|
            corresponding to (x, y) points outside of the triangulation
 | 
						|
            are masked out.
 | 
						|
            The first returned array contains the values of
 | 
						|
            :math:`\frac{\partial z}{\partial x}` and the second those of
 | 
						|
            :math:`\frac{\partial z}{\partial y}`.
 | 
						|
 | 
						|
        """
 | 
						|
 | 
						|
    def _interpolate_multikeys(self, x, y, tri_index=None,
 | 
						|
                               return_keys=('z',)):
 | 
						|
        """
 | 
						|
        Versatile (private) method defined for all TriInterpolators.
 | 
						|
 | 
						|
        :meth:`_interpolate_multikeys` is a wrapper around method
 | 
						|
        :meth:`_interpolate_single_key` (to be defined in the child
 | 
						|
        subclasses).
 | 
						|
        :meth:`_interpolate_single_key actually performs the interpolation,
 | 
						|
        but only for 1-dimensional inputs and at valid locations (inside
 | 
						|
        unmasked triangles of the triangulation).
 | 
						|
 | 
						|
        The purpose of :meth:`_interpolate_multikeys` is to implement the
 | 
						|
        following common tasks needed in all subclasses implementations:
 | 
						|
 | 
						|
        - calculation of containing triangles
 | 
						|
        - dealing with more than one interpolation request at the same
 | 
						|
          location (e.g., if the 2 derivatives are requested, it is
 | 
						|
          unnecessary to compute the containing triangles twice)
 | 
						|
        - scaling according to self._unit_x, self._unit_y
 | 
						|
        - dealing with points outside of the grid (with fill value np.nan)
 | 
						|
        - dealing with multi-dimensional *x*, *y* arrays: flattening for
 | 
						|
          :meth:`_interpolate_params` call and final reshaping.
 | 
						|
 | 
						|
        (Note that np.vectorize could do most of those things very well for
 | 
						|
        you, but it does it by function evaluations over successive tuples of
 | 
						|
        the input arrays. Therefore, this tends to be more time-consuming than
 | 
						|
        using optimized numpy functions - e.g., np.dot - which can be used
 | 
						|
        easily on the flattened inputs, in the child-subclass methods
 | 
						|
        :meth:`_interpolate_single_key`.)
 | 
						|
 | 
						|
        It is guaranteed that the calls to :meth:`_interpolate_single_key`
 | 
						|
        will be done with flattened (1-d) array-like input parameters *x*, *y*
 | 
						|
        and with flattened, valid `tri_index` arrays (no -1 index allowed).
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        x, y : array-like
 | 
						|
            x and y coordinates where interpolated values are requested.
 | 
						|
        tri_index : array-like of int, optional
 | 
						|
            Array of the containing triangle indices, same shape as
 | 
						|
            *x* and *y*. Defaults to None. If None, these indices
 | 
						|
            will be computed by a TriFinder instance.
 | 
						|
            (Note: For point outside the grid, tri_index[ipt] shall be -1).
 | 
						|
        return_keys : tuple of keys from {'z', 'dzdx', 'dzdy'}
 | 
						|
            Defines the interpolation arrays to return, and in which order.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        list of arrays
 | 
						|
            Each array-like contains the expected interpolated values in the
 | 
						|
            order defined by *return_keys* parameter.
 | 
						|
        """
 | 
						|
        # Flattening and rescaling inputs arrays x, y
 | 
						|
        # (initial shape is stored for output)
 | 
						|
        x = np.asarray(x, dtype=np.float64)
 | 
						|
        y = np.asarray(y, dtype=np.float64)
 | 
						|
        sh_ret = x.shape
 | 
						|
        if x.shape != y.shape:
 | 
						|
            raise ValueError("x and y shall have same shapes."
 | 
						|
                             f" Given: {x.shape} and {y.shape}")
 | 
						|
        x = np.ravel(x)
 | 
						|
        y = np.ravel(y)
 | 
						|
        x_scaled = x/self._unit_x
 | 
						|
        y_scaled = y/self._unit_y
 | 
						|
        size_ret = np.size(x_scaled)
 | 
						|
 | 
						|
        # Computes & ravels the element indexes, extract the valid ones.
 | 
						|
        if tri_index is None:
 | 
						|
            tri_index = self._trifinder(x, y)
 | 
						|
        else:
 | 
						|
            if tri_index.shape != sh_ret:
 | 
						|
                raise ValueError(
 | 
						|
                    "tri_index array is provided and shall"
 | 
						|
                    " have same shape as x and y. Given: "
 | 
						|
                    f"{tri_index.shape} and {sh_ret}")
 | 
						|
            tri_index = np.ravel(tri_index)
 | 
						|
 | 
						|
        mask_in = (tri_index != -1)
 | 
						|
        if self._tri_renum is None:
 | 
						|
            valid_tri_index = tri_index[mask_in]
 | 
						|
        else:
 | 
						|
            valid_tri_index = self._tri_renum[tri_index[mask_in]]
 | 
						|
        valid_x = x_scaled[mask_in]
 | 
						|
        valid_y = y_scaled[mask_in]
 | 
						|
 | 
						|
        ret = []
 | 
						|
        for return_key in return_keys:
 | 
						|
            # Find the return index associated with the key.
 | 
						|
            try:
 | 
						|
                return_index = {'z': 0, 'dzdx': 1, 'dzdy': 2}[return_key]
 | 
						|
            except KeyError as err:
 | 
						|
                raise ValueError("return_keys items shall take values in"
 | 
						|
                                 " {'z', 'dzdx', 'dzdy'}") from err
 | 
						|
 | 
						|
            # Sets the scale factor for f & df components
 | 
						|
            scale = [1., 1./self._unit_x, 1./self._unit_y][return_index]
 | 
						|
 | 
						|
            # Computes the interpolation
 | 
						|
            ret_loc = np.empty(size_ret, dtype=np.float64)
 | 
						|
            ret_loc[~mask_in] = np.nan
 | 
						|
            ret_loc[mask_in] = self._interpolate_single_key(
 | 
						|
                return_key, valid_tri_index, valid_x, valid_y) * scale
 | 
						|
            ret += [np.ma.masked_invalid(ret_loc.reshape(sh_ret), copy=False)]
 | 
						|
 | 
						|
        return ret
 | 
						|
 | 
						|
    def _interpolate_single_key(self, return_key, tri_index, x, y):
 | 
						|
        """
 | 
						|
        Interpolate at points belonging to the triangulation
 | 
						|
        (inside an unmasked triangles).
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        return_key : {'z', 'dzdx', 'dzdy'}
 | 
						|
            The requested values (z or its derivatives).
 | 
						|
        tri_index : 1D int array
 | 
						|
            Valid triangle index (cannot be -1).
 | 
						|
        x, y : 1D arrays, same shape as `tri_index`
 | 
						|
            Valid locations where interpolation is requested.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        1-d array
 | 
						|
            Returned array of the same size as *tri_index*
 | 
						|
        """
 | 
						|
        raise NotImplementedError("TriInterpolator subclasses" +
 | 
						|
                                  "should implement _interpolate_single_key!")
 | 
						|
 | 
						|
 | 
						|
class LinearTriInterpolator(TriInterpolator):
 | 
						|
    """
 | 
						|
    Linear interpolator on a triangular grid.
 | 
						|
 | 
						|
    Each triangle is represented by a plane so that an interpolated value at
 | 
						|
    point (x, y) lies on the plane of the triangle containing (x, y).
 | 
						|
    Interpolated values are therefore continuous across the triangulation, but
 | 
						|
    their first derivatives are discontinuous at edges between triangles.
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    triangulation : `~matplotlib.tri.Triangulation`
 | 
						|
        The triangulation to interpolate over.
 | 
						|
    z : (npoints,) array-like
 | 
						|
        Array of values, defined at grid points, to interpolate between.
 | 
						|
    trifinder : `~matplotlib.tri.TriFinder`, optional
 | 
						|
        If this is not specified, the Triangulation's default TriFinder will
 | 
						|
        be used by calling `.Triangulation.get_trifinder`.
 | 
						|
 | 
						|
    Methods
 | 
						|
    -------
 | 
						|
    `__call__` (x, y) : Returns interpolated values at (x, y) points.
 | 
						|
    `gradient` (x, y) : Returns interpolated derivatives at (x, y) points.
 | 
						|
 | 
						|
    """
 | 
						|
    def __init__(self, triangulation, z, trifinder=None):
 | 
						|
        super().__init__(triangulation, z, trifinder)
 | 
						|
 | 
						|
        # Store plane coefficients for fast interpolation calculations.
 | 
						|
        self._plane_coefficients = \
 | 
						|
            self._triangulation.calculate_plane_coefficients(self._z)
 | 
						|
 | 
						|
    def __call__(self, x, y):
 | 
						|
        return self._interpolate_multikeys(x, y, tri_index=None,
 | 
						|
                                           return_keys=('z',))[0]
 | 
						|
    __call__.__doc__ = TriInterpolator._docstring__call__
 | 
						|
 | 
						|
    def gradient(self, x, y):
 | 
						|
        return self._interpolate_multikeys(x, y, tri_index=None,
 | 
						|
                                           return_keys=('dzdx', 'dzdy'))
 | 
						|
    gradient.__doc__ = TriInterpolator._docstringgradient
 | 
						|
 | 
						|
    def _interpolate_single_key(self, return_key, tri_index, x, y):
 | 
						|
        _api.check_in_list(['z', 'dzdx', 'dzdy'], return_key=return_key)
 | 
						|
        if return_key == 'z':
 | 
						|
            return (self._plane_coefficients[tri_index, 0]*x +
 | 
						|
                    self._plane_coefficients[tri_index, 1]*y +
 | 
						|
                    self._plane_coefficients[tri_index, 2])
 | 
						|
        elif return_key == 'dzdx':
 | 
						|
            return self._plane_coefficients[tri_index, 0]
 | 
						|
        else:  # 'dzdy'
 | 
						|
            return self._plane_coefficients[tri_index, 1]
 | 
						|
 | 
						|
 | 
						|
class CubicTriInterpolator(TriInterpolator):
 | 
						|
    r"""
 | 
						|
    Cubic interpolator on a triangular grid.
 | 
						|
 | 
						|
    In one-dimension - on a segment - a cubic interpolating function is
 | 
						|
    defined by the values of the function and its derivative at both ends.
 | 
						|
    This is almost the same in 2D inside a triangle, except that the values
 | 
						|
    of the function and its 2 derivatives have to be defined at each triangle
 | 
						|
    node.
 | 
						|
 | 
						|
    The CubicTriInterpolator takes the value of the function at each node -
 | 
						|
    provided by the user - and internally computes the value of the
 | 
						|
    derivatives, resulting in a smooth interpolation.
 | 
						|
    (As a special feature, the user can also impose the value of the
 | 
						|
    derivatives at each node, but this is not supposed to be the common
 | 
						|
    usage.)
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    triangulation : `~matplotlib.tri.Triangulation`
 | 
						|
        The triangulation to interpolate over.
 | 
						|
    z : (npoints,) array-like
 | 
						|
        Array of values, defined at grid points, to interpolate between.
 | 
						|
    kind : {'min_E', 'geom', 'user'}, optional
 | 
						|
        Choice of the smoothing algorithm, in order to compute
 | 
						|
        the interpolant derivatives (defaults to 'min_E'):
 | 
						|
 | 
						|
        - if 'min_E': (default) The derivatives at each node is computed
 | 
						|
          to minimize a bending energy.
 | 
						|
        - if 'geom': The derivatives at each node is computed as a
 | 
						|
          weighted average of relevant triangle normals. To be used for
 | 
						|
          speed optimization (large grids).
 | 
						|
        - if 'user': The user provides the argument *dz*, no computation
 | 
						|
          is hence needed.
 | 
						|
 | 
						|
    trifinder : `~matplotlib.tri.TriFinder`, optional
 | 
						|
        If not specified, the Triangulation's default TriFinder will
 | 
						|
        be used by calling `.Triangulation.get_trifinder`.
 | 
						|
    dz : tuple of array-likes (dzdx, dzdy), optional
 | 
						|
        Used only if  *kind* ='user'. In this case *dz* must be provided as
 | 
						|
        (dzdx, dzdy) where dzdx, dzdy are arrays of the same shape as *z* and
 | 
						|
        are the interpolant first derivatives at the *triangulation* points.
 | 
						|
 | 
						|
    Methods
 | 
						|
    -------
 | 
						|
    `__call__` (x, y) : Returns interpolated values at (x, y) points.
 | 
						|
    `gradient` (x, y) : Returns interpolated derivatives at (x, y) points.
 | 
						|
 | 
						|
    Notes
 | 
						|
    -----
 | 
						|
    This note is a bit technical and details how the cubic interpolation is
 | 
						|
    computed.
 | 
						|
 | 
						|
    The interpolation is based on a Clough-Tocher subdivision scheme of
 | 
						|
    the *triangulation* mesh (to make it clearer, each triangle of the
 | 
						|
    grid will be divided in 3 child-triangles, and on each child triangle
 | 
						|
    the interpolated function is a cubic polynomial of the 2 coordinates).
 | 
						|
    This technique originates from FEM (Finite Element Method) analysis;
 | 
						|
    the element used is a reduced Hsieh-Clough-Tocher (HCT)
 | 
						|
    element. Its shape functions are described in [1]_.
 | 
						|
    The assembled function is guaranteed to be C1-smooth, i.e. it is
 | 
						|
    continuous and its first derivatives are also continuous (this
 | 
						|
    is easy to show inside the triangles but is also true when crossing the
 | 
						|
    edges).
 | 
						|
 | 
						|
    In the default case (*kind* ='min_E'), the interpolant minimizes a
 | 
						|
    curvature energy on the functional space generated by the HCT element
 | 
						|
    shape functions - with imposed values but arbitrary derivatives at each
 | 
						|
    node. The minimized functional is the integral of the so-called total
 | 
						|
    curvature (implementation based on an algorithm from [2]_ - PCG sparse
 | 
						|
    solver):
 | 
						|
 | 
						|
    .. math::
 | 
						|
 | 
						|
        E(z) = \frac{1}{2} \int_{\Omega} \left(
 | 
						|
            \left( \frac{\partial^2{z}}{\partial{x}^2} \right)^2 +
 | 
						|
            \left( \frac{\partial^2{z}}{\partial{y}^2} \right)^2 +
 | 
						|
            2\left( \frac{\partial^2{z}}{\partial{y}\partial{x}} \right)^2
 | 
						|
        \right) dx\,dy
 | 
						|
 | 
						|
    If the case *kind* ='geom' is chosen by the user, a simple geometric
 | 
						|
    approximation is used (weighted average of the triangle normal
 | 
						|
    vectors), which could improve speed on very large grids.
 | 
						|
 | 
						|
    References
 | 
						|
    ----------
 | 
						|
    .. [1] Michel Bernadou, Kamal Hassan, "Basis functions for general
 | 
						|
        Hsieh-Clough-Tocher triangles, complete or reduced.",
 | 
						|
        International Journal for Numerical Methods in Engineering,
 | 
						|
        17(5):784 - 789. 2.01.
 | 
						|
    .. [2] C.T. Kelley, "Iterative Methods for Optimization".
 | 
						|
 | 
						|
    """
 | 
						|
    def __init__(self, triangulation, z, kind='min_E', trifinder=None,
 | 
						|
                 dz=None):
 | 
						|
        super().__init__(triangulation, z, trifinder)
 | 
						|
 | 
						|
        # Loads the underlying c++ _triangulation.
 | 
						|
        # (During loading, reordering of triangulation._triangles may occur so
 | 
						|
        # that all final triangles are now anti-clockwise)
 | 
						|
        self._triangulation.get_cpp_triangulation()
 | 
						|
 | 
						|
        # To build the stiffness matrix and avoid zero-energy spurious modes
 | 
						|
        # we will only store internally the valid (unmasked) triangles and
 | 
						|
        # the necessary (used) points coordinates.
 | 
						|
        # 2 renumbering tables need to be computed and stored:
 | 
						|
        #  - a triangle renum table in order to translate the result from a
 | 
						|
        #    TriFinder instance into the internal stored triangle number.
 | 
						|
        #  - a node renum table to overwrite the self._z values into the new
 | 
						|
        #    (used) node numbering.
 | 
						|
        tri_analyzer = TriAnalyzer(self._triangulation)
 | 
						|
        (compressed_triangles, compressed_x, compressed_y, tri_renum,
 | 
						|
         node_renum) = tri_analyzer._get_compressed_triangulation()
 | 
						|
        self._triangles = compressed_triangles
 | 
						|
        self._tri_renum = tri_renum
 | 
						|
        # Taking into account the node renumbering in self._z:
 | 
						|
        valid_node = (node_renum != -1)
 | 
						|
        self._z[node_renum[valid_node]] = self._z[valid_node]
 | 
						|
 | 
						|
        # Computing scale factors
 | 
						|
        self._unit_x = np.ptp(compressed_x)
 | 
						|
        self._unit_y = np.ptp(compressed_y)
 | 
						|
        self._pts = np.column_stack([compressed_x / self._unit_x,
 | 
						|
                                     compressed_y / self._unit_y])
 | 
						|
        # Computing triangle points
 | 
						|
        self._tris_pts = self._pts[self._triangles]
 | 
						|
        # Computing eccentricities
 | 
						|
        self._eccs = self._compute_tri_eccentricities(self._tris_pts)
 | 
						|
        # Computing dof estimations for HCT triangle shape function
 | 
						|
        _api.check_in_list(['user', 'geom', 'min_E'], kind=kind)
 | 
						|
        self._dof = self._compute_dof(kind, dz=dz)
 | 
						|
        # Loading HCT element
 | 
						|
        self._ReferenceElement = _ReducedHCT_Element()
 | 
						|
 | 
						|
    def __call__(self, x, y):
 | 
						|
        return self._interpolate_multikeys(x, y, tri_index=None,
 | 
						|
                                           return_keys=('z',))[0]
 | 
						|
    __call__.__doc__ = TriInterpolator._docstring__call__
 | 
						|
 | 
						|
    def gradient(self, x, y):
 | 
						|
        return self._interpolate_multikeys(x, y, tri_index=None,
 | 
						|
                                           return_keys=('dzdx', 'dzdy'))
 | 
						|
    gradient.__doc__ = TriInterpolator._docstringgradient
 | 
						|
 | 
						|
    def _interpolate_single_key(self, return_key, tri_index, x, y):
 | 
						|
        _api.check_in_list(['z', 'dzdx', 'dzdy'], return_key=return_key)
 | 
						|
        tris_pts = self._tris_pts[tri_index]
 | 
						|
        alpha = self._get_alpha_vec(x, y, tris_pts)
 | 
						|
        ecc = self._eccs[tri_index]
 | 
						|
        dof = np.expand_dims(self._dof[tri_index], axis=1)
 | 
						|
        if return_key == 'z':
 | 
						|
            return self._ReferenceElement.get_function_values(
 | 
						|
                alpha, ecc, dof)
 | 
						|
        else:  # 'dzdx', 'dzdy'
 | 
						|
            J = self._get_jacobian(tris_pts)
 | 
						|
            dzdx = self._ReferenceElement.get_function_derivatives(
 | 
						|
                alpha, J, ecc, dof)
 | 
						|
            if return_key == 'dzdx':
 | 
						|
                return dzdx[:, 0, 0]
 | 
						|
            else:
 | 
						|
                return dzdx[:, 1, 0]
 | 
						|
 | 
						|
    def _compute_dof(self, kind, dz=None):
 | 
						|
        """
 | 
						|
        Compute and return nodal dofs according to kind.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        kind : {'min_E', 'geom', 'user'}
 | 
						|
            Choice of the _DOF_estimator subclass to estimate the gradient.
 | 
						|
        dz : tuple of array-likes (dzdx, dzdy), optional
 | 
						|
            Used only if *kind*=user; in this case passed to the
 | 
						|
            :class:`_DOF_estimator_user`.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        array-like, shape (npts, 2)
 | 
						|
            Estimation of the gradient at triangulation nodes (stored as
 | 
						|
            degree of freedoms of reduced-HCT triangle elements).
 | 
						|
        """
 | 
						|
        if kind == 'user':
 | 
						|
            if dz is None:
 | 
						|
                raise ValueError("For a CubicTriInterpolator with "
 | 
						|
                                 "*kind*='user', a valid *dz* "
 | 
						|
                                 "argument is expected.")
 | 
						|
            TE = _DOF_estimator_user(self, dz=dz)
 | 
						|
        elif kind == 'geom':
 | 
						|
            TE = _DOF_estimator_geom(self)
 | 
						|
        else:  # 'min_E', checked in __init__
 | 
						|
            TE = _DOF_estimator_min_E(self)
 | 
						|
        return TE.compute_dof_from_df()
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def _get_alpha_vec(x, y, tris_pts):
 | 
						|
        """
 | 
						|
        Fast (vectorized) function to compute barycentric coordinates alpha.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        x, y : array-like of dim 1 (shape (nx,))
 | 
						|
            Coordinates of the points whose points barycentric coordinates are
 | 
						|
            requested.
 | 
						|
        tris_pts : array like of dim 3 (shape: (nx, 3, 2))
 | 
						|
            Coordinates of the containing triangles apexes.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        array of dim 2 (shape (nx, 3))
 | 
						|
            Barycentric coordinates of the points inside the containing
 | 
						|
            triangles.
 | 
						|
        """
 | 
						|
        ndim = tris_pts.ndim-2
 | 
						|
 | 
						|
        a = tris_pts[:, 1, :] - tris_pts[:, 0, :]
 | 
						|
        b = tris_pts[:, 2, :] - tris_pts[:, 0, :]
 | 
						|
        abT = np.stack([a, b], axis=-1)
 | 
						|
        ab = _transpose_vectorized(abT)
 | 
						|
        OM = np.stack([x, y], axis=1) - tris_pts[:, 0, :]
 | 
						|
 | 
						|
        metric = ab @ abT
 | 
						|
        # Here we try to deal with the colinear cases.
 | 
						|
        # metric_inv is in this case set to the Moore-Penrose pseudo-inverse
 | 
						|
        # meaning that we will still return a set of valid barycentric
 | 
						|
        # coordinates.
 | 
						|
        metric_inv = _pseudo_inv22sym_vectorized(metric)
 | 
						|
        Covar = ab @ _transpose_vectorized(np.expand_dims(OM, ndim))
 | 
						|
        ksi = metric_inv @ Covar
 | 
						|
        alpha = _to_matrix_vectorized([
 | 
						|
            [1-ksi[:, 0, 0]-ksi[:, 1, 0]], [ksi[:, 0, 0]], [ksi[:, 1, 0]]])
 | 
						|
        return alpha
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def _get_jacobian(tris_pts):
 | 
						|
        """
 | 
						|
        Fast (vectorized) function to compute triangle jacobian matrix.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        tris_pts : array like of dim 3 (shape: (nx, 3, 2))
 | 
						|
            Coordinates of the containing triangles apexes.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        array of dim 3 (shape (nx, 2, 2))
 | 
						|
            Barycentric coordinates of the points inside the containing
 | 
						|
            triangles.
 | 
						|
            J[itri, :, :] is the jacobian matrix at apex 0 of the triangle
 | 
						|
            itri, so that the following (matrix) relationship holds:
 | 
						|
               [dz/dksi] = [J] x [dz/dx]
 | 
						|
            with x: global coordinates
 | 
						|
                 ksi: element parametric coordinates in triangle first apex
 | 
						|
                 local basis.
 | 
						|
        """
 | 
						|
        a = np.array(tris_pts[:, 1, :] - tris_pts[:, 0, :])
 | 
						|
        b = np.array(tris_pts[:, 2, :] - tris_pts[:, 0, :])
 | 
						|
        J = _to_matrix_vectorized([[a[:, 0], a[:, 1]],
 | 
						|
                                   [b[:, 0], b[:, 1]]])
 | 
						|
        return J
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def _compute_tri_eccentricities(tris_pts):
 | 
						|
        """
 | 
						|
        Compute triangle eccentricities.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        tris_pts : array like of dim 3 (shape: (nx, 3, 2))
 | 
						|
            Coordinates of the triangles apexes.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        array like of dim 2 (shape: (nx, 3))
 | 
						|
            The so-called eccentricity parameters [1] needed for HCT triangular
 | 
						|
            element.
 | 
						|
        """
 | 
						|
        a = np.expand_dims(tris_pts[:, 2, :] - tris_pts[:, 1, :], axis=2)
 | 
						|
        b = np.expand_dims(tris_pts[:, 0, :] - tris_pts[:, 2, :], axis=2)
 | 
						|
        c = np.expand_dims(tris_pts[:, 1, :] - tris_pts[:, 0, :], axis=2)
 | 
						|
        # Do not use np.squeeze, this is dangerous if only one triangle
 | 
						|
        # in the triangulation...
 | 
						|
        dot_a = (_transpose_vectorized(a) @ a)[:, 0, 0]
 | 
						|
        dot_b = (_transpose_vectorized(b) @ b)[:, 0, 0]
 | 
						|
        dot_c = (_transpose_vectorized(c) @ c)[:, 0, 0]
 | 
						|
        # Note that this line will raise a warning for dot_a, dot_b or dot_c
 | 
						|
        # zeros, but we choose not to support triangles with duplicate points.
 | 
						|
        return _to_matrix_vectorized([[(dot_c-dot_b) / dot_a],
 | 
						|
                                      [(dot_a-dot_c) / dot_b],
 | 
						|
                                      [(dot_b-dot_a) / dot_c]])
 | 
						|
 | 
						|
 | 
						|
# FEM element used for interpolation and for solving minimisation
 | 
						|
# problem (Reduced HCT element)
 | 
						|
class _ReducedHCT_Element:
 | 
						|
    """
 | 
						|
    Implementation of reduced HCT triangular element with explicit shape
 | 
						|
    functions.
 | 
						|
 | 
						|
    Computes z, dz, d2z and the element stiffness matrix for bending energy:
 | 
						|
    E(f) = integral( (d2z/dx2 + d2z/dy2)**2 dA)
 | 
						|
 | 
						|
    *** Reference for the shape functions: ***
 | 
						|
    [1] Basis functions for general Hsieh-Clough-Tocher _triangles, complete or
 | 
						|
        reduced.
 | 
						|
        Michel Bernadou, Kamal Hassan
 | 
						|
        International Journal for Numerical Methods in Engineering.
 | 
						|
        17(5):784 - 789.  2.01
 | 
						|
 | 
						|
    *** Element description: ***
 | 
						|
    9 dofs: z and dz given at 3 apex
 | 
						|
    C1 (conform)
 | 
						|
 | 
						|
    """
 | 
						|
    # 1) Loads matrices to generate shape functions as a function of
 | 
						|
    #    triangle eccentricities - based on [1] p.11 '''
 | 
						|
    M = np.array([
 | 
						|
        [ 0.00, 0.00, 0.00,  4.50,  4.50, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [-0.25, 0.00, 0.00,  0.50,  1.25, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [-0.25, 0.00, 0.00,  1.25,  0.50, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [ 0.50, 1.00, 0.00, -1.50,  0.00, 3.00, 3.00, 0.00, 0.00, 3.00],
 | 
						|
        [ 0.00, 0.00, 0.00, -0.25,  0.25, 0.00, 1.00, 0.00, 0.00, 0.50],
 | 
						|
        [ 0.25, 0.00, 0.00, -0.50, -0.25, 1.00, 0.00, 0.00, 0.00, 1.00],
 | 
						|
        [ 0.50, 0.00, 1.00,  0.00, -1.50, 0.00, 0.00, 3.00, 3.00, 3.00],
 | 
						|
        [ 0.25, 0.00, 0.00, -0.25, -0.50, 0.00, 0.00, 0.00, 1.00, 1.00],
 | 
						|
        [ 0.00, 0.00, 0.00,  0.25, -0.25, 0.00, 0.00, 1.00, 0.00, 0.50]])
 | 
						|
    M0 = np.array([
 | 
						|
        [ 0.00, 0.00, 0.00,  0.00,  0.00, 0.00, 0.00, 0.00, 0.00,  0.00],
 | 
						|
        [ 0.00, 0.00, 0.00,  0.00,  0.00, 0.00, 0.00, 0.00, 0.00,  0.00],
 | 
						|
        [ 0.00, 0.00, 0.00,  0.00,  0.00, 0.00, 0.00, 0.00, 0.00,  0.00],
 | 
						|
        [-1.00, 0.00, 0.00,  1.50,  1.50, 0.00, 0.00, 0.00, 0.00, -3.00],
 | 
						|
        [-0.50, 0.00, 0.00,  0.75,  0.75, 0.00, 0.00, 0.00, 0.00, -1.50],
 | 
						|
        [ 0.00, 0.00, 0.00,  0.00,  0.00, 0.00, 0.00, 0.00, 0.00,  0.00],
 | 
						|
        [ 1.00, 0.00, 0.00, -1.50, -1.50, 0.00, 0.00, 0.00, 0.00,  3.00],
 | 
						|
        [ 0.00, 0.00, 0.00,  0.00,  0.00, 0.00, 0.00, 0.00, 0.00,  0.00],
 | 
						|
        [ 0.50, 0.00, 0.00, -0.75, -0.75, 0.00, 0.00, 0.00, 0.00,  1.50]])
 | 
						|
    M1 = np.array([
 | 
						|
        [-0.50, 0.00, 0.00,  1.50, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [ 0.00, 0.00, 0.00,  0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [-0.25, 0.00, 0.00,  0.75, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [ 0.00, 0.00, 0.00,  0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [ 0.00, 0.00, 0.00,  0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [ 0.00, 0.00, 0.00,  0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [ 0.50, 0.00, 0.00, -1.50, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [ 0.25, 0.00, 0.00, -0.75, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [ 0.00, 0.00, 0.00,  0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00]])
 | 
						|
    M2 = np.array([
 | 
						|
        [ 0.50, 0.00, 0.00, 0.00, -1.50, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [ 0.25, 0.00, 0.00, 0.00, -0.75, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [ 0.00, 0.00, 0.00, 0.00,  0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [-0.50, 0.00, 0.00, 0.00,  1.50, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [ 0.00, 0.00, 0.00, 0.00,  0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [-0.25, 0.00, 0.00, 0.00,  0.75, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [ 0.00, 0.00, 0.00, 0.00,  0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [ 0.00, 0.00, 0.00, 0.00,  0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
 | 
						|
        [ 0.00, 0.00, 0.00, 0.00,  0.00, 0.00, 0.00, 0.00, 0.00, 0.00]])
 | 
						|
 | 
						|
    # 2) Loads matrices to rotate components of gradient & Hessian
 | 
						|
    #    vectors in the reference basis of triangle first apex (a0)
 | 
						|
    rotate_dV = np.array([[ 1.,  0.], [ 0.,  1.],
 | 
						|
                          [ 0.,  1.], [-1., -1.],
 | 
						|
                          [-1., -1.], [ 1.,  0.]])
 | 
						|
 | 
						|
    rotate_d2V = np.array([[1., 0., 0.], [0., 1., 0.], [ 0.,  0.,  1.],
 | 
						|
                           [0., 1., 0.], [1., 1., 1.], [ 0., -2., -1.],
 | 
						|
                           [1., 1., 1.], [1., 0., 0.], [-2.,  0., -1.]])
 | 
						|
 | 
						|
    # 3) Loads Gauss points & weights on the 3 sub-_triangles for P2
 | 
						|
    #    exact integral - 3 points on each subtriangles.
 | 
						|
    # NOTE: as the 2nd derivative is discontinuous , we really need those 9
 | 
						|
    # points!
 | 
						|
    n_gauss = 9
 | 
						|
    gauss_pts = np.array([[13./18.,  4./18.,  1./18.],
 | 
						|
                          [ 4./18., 13./18.,  1./18.],
 | 
						|
                          [ 7./18.,  7./18.,  4./18.],
 | 
						|
                          [ 1./18., 13./18.,  4./18.],
 | 
						|
                          [ 1./18.,  4./18., 13./18.],
 | 
						|
                          [ 4./18.,  7./18.,  7./18.],
 | 
						|
                          [ 4./18.,  1./18., 13./18.],
 | 
						|
                          [13./18.,  1./18.,  4./18.],
 | 
						|
                          [ 7./18.,  4./18.,  7./18.]], dtype=np.float64)
 | 
						|
    gauss_w = np.ones([9], dtype=np.float64) / 9.
 | 
						|
 | 
						|
    #  4) Stiffness matrix for curvature energy
 | 
						|
    E = np.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 2.]])
 | 
						|
 | 
						|
    #  5) Loads the matrix to compute DOF_rot from tri_J at apex 0
 | 
						|
    J0_to_J1 = np.array([[-1.,  1.], [-1.,  0.]])
 | 
						|
    J0_to_J2 = np.array([[ 0., -1.], [ 1., -1.]])
 | 
						|
 | 
						|
    def get_function_values(self, alpha, ecc, dofs):
 | 
						|
        """
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        alpha : is a (N x 3 x 1) array (array of column-matrices) of
 | 
						|
        barycentric coordinates,
 | 
						|
        ecc : is a (N x 3 x 1) array (array of column-matrices) of triangle
 | 
						|
        eccentricities,
 | 
						|
        dofs : is a (N x 1 x 9) arrays (arrays of row-matrices) of computed
 | 
						|
        degrees of freedom.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        Returns the N-array of interpolated function values.
 | 
						|
        """
 | 
						|
        subtri = np.argmin(alpha, axis=1)[:, 0]
 | 
						|
        ksi = _roll_vectorized(alpha, -subtri, axis=0)
 | 
						|
        E = _roll_vectorized(ecc, -subtri, axis=0)
 | 
						|
        x = ksi[:, 0, 0]
 | 
						|
        y = ksi[:, 1, 0]
 | 
						|
        z = ksi[:, 2, 0]
 | 
						|
        x_sq = x*x
 | 
						|
        y_sq = y*y
 | 
						|
        z_sq = z*z
 | 
						|
        V = _to_matrix_vectorized([
 | 
						|
            [x_sq*x], [y_sq*y], [z_sq*z], [x_sq*z], [x_sq*y], [y_sq*x],
 | 
						|
            [y_sq*z], [z_sq*y], [z_sq*x], [x*y*z]])
 | 
						|
        prod = self.M @ V
 | 
						|
        prod += _scalar_vectorized(E[:, 0, 0], self.M0 @ V)
 | 
						|
        prod += _scalar_vectorized(E[:, 1, 0], self.M1 @ V)
 | 
						|
        prod += _scalar_vectorized(E[:, 2, 0], self.M2 @ V)
 | 
						|
        s = _roll_vectorized(prod, 3*subtri, axis=0)
 | 
						|
        return (dofs @ s)[:, 0, 0]
 | 
						|
 | 
						|
    def get_function_derivatives(self, alpha, J, ecc, dofs):
 | 
						|
        """
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        *alpha* is a (N x 3 x 1) array (array of column-matrices of
 | 
						|
        barycentric coordinates)
 | 
						|
        *J* is a (N x 2 x 2) array of jacobian matrices (jacobian matrix at
 | 
						|
        triangle first apex)
 | 
						|
        *ecc* is a (N x 3 x 1) array (array of column-matrices of triangle
 | 
						|
        eccentricities)
 | 
						|
        *dofs* is a (N x 1 x 9) arrays (arrays of row-matrices) of computed
 | 
						|
        degrees of freedom.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        Returns the values of interpolated function derivatives [dz/dx, dz/dy]
 | 
						|
        in global coordinates at locations alpha, as a column-matrices of
 | 
						|
        shape (N x 2 x 1).
 | 
						|
        """
 | 
						|
        subtri = np.argmin(alpha, axis=1)[:, 0]
 | 
						|
        ksi = _roll_vectorized(alpha, -subtri, axis=0)
 | 
						|
        E = _roll_vectorized(ecc, -subtri, axis=0)
 | 
						|
        x = ksi[:, 0, 0]
 | 
						|
        y = ksi[:, 1, 0]
 | 
						|
        z = ksi[:, 2, 0]
 | 
						|
        x_sq = x*x
 | 
						|
        y_sq = y*y
 | 
						|
        z_sq = z*z
 | 
						|
        dV = _to_matrix_vectorized([
 | 
						|
            [    -3.*x_sq,     -3.*x_sq],
 | 
						|
            [     3.*y_sq,           0.],
 | 
						|
            [          0.,      3.*z_sq],
 | 
						|
            [     -2.*x*z, -2.*x*z+x_sq],
 | 
						|
            [-2.*x*y+x_sq,      -2.*x*y],
 | 
						|
            [ 2.*x*y-y_sq,        -y_sq],
 | 
						|
            [      2.*y*z,         y_sq],
 | 
						|
            [        z_sq,       2.*y*z],
 | 
						|
            [       -z_sq,  2.*x*z-z_sq],
 | 
						|
            [     x*z-y*z,      x*y-y*z]])
 | 
						|
        # Puts back dV in first apex basis
 | 
						|
        dV = dV @ _extract_submatrices(
 | 
						|
            self.rotate_dV, subtri, block_size=2, axis=0)
 | 
						|
 | 
						|
        prod = self.M @ dV
 | 
						|
        prod += _scalar_vectorized(E[:, 0, 0], self.M0 @ dV)
 | 
						|
        prod += _scalar_vectorized(E[:, 1, 0], self.M1 @ dV)
 | 
						|
        prod += _scalar_vectorized(E[:, 2, 0], self.M2 @ dV)
 | 
						|
        dsdksi = _roll_vectorized(prod, 3*subtri, axis=0)
 | 
						|
        dfdksi = dofs @ dsdksi
 | 
						|
        # In global coordinates:
 | 
						|
        # Here we try to deal with the simplest colinear cases, returning a
 | 
						|
        # null matrix.
 | 
						|
        J_inv = _safe_inv22_vectorized(J)
 | 
						|
        dfdx = J_inv @ _transpose_vectorized(dfdksi)
 | 
						|
        return dfdx
 | 
						|
 | 
						|
    def get_function_hessians(self, alpha, J, ecc, dofs):
 | 
						|
        """
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        *alpha* is a (N x 3 x 1) array (array of column-matrices) of
 | 
						|
        barycentric coordinates
 | 
						|
        *J* is a (N x 2 x 2) array of jacobian matrices (jacobian matrix at
 | 
						|
        triangle first apex)
 | 
						|
        *ecc* is a (N x 3 x 1) array (array of column-matrices) of triangle
 | 
						|
        eccentricities
 | 
						|
        *dofs* is a (N x 1 x 9) arrays (arrays of row-matrices) of computed
 | 
						|
        degrees of freedom.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        Returns the values of interpolated function 2nd-derivatives
 | 
						|
        [d2z/dx2, d2z/dy2, d2z/dxdy] in global coordinates at locations alpha,
 | 
						|
        as a column-matrices of shape (N x 3 x 1).
 | 
						|
        """
 | 
						|
        d2sdksi2 = self.get_d2Sidksij2(alpha, ecc)
 | 
						|
        d2fdksi2 = dofs @ d2sdksi2
 | 
						|
        H_rot = self.get_Hrot_from_J(J)
 | 
						|
        d2fdx2 = d2fdksi2 @ H_rot
 | 
						|
        return _transpose_vectorized(d2fdx2)
 | 
						|
 | 
						|
    def get_d2Sidksij2(self, alpha, ecc):
 | 
						|
        """
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        *alpha* is a (N x 3 x 1) array (array of column-matrices) of
 | 
						|
        barycentric coordinates
 | 
						|
        *ecc* is a (N x 3 x 1) array (array of column-matrices) of triangle
 | 
						|
        eccentricities
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        Returns the arrays d2sdksi2 (N x 3 x 1) Hessian of shape functions
 | 
						|
        expressed in covariant coordinates in first apex basis.
 | 
						|
        """
 | 
						|
        subtri = np.argmin(alpha, axis=1)[:, 0]
 | 
						|
        ksi = _roll_vectorized(alpha, -subtri, axis=0)
 | 
						|
        E = _roll_vectorized(ecc, -subtri, axis=0)
 | 
						|
        x = ksi[:, 0, 0]
 | 
						|
        y = ksi[:, 1, 0]
 | 
						|
        z = ksi[:, 2, 0]
 | 
						|
        d2V = _to_matrix_vectorized([
 | 
						|
            [     6.*x,      6.*x,      6.*x],
 | 
						|
            [     6.*y,        0.,        0.],
 | 
						|
            [       0.,      6.*z,        0.],
 | 
						|
            [     2.*z, 2.*z-4.*x, 2.*z-2.*x],
 | 
						|
            [2.*y-4.*x,      2.*y, 2.*y-2.*x],
 | 
						|
            [2.*x-4.*y,        0.,     -2.*y],
 | 
						|
            [     2.*z,        0.,      2.*y],
 | 
						|
            [       0.,      2.*y,      2.*z],
 | 
						|
            [       0., 2.*x-4.*z,     -2.*z],
 | 
						|
            [    -2.*z,     -2.*y,     x-y-z]])
 | 
						|
        # Puts back d2V in first apex basis
 | 
						|
        d2V = d2V @ _extract_submatrices(
 | 
						|
            self.rotate_d2V, subtri, block_size=3, axis=0)
 | 
						|
        prod = self.M @ d2V
 | 
						|
        prod += _scalar_vectorized(E[:, 0, 0], self.M0 @ d2V)
 | 
						|
        prod += _scalar_vectorized(E[:, 1, 0], self.M1 @ d2V)
 | 
						|
        prod += _scalar_vectorized(E[:, 2, 0], self.M2 @ d2V)
 | 
						|
        d2sdksi2 = _roll_vectorized(prod, 3*subtri, axis=0)
 | 
						|
        return d2sdksi2
 | 
						|
 | 
						|
    def get_bending_matrices(self, J, ecc):
 | 
						|
        """
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        *J* is a (N x 2 x 2) array of jacobian matrices (jacobian matrix at
 | 
						|
        triangle first apex)
 | 
						|
        *ecc* is a (N x 3 x 1) array (array of column-matrices) of triangle
 | 
						|
        eccentricities
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        Returns the element K matrices for bending energy expressed in
 | 
						|
        GLOBAL nodal coordinates.
 | 
						|
        K_ij = integral [ (d2zi/dx2 + d2zi/dy2) * (d2zj/dx2 + d2zj/dy2) dA]
 | 
						|
        tri_J is needed to rotate dofs from local basis to global basis
 | 
						|
        """
 | 
						|
        n = np.size(ecc, 0)
 | 
						|
 | 
						|
        # 1) matrix to rotate dofs in global coordinates
 | 
						|
        J1 = self.J0_to_J1 @ J
 | 
						|
        J2 = self.J0_to_J2 @ J
 | 
						|
        DOF_rot = np.zeros([n, 9, 9], dtype=np.float64)
 | 
						|
        DOF_rot[:, 0, 0] = 1
 | 
						|
        DOF_rot[:, 3, 3] = 1
 | 
						|
        DOF_rot[:, 6, 6] = 1
 | 
						|
        DOF_rot[:, 1:3, 1:3] = J
 | 
						|
        DOF_rot[:, 4:6, 4:6] = J1
 | 
						|
        DOF_rot[:, 7:9, 7:9] = J2
 | 
						|
 | 
						|
        # 2) matrix to rotate Hessian in global coordinates.
 | 
						|
        H_rot, area = self.get_Hrot_from_J(J, return_area=True)
 | 
						|
 | 
						|
        # 3) Computes stiffness matrix
 | 
						|
        # Gauss quadrature.
 | 
						|
        K = np.zeros([n, 9, 9], dtype=np.float64)
 | 
						|
        weights = self.gauss_w
 | 
						|
        pts = self.gauss_pts
 | 
						|
        for igauss in range(self.n_gauss):
 | 
						|
            alpha = np.tile(pts[igauss, :], n).reshape(n, 3)
 | 
						|
            alpha = np.expand_dims(alpha, 2)
 | 
						|
            weight = weights[igauss]
 | 
						|
            d2Skdksi2 = self.get_d2Sidksij2(alpha, ecc)
 | 
						|
            d2Skdx2 = d2Skdksi2 @ H_rot
 | 
						|
            K += weight * (d2Skdx2 @ self.E @ _transpose_vectorized(d2Skdx2))
 | 
						|
 | 
						|
        # 4) With nodal (not elem) dofs
 | 
						|
        K = _transpose_vectorized(DOF_rot) @ K @ DOF_rot
 | 
						|
 | 
						|
        # 5) Need the area to compute total element energy
 | 
						|
        return _scalar_vectorized(area, K)
 | 
						|
 | 
						|
    def get_Hrot_from_J(self, J, return_area=False):
 | 
						|
        """
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        *J* is a (N x 2 x 2) array of jacobian matrices (jacobian matrix at
 | 
						|
        triangle first apex)
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        Returns H_rot used to rotate Hessian from local basis of first apex,
 | 
						|
        to global coordinates.
 | 
						|
        if *return_area* is True, returns also the triangle area (0.5*det(J))
 | 
						|
        """
 | 
						|
        # Here we try to deal with the simplest colinear cases; a null
 | 
						|
        # energy and area is imposed.
 | 
						|
        J_inv = _safe_inv22_vectorized(J)
 | 
						|
        Ji00 = J_inv[:, 0, 0]
 | 
						|
        Ji11 = J_inv[:, 1, 1]
 | 
						|
        Ji10 = J_inv[:, 1, 0]
 | 
						|
        Ji01 = J_inv[:, 0, 1]
 | 
						|
        H_rot = _to_matrix_vectorized([
 | 
						|
            [Ji00*Ji00, Ji10*Ji10, Ji00*Ji10],
 | 
						|
            [Ji01*Ji01, Ji11*Ji11, Ji01*Ji11],
 | 
						|
            [2*Ji00*Ji01, 2*Ji11*Ji10, Ji00*Ji11+Ji10*Ji01]])
 | 
						|
        if not return_area:
 | 
						|
            return H_rot
 | 
						|
        else:
 | 
						|
            area = 0.5 * (J[:, 0, 0]*J[:, 1, 1] - J[:, 0, 1]*J[:, 1, 0])
 | 
						|
            return H_rot, area
 | 
						|
 | 
						|
    def get_Kff_and_Ff(self, J, ecc, triangles, Uc):
 | 
						|
        """
 | 
						|
        Build K and F for the following elliptic formulation:
 | 
						|
        minimization of curvature energy with value of function at node
 | 
						|
        imposed and derivatives 'free'.
 | 
						|
 | 
						|
        Build the global Kff matrix in cco format.
 | 
						|
        Build the full Ff vec Ff = - Kfc x Uc.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        *J* is a (N x 2 x 2) array of jacobian matrices (jacobian matrix at
 | 
						|
        triangle first apex)
 | 
						|
        *ecc* is a (N x 3 x 1) array (array of column-matrices) of triangle
 | 
						|
        eccentricities
 | 
						|
        *triangles* is a (N x 3) array of nodes indexes.
 | 
						|
        *Uc* is (N x 3) array of imposed displacements at nodes
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        (Kff_rows, Kff_cols, Kff_vals) Kff matrix in coo format - Duplicate
 | 
						|
        (row, col) entries must be summed.
 | 
						|
        Ff: force vector - dim npts * 3
 | 
						|
        """
 | 
						|
        ntri = np.size(ecc, 0)
 | 
						|
        vec_range = np.arange(ntri, dtype=np.int32)
 | 
						|
        c_indices = np.full(ntri, -1, dtype=np.int32)  # for unused dofs, -1
 | 
						|
        f_dof = [1, 2, 4, 5, 7, 8]
 | 
						|
        c_dof = [0, 3, 6]
 | 
						|
 | 
						|
        # vals, rows and cols indices in global dof numbering
 | 
						|
        f_dof_indices = _to_matrix_vectorized([[
 | 
						|
            c_indices, triangles[:, 0]*2, triangles[:, 0]*2+1,
 | 
						|
            c_indices, triangles[:, 1]*2, triangles[:, 1]*2+1,
 | 
						|
            c_indices, triangles[:, 2]*2, triangles[:, 2]*2+1]])
 | 
						|
 | 
						|
        expand_indices = np.ones([ntri, 9, 1], dtype=np.int32)
 | 
						|
        f_row_indices = _transpose_vectorized(expand_indices @ f_dof_indices)
 | 
						|
        f_col_indices = expand_indices @ f_dof_indices
 | 
						|
        K_elem = self.get_bending_matrices(J, ecc)
 | 
						|
 | 
						|
        # Extracting sub-matrices
 | 
						|
        # Explanation & notations:
 | 
						|
        # * Subscript f denotes 'free' degrees of freedom (i.e. dz/dx, dz/dx)
 | 
						|
        # * Subscript c denotes 'condensated' (imposed) degrees of freedom
 | 
						|
        #    (i.e. z at all nodes)
 | 
						|
        # * F = [Ff, Fc] is the force vector
 | 
						|
        # * U = [Uf, Uc] is the imposed dof vector
 | 
						|
        #        [ Kff Kfc ]
 | 
						|
        # * K =  [         ]  is the laplacian stiffness matrix
 | 
						|
        #        [ Kcf Kff ]
 | 
						|
        # * As F = K x U one gets straightforwardly: Ff = - Kfc x Uc
 | 
						|
 | 
						|
        # Computing Kff stiffness matrix in sparse coo format
 | 
						|
        Kff_vals = np.ravel(K_elem[np.ix_(vec_range, f_dof, f_dof)])
 | 
						|
        Kff_rows = np.ravel(f_row_indices[np.ix_(vec_range, f_dof, f_dof)])
 | 
						|
        Kff_cols = np.ravel(f_col_indices[np.ix_(vec_range, f_dof, f_dof)])
 | 
						|
 | 
						|
        # Computing Ff force vector in sparse coo format
 | 
						|
        Kfc_elem = K_elem[np.ix_(vec_range, f_dof, c_dof)]
 | 
						|
        Uc_elem = np.expand_dims(Uc, axis=2)
 | 
						|
        Ff_elem = -(Kfc_elem @ Uc_elem)[:, :, 0]
 | 
						|
        Ff_indices = f_dof_indices[np.ix_(vec_range, [0], f_dof)][:, 0, :]
 | 
						|
 | 
						|
        # Extracting Ff force vector in dense format
 | 
						|
        # We have to sum duplicate indices -  using bincount
 | 
						|
        Ff = np.bincount(np.ravel(Ff_indices), weights=np.ravel(Ff_elem))
 | 
						|
        return Kff_rows, Kff_cols, Kff_vals, Ff
 | 
						|
 | 
						|
 | 
						|
# :class:_DOF_estimator, _DOF_estimator_user, _DOF_estimator_geom,
 | 
						|
# _DOF_estimator_min_E
 | 
						|
# Private classes used to compute the degree of freedom of each triangular
 | 
						|
# element for the TriCubicInterpolator.
 | 
						|
class _DOF_estimator:
 | 
						|
    """
 | 
						|
    Abstract base class for classes used to estimate a function's first
 | 
						|
    derivatives, and deduce the dofs for a CubicTriInterpolator using a
 | 
						|
    reduced HCT element formulation.
 | 
						|
 | 
						|
    Derived classes implement ``compute_df(self, **kwargs)``, returning
 | 
						|
    ``np.vstack([dfx, dfy]).T`` where ``dfx, dfy`` are the estimation of the 2
 | 
						|
    gradient coordinates.
 | 
						|
    """
 | 
						|
    def __init__(self, interpolator, **kwargs):
 | 
						|
        _api.check_isinstance(CubicTriInterpolator, interpolator=interpolator)
 | 
						|
        self._pts = interpolator._pts
 | 
						|
        self._tris_pts = interpolator._tris_pts
 | 
						|
        self.z = interpolator._z
 | 
						|
        self._triangles = interpolator._triangles
 | 
						|
        (self._unit_x, self._unit_y) = (interpolator._unit_x,
 | 
						|
                                        interpolator._unit_y)
 | 
						|
        self.dz = self.compute_dz(**kwargs)
 | 
						|
        self.compute_dof_from_df()
 | 
						|
 | 
						|
    def compute_dz(self, **kwargs):
 | 
						|
        raise NotImplementedError
 | 
						|
 | 
						|
    def compute_dof_from_df(self):
 | 
						|
        """
 | 
						|
        Compute reduced-HCT elements degrees of freedom, from the gradient.
 | 
						|
        """
 | 
						|
        J = CubicTriInterpolator._get_jacobian(self._tris_pts)
 | 
						|
        tri_z = self.z[self._triangles]
 | 
						|
        tri_dz = self.dz[self._triangles]
 | 
						|
        tri_dof = self.get_dof_vec(tri_z, tri_dz, J)
 | 
						|
        return tri_dof
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def get_dof_vec(tri_z, tri_dz, J):
 | 
						|
        """
 | 
						|
        Compute the dof vector of a triangle, from the value of f, df and
 | 
						|
        of the local Jacobian at each node.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        tri_z : shape (3,) array
 | 
						|
            f nodal values.
 | 
						|
        tri_dz : shape (3, 2) array
 | 
						|
            df/dx, df/dy nodal values.
 | 
						|
        J
 | 
						|
            Jacobian matrix in local basis of apex 0.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        dof : shape (9,) array
 | 
						|
            For each apex ``iapex``::
 | 
						|
 | 
						|
                dof[iapex*3+0] = f(Ai)
 | 
						|
                dof[iapex*3+1] = df(Ai).(AiAi+)
 | 
						|
                dof[iapex*3+2] = df(Ai).(AiAi-)
 | 
						|
        """
 | 
						|
        npt = tri_z.shape[0]
 | 
						|
        dof = np.zeros([npt, 9], dtype=np.float64)
 | 
						|
        J1 = _ReducedHCT_Element.J0_to_J1 @ J
 | 
						|
        J2 = _ReducedHCT_Element.J0_to_J2 @ J
 | 
						|
 | 
						|
        col0 = J @ np.expand_dims(tri_dz[:, 0, :], axis=2)
 | 
						|
        col1 = J1 @ np.expand_dims(tri_dz[:, 1, :], axis=2)
 | 
						|
        col2 = J2 @ np.expand_dims(tri_dz[:, 2, :], axis=2)
 | 
						|
 | 
						|
        dfdksi = _to_matrix_vectorized([
 | 
						|
            [col0[:, 0, 0], col1[:, 0, 0], col2[:, 0, 0]],
 | 
						|
            [col0[:, 1, 0], col1[:, 1, 0], col2[:, 1, 0]]])
 | 
						|
        dof[:, 0:7:3] = tri_z
 | 
						|
        dof[:, 1:8:3] = dfdksi[:, 0]
 | 
						|
        dof[:, 2:9:3] = dfdksi[:, 1]
 | 
						|
        return dof
 | 
						|
 | 
						|
 | 
						|
class _DOF_estimator_user(_DOF_estimator):
 | 
						|
    """dz is imposed by user; accounts for scaling if any."""
 | 
						|
 | 
						|
    def compute_dz(self, dz):
 | 
						|
        (dzdx, dzdy) = dz
 | 
						|
        dzdx = dzdx * self._unit_x
 | 
						|
        dzdy = dzdy * self._unit_y
 | 
						|
        return np.vstack([dzdx, dzdy]).T
 | 
						|
 | 
						|
 | 
						|
class _DOF_estimator_geom(_DOF_estimator):
 | 
						|
    """Fast 'geometric' approximation, recommended for large arrays."""
 | 
						|
 | 
						|
    def compute_dz(self):
 | 
						|
        """
 | 
						|
        self.df is computed as weighted average of _triangles sharing a common
 | 
						|
        node. On each triangle itri f is first assumed linear (= ~f), which
 | 
						|
        allows to compute d~f[itri]
 | 
						|
        Then the following approximation of df nodal values is then proposed:
 | 
						|
            f[ipt] = SUM ( w[itri] x d~f[itri] , for itri sharing apex ipt)
 | 
						|
        The weighted coeff. w[itri] are proportional to the angle of the
 | 
						|
        triangle itri at apex ipt
 | 
						|
        """
 | 
						|
        el_geom_w = self.compute_geom_weights()
 | 
						|
        el_geom_grad = self.compute_geom_grads()
 | 
						|
 | 
						|
        # Sum of weights coeffs
 | 
						|
        w_node_sum = np.bincount(np.ravel(self._triangles),
 | 
						|
                                 weights=np.ravel(el_geom_w))
 | 
						|
 | 
						|
        # Sum of weighted df = (dfx, dfy)
 | 
						|
        dfx_el_w = np.empty_like(el_geom_w)
 | 
						|
        dfy_el_w = np.empty_like(el_geom_w)
 | 
						|
        for iapex in range(3):
 | 
						|
            dfx_el_w[:, iapex] = el_geom_w[:, iapex]*el_geom_grad[:, 0]
 | 
						|
            dfy_el_w[:, iapex] = el_geom_w[:, iapex]*el_geom_grad[:, 1]
 | 
						|
        dfx_node_sum = np.bincount(np.ravel(self._triangles),
 | 
						|
                                   weights=np.ravel(dfx_el_w))
 | 
						|
        dfy_node_sum = np.bincount(np.ravel(self._triangles),
 | 
						|
                                   weights=np.ravel(dfy_el_w))
 | 
						|
 | 
						|
        # Estimation of df
 | 
						|
        dfx_estim = dfx_node_sum/w_node_sum
 | 
						|
        dfy_estim = dfy_node_sum/w_node_sum
 | 
						|
        return np.vstack([dfx_estim, dfy_estim]).T
 | 
						|
 | 
						|
    def compute_geom_weights(self):
 | 
						|
        """
 | 
						|
        Build the (nelems, 3) weights coeffs of _triangles angles,
 | 
						|
        renormalized so that np.sum(weights, axis=1) == np.ones(nelems)
 | 
						|
        """
 | 
						|
        weights = np.zeros([np.size(self._triangles, 0), 3])
 | 
						|
        tris_pts = self._tris_pts
 | 
						|
        for ipt in range(3):
 | 
						|
            p0 = tris_pts[:, ipt % 3, :]
 | 
						|
            p1 = tris_pts[:, (ipt+1) % 3, :]
 | 
						|
            p2 = tris_pts[:, (ipt-1) % 3, :]
 | 
						|
            alpha1 = np.arctan2(p1[:, 1]-p0[:, 1], p1[:, 0]-p0[:, 0])
 | 
						|
            alpha2 = np.arctan2(p2[:, 1]-p0[:, 1], p2[:, 0]-p0[:, 0])
 | 
						|
            # In the below formula we could take modulo 2. but
 | 
						|
            # modulo 1. is safer regarding round-off errors (flat triangles).
 | 
						|
            angle = np.abs(((alpha2-alpha1) / np.pi) % 1)
 | 
						|
            # Weight proportional to angle up np.pi/2; null weight for
 | 
						|
            # degenerated cases 0 and np.pi (note that *angle* is normalized
 | 
						|
            # by np.pi).
 | 
						|
            weights[:, ipt] = 0.5 - np.abs(angle-0.5)
 | 
						|
        return weights
 | 
						|
 | 
						|
    def compute_geom_grads(self):
 | 
						|
        """
 | 
						|
        Compute the (global) gradient component of f assumed linear (~f).
 | 
						|
        returns array df of shape (nelems, 2)
 | 
						|
        df[ielem].dM[ielem] = dz[ielem] i.e. df = dz x dM = dM.T^-1 x dz
 | 
						|
        """
 | 
						|
        tris_pts = self._tris_pts
 | 
						|
        tris_f = self.z[self._triangles]
 | 
						|
 | 
						|
        dM1 = tris_pts[:, 1, :] - tris_pts[:, 0, :]
 | 
						|
        dM2 = tris_pts[:, 2, :] - tris_pts[:, 0, :]
 | 
						|
        dM = np.dstack([dM1, dM2])
 | 
						|
        # Here we try to deal with the simplest colinear cases: a null
 | 
						|
        # gradient is assumed in this case.
 | 
						|
        dM_inv = _safe_inv22_vectorized(dM)
 | 
						|
 | 
						|
        dZ1 = tris_f[:, 1] - tris_f[:, 0]
 | 
						|
        dZ2 = tris_f[:, 2] - tris_f[:, 0]
 | 
						|
        dZ = np.vstack([dZ1, dZ2]).T
 | 
						|
        df = np.empty_like(dZ)
 | 
						|
 | 
						|
        # With np.einsum: could be ej,eji -> ej
 | 
						|
        df[:, 0] = dZ[:, 0]*dM_inv[:, 0, 0] + dZ[:, 1]*dM_inv[:, 1, 0]
 | 
						|
        df[:, 1] = dZ[:, 0]*dM_inv[:, 0, 1] + dZ[:, 1]*dM_inv[:, 1, 1]
 | 
						|
        return df
 | 
						|
 | 
						|
 | 
						|
class _DOF_estimator_min_E(_DOF_estimator_geom):
 | 
						|
    """
 | 
						|
    The 'smoothest' approximation, df is computed through global minimization
 | 
						|
    of the bending energy:
 | 
						|
      E(f) = integral[(d2z/dx2 + d2z/dy2 + 2 d2z/dxdy)**2 dA]
 | 
						|
    """
 | 
						|
    def __init__(self, Interpolator):
 | 
						|
        self._eccs = Interpolator._eccs
 | 
						|
        super().__init__(Interpolator)
 | 
						|
 | 
						|
    def compute_dz(self):
 | 
						|
        """
 | 
						|
        Elliptic solver for bending energy minimization.
 | 
						|
        Uses a dedicated 'toy' sparse Jacobi PCG solver.
 | 
						|
        """
 | 
						|
        # Initial guess for iterative PCG solver.
 | 
						|
        dz_init = super().compute_dz()
 | 
						|
        Uf0 = np.ravel(dz_init)
 | 
						|
 | 
						|
        reference_element = _ReducedHCT_Element()
 | 
						|
        J = CubicTriInterpolator._get_jacobian(self._tris_pts)
 | 
						|
        eccs = self._eccs
 | 
						|
        triangles = self._triangles
 | 
						|
        Uc = self.z[self._triangles]
 | 
						|
 | 
						|
        # Building stiffness matrix and force vector in coo format
 | 
						|
        Kff_rows, Kff_cols, Kff_vals, Ff = reference_element.get_Kff_and_Ff(
 | 
						|
            J, eccs, triangles, Uc)
 | 
						|
 | 
						|
        # Building sparse matrix and solving minimization problem
 | 
						|
        # We could use scipy.sparse direct solver; however to avoid this
 | 
						|
        # external dependency an implementation of a simple PCG solver with
 | 
						|
        # a simple diagonal Jacobi preconditioner is implemented.
 | 
						|
        tol = 1.e-10
 | 
						|
        n_dof = Ff.shape[0]
 | 
						|
        Kff_coo = _Sparse_Matrix_coo(Kff_vals, Kff_rows, Kff_cols,
 | 
						|
                                     shape=(n_dof, n_dof))
 | 
						|
        Kff_coo.compress_csc()
 | 
						|
        Uf, err = _cg(A=Kff_coo, b=Ff, x0=Uf0, tol=tol)
 | 
						|
        # If the PCG did not converge, we return the best guess between Uf0
 | 
						|
        # and Uf.
 | 
						|
        err0 = np.linalg.norm(Kff_coo.dot(Uf0) - Ff)
 | 
						|
        if err0 < err:
 | 
						|
            # Maybe a good occasion to raise a warning here ?
 | 
						|
            _api.warn_external("In TriCubicInterpolator initialization, "
 | 
						|
                               "PCG sparse solver did not converge after "
 | 
						|
                               "1000 iterations. `geom` approximation is "
 | 
						|
                               "used instead of `min_E`")
 | 
						|
            Uf = Uf0
 | 
						|
 | 
						|
        # Building dz from Uf
 | 
						|
        dz = np.empty([self._pts.shape[0], 2], dtype=np.float64)
 | 
						|
        dz[:, 0] = Uf[::2]
 | 
						|
        dz[:, 1] = Uf[1::2]
 | 
						|
        return dz
 | 
						|
 | 
						|
 | 
						|
# The following private :class:_Sparse_Matrix_coo and :func:_cg provide
 | 
						|
# a PCG sparse solver for (symmetric) elliptic problems.
 | 
						|
class _Sparse_Matrix_coo:
 | 
						|
    def __init__(self, vals, rows, cols, shape):
 | 
						|
        """
 | 
						|
        Create a sparse matrix in coo format.
 | 
						|
        *vals*: arrays of values of non-null entries of the matrix
 | 
						|
        *rows*: int arrays of rows of non-null entries of the matrix
 | 
						|
        *cols*: int arrays of cols of non-null entries of the matrix
 | 
						|
        *shape*: 2-tuple (n, m) of matrix shape
 | 
						|
        """
 | 
						|
        self.n, self.m = shape
 | 
						|
        self.vals = np.asarray(vals, dtype=np.float64)
 | 
						|
        self.rows = np.asarray(rows, dtype=np.int32)
 | 
						|
        self.cols = np.asarray(cols, dtype=np.int32)
 | 
						|
 | 
						|
    def dot(self, V):
 | 
						|
        """
 | 
						|
        Dot product of self by a vector *V* in sparse-dense to dense format
 | 
						|
        *V* dense vector of shape (self.m,).
 | 
						|
        """
 | 
						|
        assert V.shape == (self.m,)
 | 
						|
        return np.bincount(self.rows,
 | 
						|
                           weights=self.vals*V[self.cols],
 | 
						|
                           minlength=self.m)
 | 
						|
 | 
						|
    def compress_csc(self):
 | 
						|
        """
 | 
						|
        Compress rows, cols, vals / summing duplicates. Sort for csc format.
 | 
						|
        """
 | 
						|
        _, unique, indices = np.unique(
 | 
						|
            self.rows + self.n*self.cols,
 | 
						|
            return_index=True, return_inverse=True)
 | 
						|
        self.rows = self.rows[unique]
 | 
						|
        self.cols = self.cols[unique]
 | 
						|
        self.vals = np.bincount(indices, weights=self.vals)
 | 
						|
 | 
						|
    def compress_csr(self):
 | 
						|
        """
 | 
						|
        Compress rows, cols, vals / summing duplicates. Sort for csr format.
 | 
						|
        """
 | 
						|
        _, unique, indices = np.unique(
 | 
						|
            self.m*self.rows + self.cols,
 | 
						|
            return_index=True, return_inverse=True)
 | 
						|
        self.rows = self.rows[unique]
 | 
						|
        self.cols = self.cols[unique]
 | 
						|
        self.vals = np.bincount(indices, weights=self.vals)
 | 
						|
 | 
						|
    def to_dense(self):
 | 
						|
        """
 | 
						|
        Return a dense matrix representing self, mainly for debugging purposes.
 | 
						|
        """
 | 
						|
        ret = np.zeros([self.n, self.m], dtype=np.float64)
 | 
						|
        nvals = self.vals.size
 | 
						|
        for i in range(nvals):
 | 
						|
            ret[self.rows[i], self.cols[i]] += self.vals[i]
 | 
						|
        return ret
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return self.to_dense().__str__()
 | 
						|
 | 
						|
    @property
 | 
						|
    def diag(self):
 | 
						|
        """Return the (dense) vector of the diagonal elements."""
 | 
						|
        in_diag = (self.rows == self.cols)
 | 
						|
        diag = np.zeros(min(self.n, self.n), dtype=np.float64)  # default 0.
 | 
						|
        diag[self.rows[in_diag]] = self.vals[in_diag]
 | 
						|
        return diag
 | 
						|
 | 
						|
 | 
						|
def _cg(A, b, x0=None, tol=1.e-10, maxiter=1000):
 | 
						|
    """
 | 
						|
    Use Preconditioned Conjugate Gradient iteration to solve A x = b
 | 
						|
    A simple Jacobi (diagonal) preconditioner is used.
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    A : _Sparse_Matrix_coo
 | 
						|
        *A* must have been compressed before by compress_csc or
 | 
						|
        compress_csr method.
 | 
						|
    b : array
 | 
						|
        Right hand side of the linear system.
 | 
						|
    x0 : array, optional
 | 
						|
        Starting guess for the solution. Defaults to the zero vector.
 | 
						|
    tol : float, optional
 | 
						|
        Tolerance to achieve. The algorithm terminates when the relative
 | 
						|
        residual is below tol. Default is 1e-10.
 | 
						|
    maxiter : int, optional
 | 
						|
        Maximum number of iterations.  Iteration will stop after *maxiter*
 | 
						|
        steps even if the specified tolerance has not been achieved. Defaults
 | 
						|
        to 1000.
 | 
						|
 | 
						|
    Returns
 | 
						|
    -------
 | 
						|
    x : array
 | 
						|
        The converged solution.
 | 
						|
    err : float
 | 
						|
        The absolute error np.linalg.norm(A.dot(x) - b)
 | 
						|
    """
 | 
						|
    n = b.size
 | 
						|
    assert A.n == n
 | 
						|
    assert A.m == n
 | 
						|
    b_norm = np.linalg.norm(b)
 | 
						|
 | 
						|
    # Jacobi pre-conditioner
 | 
						|
    kvec = A.diag
 | 
						|
    # For diag elem < 1e-6 we keep 1e-6.
 | 
						|
    kvec = np.maximum(kvec, 1e-6)
 | 
						|
 | 
						|
    # Initial guess
 | 
						|
    if x0 is None:
 | 
						|
        x = np.zeros(n)
 | 
						|
    else:
 | 
						|
        x = x0
 | 
						|
 | 
						|
    r = b - A.dot(x)
 | 
						|
    w = r/kvec
 | 
						|
 | 
						|
    p = np.zeros(n)
 | 
						|
    beta = 0.0
 | 
						|
    rho = np.dot(r, w)
 | 
						|
    k = 0
 | 
						|
 | 
						|
    # Following C. T. Kelley
 | 
						|
    while (np.sqrt(abs(rho)) > tol*b_norm) and (k < maxiter):
 | 
						|
        p = w + beta*p
 | 
						|
        z = A.dot(p)
 | 
						|
        alpha = rho/np.dot(p, z)
 | 
						|
        r = r - alpha*z
 | 
						|
        w = r/kvec
 | 
						|
        rhoold = rho
 | 
						|
        rho = np.dot(r, w)
 | 
						|
        x = x + alpha*p
 | 
						|
        beta = rho/rhoold
 | 
						|
        # err = np.linalg.norm(A.dot(x) - b)  # absolute accuracy - not used
 | 
						|
        k += 1
 | 
						|
    err = np.linalg.norm(A.dot(x) - b)
 | 
						|
    return x, err
 | 
						|
 | 
						|
 | 
						|
# The following private functions:
 | 
						|
#     :func:`_safe_inv22_vectorized`
 | 
						|
#     :func:`_pseudo_inv22sym_vectorized`
 | 
						|
#     :func:`_scalar_vectorized`
 | 
						|
#     :func:`_transpose_vectorized`
 | 
						|
#     :func:`_roll_vectorized`
 | 
						|
#     :func:`_to_matrix_vectorized`
 | 
						|
#     :func:`_extract_submatrices`
 | 
						|
# provide fast numpy implementation of some standard operations on arrays of
 | 
						|
# matrices - stored as (:, n_rows, n_cols)-shaped np.arrays.
 | 
						|
 | 
						|
# Development note: Dealing with pathologic 'flat' triangles in the
 | 
						|
# CubicTriInterpolator code and impact on (2, 2)-matrix inversion functions
 | 
						|
# :func:`_safe_inv22_vectorized` and :func:`_pseudo_inv22sym_vectorized`.
 | 
						|
#
 | 
						|
# Goals:
 | 
						|
# 1) The CubicTriInterpolator should be able to handle flat or almost flat
 | 
						|
#    triangles without raising an error,
 | 
						|
# 2) These degenerated triangles should have no impact on the automatic dof
 | 
						|
#    calculation (associated with null weight for the _DOF_estimator_geom and
 | 
						|
#    with null energy for the _DOF_estimator_min_E),
 | 
						|
# 3) Linear patch test should be passed exactly on degenerated meshes,
 | 
						|
# 4) Interpolation (with :meth:`_interpolate_single_key` or
 | 
						|
#    :meth:`_interpolate_multi_key`) shall be correctly handled even *inside*
 | 
						|
#    the pathologic triangles, to interact correctly with a TriRefiner class.
 | 
						|
#
 | 
						|
# Difficulties:
 | 
						|
# Flat triangles have rank-deficient *J* (so-called jacobian matrix) and
 | 
						|
# *metric* (the metric tensor = J x J.T). Computation of the local
 | 
						|
# tangent plane is also problematic.
 | 
						|
#
 | 
						|
# Implementation:
 | 
						|
# Most of the time, when computing the inverse of a rank-deficient matrix it
 | 
						|
# is safe to simply return the null matrix (which is the implementation in
 | 
						|
# :func:`_safe_inv22_vectorized`). This is because of point 2), itself
 | 
						|
# enforced by:
 | 
						|
#    - null area hence null energy in :class:`_DOF_estimator_min_E`
 | 
						|
#    - angles close or equal to 0 or np.pi hence null weight in
 | 
						|
#      :class:`_DOF_estimator_geom`.
 | 
						|
#      Note that the function angle -> weight is continuous and maximum for an
 | 
						|
#      angle np.pi/2 (refer to :meth:`compute_geom_weights`)
 | 
						|
# The exception is the computation of barycentric coordinates, which is done
 | 
						|
# by inversion of the *metric* matrix. In this case, we need to compute a set
 | 
						|
# of valid coordinates (1 among numerous possibilities), to ensure point 4).
 | 
						|
# We benefit here from the symmetry of metric = J x J.T, which makes it easier
 | 
						|
# to compute a pseudo-inverse in :func:`_pseudo_inv22sym_vectorized`
 | 
						|
def _safe_inv22_vectorized(M):
 | 
						|
    """
 | 
						|
    Inversion of arrays of (2, 2) matrices, returns 0 for rank-deficient
 | 
						|
    matrices.
 | 
						|
 | 
						|
    *M* : array of (2, 2) matrices to inverse, shape (n, 2, 2)
 | 
						|
    """
 | 
						|
    _api.check_shape((None, 2, 2), M=M)
 | 
						|
    M_inv = np.empty_like(M)
 | 
						|
    prod1 = M[:, 0, 0]*M[:, 1, 1]
 | 
						|
    delta = prod1 - M[:, 0, 1]*M[:, 1, 0]
 | 
						|
 | 
						|
    # We set delta_inv to 0. in case of a rank deficient matrix; a
 | 
						|
    # rank-deficient input matrix *M* will lead to a null matrix in output
 | 
						|
    rank2 = (np.abs(delta) > 1e-8*np.abs(prod1))
 | 
						|
    if np.all(rank2):
 | 
						|
        # Normal 'optimized' flow.
 | 
						|
        delta_inv = 1./delta
 | 
						|
    else:
 | 
						|
        # 'Pathologic' flow.
 | 
						|
        delta_inv = np.zeros(M.shape[0])
 | 
						|
        delta_inv[rank2] = 1./delta[rank2]
 | 
						|
 | 
						|
    M_inv[:, 0, 0] = M[:, 1, 1]*delta_inv
 | 
						|
    M_inv[:, 0, 1] = -M[:, 0, 1]*delta_inv
 | 
						|
    M_inv[:, 1, 0] = -M[:, 1, 0]*delta_inv
 | 
						|
    M_inv[:, 1, 1] = M[:, 0, 0]*delta_inv
 | 
						|
    return M_inv
 | 
						|
 | 
						|
 | 
						|
def _pseudo_inv22sym_vectorized(M):
 | 
						|
    """
 | 
						|
    Inversion of arrays of (2, 2) SYMMETRIC matrices; returns the
 | 
						|
    (Moore-Penrose) pseudo-inverse for rank-deficient matrices.
 | 
						|
 | 
						|
    In case M is of rank 1, we have M = trace(M) x P where P is the orthogonal
 | 
						|
    projection on Im(M), and we return trace(M)^-1 x P == M / trace(M)**2
 | 
						|
    In case M is of rank 0, we return the null matrix.
 | 
						|
 | 
						|
    *M* : array of (2, 2) matrices to inverse, shape (n, 2, 2)
 | 
						|
    """
 | 
						|
    _api.check_shape((None, 2, 2), M=M)
 | 
						|
    M_inv = np.empty_like(M)
 | 
						|
    prod1 = M[:, 0, 0]*M[:, 1, 1]
 | 
						|
    delta = prod1 - M[:, 0, 1]*M[:, 1, 0]
 | 
						|
    rank2 = (np.abs(delta) > 1e-8*np.abs(prod1))
 | 
						|
 | 
						|
    if np.all(rank2):
 | 
						|
        # Normal 'optimized' flow.
 | 
						|
        M_inv[:, 0, 0] = M[:, 1, 1] / delta
 | 
						|
        M_inv[:, 0, 1] = -M[:, 0, 1] / delta
 | 
						|
        M_inv[:, 1, 0] = -M[:, 1, 0] / delta
 | 
						|
        M_inv[:, 1, 1] = M[:, 0, 0] / delta
 | 
						|
    else:
 | 
						|
        # 'Pathologic' flow.
 | 
						|
        # Here we have to deal with 2 sub-cases
 | 
						|
        # 1) First sub-case: matrices of rank 2:
 | 
						|
        delta = delta[rank2]
 | 
						|
        M_inv[rank2, 0, 0] = M[rank2, 1, 1] / delta
 | 
						|
        M_inv[rank2, 0, 1] = -M[rank2, 0, 1] / delta
 | 
						|
        M_inv[rank2, 1, 0] = -M[rank2, 1, 0] / delta
 | 
						|
        M_inv[rank2, 1, 1] = M[rank2, 0, 0] / delta
 | 
						|
        # 2) Second sub-case: rank-deficient matrices of rank 0 and 1:
 | 
						|
        rank01 = ~rank2
 | 
						|
        tr = M[rank01, 0, 0] + M[rank01, 1, 1]
 | 
						|
        tr_zeros = (np.abs(tr) < 1.e-8)
 | 
						|
        sq_tr_inv = (1.-tr_zeros) / (tr**2+tr_zeros)
 | 
						|
        # sq_tr_inv = 1. / tr**2
 | 
						|
        M_inv[rank01, 0, 0] = M[rank01, 0, 0] * sq_tr_inv
 | 
						|
        M_inv[rank01, 0, 1] = M[rank01, 0, 1] * sq_tr_inv
 | 
						|
        M_inv[rank01, 1, 0] = M[rank01, 1, 0] * sq_tr_inv
 | 
						|
        M_inv[rank01, 1, 1] = M[rank01, 1, 1] * sq_tr_inv
 | 
						|
 | 
						|
    return M_inv
 | 
						|
 | 
						|
 | 
						|
def _scalar_vectorized(scalar, M):
 | 
						|
    """
 | 
						|
    Scalar product between scalars and matrices.
 | 
						|
    """
 | 
						|
    return scalar[:, np.newaxis, np.newaxis]*M
 | 
						|
 | 
						|
 | 
						|
def _transpose_vectorized(M):
 | 
						|
    """
 | 
						|
    Transposition of an array of matrices *M*.
 | 
						|
    """
 | 
						|
    return np.transpose(M, [0, 2, 1])
 | 
						|
 | 
						|
 | 
						|
def _roll_vectorized(M, roll_indices, axis):
 | 
						|
    """
 | 
						|
    Roll an array of matrices along *axis* (0: rows, 1: columns) according to
 | 
						|
    an array of indices *roll_indices*.
 | 
						|
    """
 | 
						|
    assert axis in [0, 1]
 | 
						|
    ndim = M.ndim
 | 
						|
    assert ndim == 3
 | 
						|
    ndim_roll = roll_indices.ndim
 | 
						|
    assert ndim_roll == 1
 | 
						|
    sh = M.shape
 | 
						|
    r, c = sh[-2:]
 | 
						|
    assert sh[0] == roll_indices.shape[0]
 | 
						|
    vec_indices = np.arange(sh[0], dtype=np.int32)
 | 
						|
 | 
						|
    # Builds the rolled matrix
 | 
						|
    M_roll = np.empty_like(M)
 | 
						|
    if axis == 0:
 | 
						|
        for ir in range(r):
 | 
						|
            for ic in range(c):
 | 
						|
                M_roll[:, ir, ic] = M[vec_indices, (-roll_indices+ir) % r, ic]
 | 
						|
    else:  # 1
 | 
						|
        for ir in range(r):
 | 
						|
            for ic in range(c):
 | 
						|
                M_roll[:, ir, ic] = M[vec_indices, ir, (-roll_indices+ic) % c]
 | 
						|
    return M_roll
 | 
						|
 | 
						|
 | 
						|
def _to_matrix_vectorized(M):
 | 
						|
    """
 | 
						|
    Build an array of matrices from individuals np.arrays of identical shapes.
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    M
 | 
						|
        ncols-list of nrows-lists of shape sh.
 | 
						|
 | 
						|
    Returns
 | 
						|
    -------
 | 
						|
    M_res : np.array of shape (sh, nrow, ncols)
 | 
						|
        *M_res* satisfies ``M_res[..., i, j] = M[i][j]``.
 | 
						|
    """
 | 
						|
    assert isinstance(M, (tuple, list))
 | 
						|
    assert all(isinstance(item, (tuple, list)) for item in M)
 | 
						|
    c_vec = np.asarray([len(item) for item in M])
 | 
						|
    assert np.all(c_vec-c_vec[0] == 0)
 | 
						|
    r = len(M)
 | 
						|
    c = c_vec[0]
 | 
						|
    M00 = np.asarray(M[0][0])
 | 
						|
    dt = M00.dtype
 | 
						|
    sh = [M00.shape[0], r, c]
 | 
						|
    M_ret = np.empty(sh, dtype=dt)
 | 
						|
    for irow in range(r):
 | 
						|
        for icol in range(c):
 | 
						|
            M_ret[:, irow, icol] = np.asarray(M[irow][icol])
 | 
						|
    return M_ret
 | 
						|
 | 
						|
 | 
						|
def _extract_submatrices(M, block_indices, block_size, axis):
 | 
						|
    """
 | 
						|
    Extract selected blocks of a matrices *M* depending on parameters
 | 
						|
    *block_indices* and *block_size*.
 | 
						|
 | 
						|
    Returns the array of extracted matrices *Mres* so that ::
 | 
						|
 | 
						|
        M_res[..., ir, :] = M[(block_indices*block_size+ir), :]
 | 
						|
    """
 | 
						|
    assert block_indices.ndim == 1
 | 
						|
    assert axis in [0, 1]
 | 
						|
 | 
						|
    r, c = M.shape
 | 
						|
    if axis == 0:
 | 
						|
        sh = [block_indices.shape[0], block_size, c]
 | 
						|
    else:  # 1
 | 
						|
        sh = [block_indices.shape[0], r, block_size]
 | 
						|
 | 
						|
    dt = M.dtype
 | 
						|
    M_res = np.empty(sh, dtype=dt)
 | 
						|
    if axis == 0:
 | 
						|
        for ir in range(block_size):
 | 
						|
            M_res[:, ir, :] = M[(block_indices*block_size+ir), :]
 | 
						|
    else:  # 1
 | 
						|
        for ic in range(block_size):
 | 
						|
            M_res[:, :, ic] = M[:, (block_indices*block_size+ic)]
 | 
						|
 | 
						|
    return M_res
 |