# Library of SPAM image correlation functions.
# Copyright (C) 2020 SPAM Contributors
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""
Set of functions for performing mechanically regularised global DVC.
The regularisation implementation is taken from [Mendoza2019]_.
Here is how it can be used:
.. code-block:: python
import spam.mesh
import spam.DIC
# Mesh
points, connectivity = spam.mesh.createCylinder([-24, 27], 12.5, 97.3, 10, zOrigin=-4.2)
# Regularisation: parameters
myParameters = {
"young": 10,
"poisson": 0.25,
"dirichlet": {
"z_start": {"dof": [0]},
"z_end": {"dof": [0]},
},
}
p = spam.DIC.regularisationParameters(myParameters)
# Regularisation step 1: get labels
labels = spam.DIC.surfaceLabels(points, surfaces=p["surfaces"])
# Regularisation step 2: build regularisation matrix
regularisationMatrix, regularisationField = spam.DIC.regularisationMatrix(
points,
connectivity,
young=p["young"],
poisson=p["poisson"],
xiBulk=p["xi"],
dirichlet=p["dirichlet"],
periods=p["periods"],
labels=labels,
)
# Global DVC
globalCorrelation(
im1,
im2,
points,
connectivity,
regularisationMatrix=regularisationMatrix,
regularisationField=regularisationField,
)
.. [Mendoza2019] A. Mendoza, J. Neggers, F. Hild, S Roux (2019). Complete mechanical regularization applied to digital image and volume correlation.
*Computer Methods in Applied Mechanics and Engineering*, Volume 355, 1 October 2019, Pages 27-43
https://doi.org/10.1016/j.cma.2019.06.005
.. _RegularisationParameter: _static/gdvc-regularisation-parameters.yaml
Note
----
Make a link to the script
"""
import time # for debug
import numpy
import spam.DIC
import spam.label
import spam.mesh
import tifffile
# 2017-05-29 ER and EA
# This is spam's C++ DIC toolkit, but since we're in the tools/ directory we can import it directly
from spam.DIC.DICToolkit import (
computeDICglobalMatrix,
computeDICglobalVector,
computeGradientPerTet,
)
def _check_symmetric(a, rtol=1e-05, atol=1e-08):
"""Helper function to check if a matrix is symmetric."""
return numpy.allclose(a, a.T, rtol=rtol, atol=atol)
def _errorCalc(im1, im2, im2ref, meshPaddingSlice):
"""Helper function to compute the error between two images."""
errorInitial = numpy.sqrt(numpy.square(im2ref[meshPaddingSlice] - im1[meshPaddingSlice]).sum())
errorCurrent = numpy.sqrt(numpy.square(im2[meshPaddingSlice] - im1[meshPaddingSlice]).sum())
return errorCurrent / errorInitial
def _computeFunctional(u, K, L=None):
"""Helper function to compute global DVC functional"""
u = numpy.ravel(u)
if L is None:
return numpy.matmul(u.T, numpy.matmul(K.T, numpy.matmul(K, u)))
else:
return numpy.matmul(u.T, numpy.matmul(K.T, numpy.matmul(L, numpy.matmul(K, u))))
def _normalisedEnergies(v, Km, Ks, Ls):
"""Helper function to compute globale DVC normalised energies"""
Em = _computeFunctional(v, Km)
Es = [_computeFunctional(v, K, L=L) for K, L in zip(Ks, Ls)]
return Em, Es
def _computeWeights(kMag, xiBulk, xiDirichlet):
"""Helper function to compute global DVC weights"""
print(f"[regularisation] xi bulk = {xiBulk:.2f}")
Wm = (xiBulk * kMag) ** 4
Ws = []
for i, xi in enumerate(xiDirichlet):
print(f"[regularisation] xi dirichlet {i} = {xi:.2f}")
Ws.append((xi * kMag) ** 4)
return Wm, Ws
[docs]
def surfaceLabels(points, surfaces=[], connectivity=None):
"""Creates a label for each points based on a list of keywords that corresponds to surfaces (`ie.` ``["z_start", "z_end"]``).
The label value is based on the position of the surface in the list.
Parameters
----------
points: Nx3 array
List of coordinates of the mesh nodes.
surfaces: list of string
A list of keywords corresponding to surfaces.
- ``z_start``: plane at ``z == min(points[:,0])``
- ``z_end``: plane at ``z == max(points[:,0])``
- ``y_start``: plane at ``y == min(points[:,1])``
- ``y_end``: plane at ``y == max(points[:,1])``
- ``x_start``: plane at ``x == min(points[:,2])``
- ``x_end``: plane at ``x == max(points[:,2])``
- ``z_lateral``: lateral surface of a cylinder oriented in the first direction.
- ``y_lateral``: lateral surface of a cylinder oriented in the second direction.
- ``x_lateral``: lateral surface of a cylinder oriented in the third direction.
connectivity: array (only for debug purposes)
Connectivity matrix. If set, creates a VTK file with labels.
Returns
-------
N x 1 array:
Surface label for each points.
Note
----
- Surface labels start at 1, 0 corresponding to bulk or not specified surfaces.
- Points belong to a single surface. The first surface in `surfaces` prevails.
Example
-------
>>> import spam.mesh
>>> import spam.DIC
>>>
>>> # create mesh
>>> points, connectivity = spam.mesh.createCylinder([-24, 27], 12.5, 97.3, 10, zOrigin=-4.2)
>>> # compute labels for bottom and top surface only
>>> labels = spam.DIC.surfaceLabels(points, surfaces=["z_start", "z_end"], connectivity=connectivity)
"""
def _belongs_to_lateral_surface(point, centre, radius, epsilon=1e-6):
"""Returns True if point belongs to the lateral surface of a cylinder"""
d = ((centre[0] - point[1]) ** 2 + (centre[1] - point[2]) ** 2) ** 0.5
return abs(d - radius) < epsilon
def _belongs_to_plane(point, direction, coordinate, epsilon=1e-6):
"""Returns True if point belongs to a surface of position `coordinate` in direction `direction`"""
return abs(point[direction] - coordinate) < epsilon
# Get geometrical data from the points coordinates
maxCoord = numpy.max(points, axis=0)
minCoord = numpy.min(points, axis=0)
centre = [0.0] * 3
radii = [0.0] * 3
for dz in range(3):
dy = (dz + 1) % 3
dx = (dz + 2) % 3
centre[dz] = [0.5 * (minCoord[d] + maxCoord[d]) for d in [dy, dx]]
radii[dz] = 0.25 * (maxCoord[dx] + maxCoord[dy] - minCoord[dx] - minCoord[dy])
labels = numpy.zeros(points.shape[0], dtype=int)
# Loop over the points
for A, point in enumerate(points):
# Loop over the surfaces to enforce order for edges and vertices
for i, surface in enumerate(surfaces):
direction, position = surface.split("_")
direction = {"z": 0, "y": 1, "x": 2}[direction] # direction as integer z: 0, y: 1, x: 2
if position == "start" and _belongs_to_plane(point, direction, minCoord[direction]):
labels[A] = i + 1
break
elif position == "end" and _belongs_to_plane(point, direction, maxCoord[direction]):
labels[A] = i + 1
break
elif position == "lateral" and _belongs_to_lateral_surface(point, centre[direction], radii[direction]):
labels[A] = i + 1
break
if connectivity is not None: # debug
spam.helpers.writeUnstructuredVTK(points, connectivity, pointData={"label": labels}, fileName="gdvc-labels.vtk")
return labels
[docs]
def projectionMatrices(points, connectivity, labels, dirichlet=[]):
"""Compute binary diagonal matrices and Laplace-Beltrami operator.
Details on the meaning of these matrices can be found in [Mendoza2019]_ `eq. (12) to (14)` and `eq. (17) to (19)`.
Parameters
----------
points: Nx3 array
List of coordinates of the mesh nodes.
connectivity: Mx4 numpay array
Connectivity matrice of the mesh (tetrahedra).
labels: Nx1 array
Surface labels for each points as defined in ``spam.DIC.surfaceLabels()``
dirichlet: list of (int, list, )
Each element of the list defines a surface with Dirichlet boundary conditions by a tuple.
- The first element of the tuple is the surface label which should belong to `labels`.
- The second element of the tuple is a list of degrees of freedom directions to consider for the Dirichlet boundary conditions.
- Extra elements in the tuple are ignored.
.. code-block:: python
dirichlet = [
# surface 1, dof in x, y
(
1,
[1, 2],
),
# surface 3, dof in z
(
3,
[0],
),
]
Returns
-------
3Nx3N array:
:math:`D_m` The binary diagonal matrix corresponding to all dofs of the bulk and neumann surfaces nodes (ie the non dirichlet)
List of 3Nx3N array:
:math:`D_{S_i}` Binary diagonal matrices corresponding to the dofs of each Dirichlet surfaces
List of 3Nx3N array:
:math:`L_{S_i}` List of Laplace-Beltrami operators corresponding to the same Dirichlet surfaces.
"""
if dirichlet is None:
dirichlet = []
# all matrices out here are of shape ndof x ndof (same as stiffness matrix)
ndof = 3 * points.shape[0]
print(f"[projectionMatrices] number of dirichlet surfaces: {len(dirichlet)} ({dirichlet})")
# initialise list of A and B matrices in order to compute the L operator (size ndof x ndof)
matricesA = [numpy.zeros((ndof, ndof)) for _ in dirichlet]
matricesB = [numpy.zeros((ndof, ndof)) for _ in dirichlet]
matricesD = [numpy.zeros((ndof, ndof)) for _ in dirichlet]
# list of all nodes and dof numbers on dirichlet surfaces
# NOTE: taking label == 0 (ie background) leads to singlular sub_A matrix
dirichletSurfaces = [(numpy.where(labels == d[0])[0], d[1]) for d in dirichlet if d[0]]
DEBUG_dirichlet_connectivity_all = []
DEBUG_dirichlet_points_all = []
for si, _ in enumerate(dirichlet):
DEBUG_dirichlet_connectivity_all.append([])
DEBUG_dirichlet_points_all.append([])
# # loop over dirichlet matrix
for si, (surfacePoints, dofDirections) in enumerate(dirichletSurfaces):
dofs = [3 * A + d for A in surfacePoints for d in dofDirections]
print(f"[projectionMatrices] Dirichlet #{si} label {dirichlet[si][0]}) #points = {len(surfacePoints)} dofs directions = {dofDirections} {len(dofs)} dofs")
surfacePoints = set(surfacePoints)
for tetra_connectivity in connectivity:
tri_con = list(surfacePoints.intersection(tetra_connectivity))
tri_nodes = [list(points[i]) for i in tri_con]
if len(tri_con) != 3:
# the element doesn't have a triangle a this surface
continue
# get 4th point of the tetrahedron to compute 3D shape functions
point_4_n = list(set(tri_con).symmetric_difference(set(tetra_connectivity)))[0]
_, tri_coefficients = spam.mesh.shapeFunctions(tri_nodes + [list(points[point_4_n])])
# we've got a triangle on the surface!!!
DEBUG_dirichlet_connectivity_all[si].append(list(tri_con))
DEBUG_dirichlet_points_all[si].append(tri_nodes)
# assemble the dirichlet connectivity matrix
a = numpy.array(tri_coefficients[:3, 0])
bcd = numpy.array(tri_coefficients[:3, 1:])
B = numpy.array(tri_nodes).T
def phi(i, L):
return a[i] + numpy.matmul(bcd[i], numpy.matmul(B, L.T))
def dphi(i):
return bcd[i]
# gauss points
L_gp = numpy.array([[0.5, 0.5, 0.0], [0.5, 0.0, 0.5], [0.0, 0.5, 0.5]])
# STEP 3: compute the area
area = 0.5 * numpy.linalg.norm(
numpy.cross(
numpy.subtract(tri_nodes[1], tri_nodes[0]),
numpy.subtract(tri_nodes[2], tri_nodes[0]),
)
)
# STEP 3: compute inner products of the shape functions
for i in range(3):
for j in range(3):
inner = 0.0
for L in L_gp:
# print(inner, i, j, L, phi(i, L), phi(j, L))
inner += phi(i, L) * phi(j, L)
inner *= area / 3.0 # the 1/3. comes from the weight
dinner = area * numpy.inner(dphi(i), dphi(j))
for d in dofDirections:
P = int(3 * tri_con[i] + d)
Q = int(3 * tri_con[j] + d)
matricesA[si][P, Q] += inner
matricesB[si][P, Q] += dinner
# # invert matrix A
# for si, (surfacePoints, dofDirections) in enumerate(dirichletSurfaces):
dofs = [3 * A + d for A in surfacePoints for d in dofDirections]
# 1. extract submatrix A from full size matricesA and invert
sub_A = matricesA[si][:, dofs]
sub_A = sub_A[dofs, :]
sub_A = numpy.linalg.inv(sub_A)
# 2. push back inverted submatrix into full size matricesA
for i, P in enumerate(dofs):
matricesD[si][P, P] = 1
for j, Q in enumerate(dofs):
matricesA[si][P, Q] = sub_A[i, j]
# return also bulk + neumann binary diagonal projection matrices
# Dm = Db + Dn = I - Sum(Dd)
Dm = numpy.eye(ndof)
for Ds in matricesD:
Dm -= Ds
# return a list of D and L = AxB for each dirichlet surface
return Dm, matricesD, [numpy.matmul(A, B) for A, B in zip(matricesA, matricesB)]
[docs]
def isochoricField(points, periods=3, connectivity=None):
r"""Helper function to build the isochoric test function used to normalised the mechanical regularisation functionals.
The function is a shear displacement field with its wave vector in the direction of the longest dimension of the mesh.
.. math::
\textbf{v}(\textbf{x}) = \sin(2 \pi \textbf{k}\cdot \textbf{x} )
[Mendoza2019]_ `eq. (25)`.
Parameters
----------
points: Nx3 array
List of coordinates of the mesh nodes.
periods: int
The number of periods of the shear wave.
connectivity: array (only for debug purposes)
Connectivity matrix. If set, creates a VTK file with the field.
Returns
-------
float:
The magnitude of the wave vector :math:`\bf{k}`.
Nx3 array:
The field :math:`\bf{v}` at each nodes.
"""
sizes = [ma - mi for ma, mi in zip(numpy.max(points, axis=0), numpy.min(points, axis=0))]
d = numpy.argmax(sizes) # direction of largest size
size = float(max(sizes)) # largest size
mag = periods / size
v = numpy.zeros_like(points)
for i, point in enumerate(points):
kdotx = mag * point[d]
v[i][(d + 0) % 3] = 0
v[i][(d + 1) % 3] = numpy.sqrt(0.5) * numpy.sin(2 * numpy.pi * kdotx)
v[i][(d + 2) % 3] = numpy.sqrt(0.5) * numpy.sin(2 * numpy.pi * kdotx)
if connectivity is not None:
print("[isochoricField] plot vtk")
spam.helpers.writeUnstructuredVTK(
points,
connectivity,
elementType="tetra",
pointData={"v": v},
cellData={},
fileName="gdvc-v.vtk",
)
return mag, v
[docs]
def regularisationMatrix(points, connectivity, young=1, poisson=0.25, voxelSize=1, xiBulk=None, dirichlet=[], labels=[], periods=3):
r"""Computes the mechanical regularisation matrix :math:`M_{reg}` for the global DVC:
$$(M_{reg} + M_{c}) \\delta\\textbf{u} = \\textbf{b} - M_{reg} \\textbf{u} $$
where
$$M_{reg} = M_m + \\sum_{i} M_{S_i}$$
corresponds to both bulk/Neuman and Dirichlet surfaces contributions ([Mendoza2019]_ `eq. (29) and (30)`).
Parameters
----------
points: Nx3 array
List of coordinates of the mesh nodes.
connectivity: Mx4 array
Connectivity matrix of the mesh.
young: float (optional)
The Young modulus used to build the stiffness matrix :math:`K` from which :math:`M_m` and :math:`M_{S_i}` are derived.
Default = 1 (it is useless to change this value if you don't impose forces with meaningful units on the Neumann surfaces)
poisson: float (optional)
The Poisson ratio used to build the stiffness matrix :math:`K` from which :math:`M_m` and :math:`M_{S_i}` are derived.
Default = 0.25
voxelSize: float (optional)
The size of a voxel in the same unit used to set the Young modulus assuming that the mesh geometry is in voxels.
This voxel size :math:`v_s` is used to convert the Young modulus in Newton per voxels squared.
Default = 1 (it is useless to change this value if you don't impose forces with meaningful units on the Neumann surfaces).
xiBulk: float (optional)
The regularisation length :math:`\xi_m` of the bulk/Neumann contribution :math:`M_{reg}`.
It has to be compared to the characteristic length of the mesh.
The bigger the regularisation length the more important the regularisation contribution.
If None it is taken to be equal to the mesh characteristic length.
Default = None
dirichlet: list of (int, list, float) (optional)
Each element of the list defines a surface with Dirichlet boundary conditions by a tuple.
- The first element of the tuple is the surface label which should belong to `labels`.
- The second element of the tuple is a list of degrees of freedom directions to consider for the Dirichlet boundary conditions.
- The third element of the tuple correspond to the regularisation length of the surface :math:`\xi_{S_i}`
.. code-block:: python
dirichlet = [
# surface 1, dof in x, y, xi = 0.1
[1, [1, 2], 0.1],
# surface 3, dof in z, xi not set
[3, [0], None],
]
labels: Nx1 array (optional)
Surface labels for each points as defined in ``spam.DIC.surfaceLabels()``. Mandatory only if ``dirichlet`` is set.
periods: float (optional)
Number of periods of the isochoric function :math:`v` used to compute the normalized energy ([Mendoza2019]_ `eq. (27)`).
:math:`v` is computed with ``spam.DIC.isochoricField()`` with a default number of periods of 3.
Returns
-------
3Nx3N array:
:math:`M_{req}` the regularisation matrix.
Nx3 array:
:math:`v` the isochoric field at each nodes of the mesh.
Note
----
- The ``dirichlet`` argument is compatible with the one in ``spam.DIC.projectionMatrices()``
- As long as you don't impose forces to the Neumann surfaces it is usless to set a specific Young modulus and a voxel size.
- Imposing forces is not implemented yet :)
Warning
-------
This function is not tested yet
"""
def _imshow(matlist, title):
from matplotlib import pyplot as plt
if not isinstance(matlist, list):
matlist = [matlist]
for i, mat in enumerate(matlist):
plt.imshow(mat)
plt.title(f"{title} {i}")
# plt.show()
print(f"[regularisation] Young modulus using si: {young}")
young *= voxelSize**2
print(f"[regularisation] Young modulus using voxels: {young}")
print("[regularisation] Build projection matrices")
Dm, Ds, Ls = projectionMatrices(points, connectivity, labels, dirichlet=dirichlet)
_imshow(Dm, "Dm")
_imshow(Ds, "Ds")
_imshow(Ls, "Ls")
print("[regularisation] Build global stiffness matrix")
K = spam.mesh.globalStiffnessMatrix(points, connectivity, young, poisson)
print("[regularisation] Build bulk stiffness matrix")
Km = numpy.matmul(Dm, K)
print("[regularisation] Build dirichlet stiffness matrices")
Ks = [numpy.matmul(Ds_i, K) for Ds_i in Ds]
_imshow(K, "K")
_imshow(Km, "Km")
_imshow(Ks, "Ks")
del K
print("[regularisation] Build isochoric function")
kMag, v = isochoricField(points, periods=periods)
print("[regularisation] Compute normalised energy and weight")
Em, Es = _normalisedEnergies(v, Km, Ks, Ls)
print(f'[regularisation] Em = {Em:.3e}, Es = {" ".join([f"{_:.3e}" for _ in Es])}')
lc = spam.mesh.getMeshCharacteristicLength(points, connectivity)
print(f"[regularisation] mesh characteristic length lc = {lc}")
xiBulk = lc if xiBulk is None else xiBulk
xiDirichlet = [lc if _[2] is None else _[2] for _ in dirichlet]
Wm, Ws = _computeWeights(kMag, xiBulk, xiDirichlet)
print(f'[regularisation] Wm = {Wm:.2f}, Ws = {" ".join([f"{_:.2f}" for _ in Ws])}')
print(f'[regularisation] Wm/Em = {Wm/Em:.3e} Ws/Es = {" ".join([f"{a/b:.3e}" for a, b in zip(Ws, Es)])}')
# 5.4 compute Mreg
Mreg = numpy.zeros_like(Km)
Mreg = Wm * numpy.matmul(Km.T, Km) / Em
for W, E, K, L in [(W, E, K, L) for W, E, K, L in zip(Ws, Es, Ks, Ls) if E]:
Mreg += W * numpy.matmul(K.T, numpy.matmul(L, K)) / E
_imshow(Mreg, "Mreg")
return Mreg, v
[docs]
def regularisationParameters(userParameters):
"""
Convert a user friendly dictionary of parameters into a dictionary of variables compatible with the regularisation functions of this module.
Parameters
----------
userParameters: (str | dict)
The user parameters for the mechanical regularisation scheme. It can be:
- if ``str``: the path to a YAML file. A dummy example can be downloaded here: `RegularisationParameter`_
- if ``dict``: a dictionary containing the following keys and values:
.. code-block:: python
{
# bulk / Neumann regularisation
"young": 10, # mandatory (the Young modulus)
"poisson": 0.25, # mandatory (the Poisson ratio)
"xiBulk": 30, # optional (the bulk/Neumann regularisation lentgh)
"periods": 3, # the number of periods of the isochoric function (optional)
"voxelSize": 0.01, # the voxel size (optional)
# Information about the Dirichlet surface regularisation
# Each surface is defined by a search keyword among
# z_start, z_end, y_start, y_end, x_start and x_end for plane lookups
# z_lateral, y_lateral, x_lateral for cylinder lateral surface lookups
"dirichlet": {
"z_start": { # surface label 1
"xi": 5, # the regularisation length (optional)
"dof": [0, 1, 2], # mandatory
},
"z_end": {"dof": [0]}, # surface label 2 # mandatory
},
}
Returns
-------
dict:
A dictionary with the variables needed for the regularisation functions.
The dictionary keys are named to match the function's signatures:
- ``surface``: the dirichlet surfaces for ``spam.DIC.surfaceLabels()``
- ``dirichlet``: the dirichlet surfaces for ``spam.DIC.regularisationMatrix()``
- ``young``: the Young modulus for ``spam.DIC.regularisationMatrix()``
- ``poisson``: the Poisson ratio for ``spam.DIC.regularisationMatrix()``
- ``xiBulk``: the bulk characteristic lentgh for ``spam.DIC.regularisationMatrix()``
- ``periods``: the Poisson ratio for ``spam.DIC.regularisationMatrix()``
- ``voxelSize``: the Poisson ratio for ``spam.DIC.regularisationMatrix()``
"""
surfaces = [k for k in userParameters.get("dirichlet", {})]
dirichlet = [(i + 1, surf["dof"], surf.get("xi")) for i, surf in enumerate(userParameters.get("dirichlet", {}).values())]
young = userParameters["young"]
poisson = userParameters["poisson"]
xiBulk = userParameters.get("xiBulk")
periods = userParameters.get("periods", 3)
voxelSize = userParameters.get("voxelSize", 1.0)
parameters = {"surfaces": surfaces, "young": young, "poisson": poisson, "xiBulk": xiBulk, "dirichlet": dirichlet, "periods": periods, "voxelSize": voxelSize}
for k, v in parameters.items():
print(f"[regularisation parameters] {k:<10}: {v}")
return parameters
[docs]
def globalCorrelation(
im1,
im2,
points,
connectivity,
regularisationMatrix=None,
regularisationField=None,
initialDisplacements=None,
convergenceCriterion=0.01,
maxIterations=20,
medianFilterEachIteration=False,
debugFiles=False,
prefix="globalCorrelation",
nThreads=None,
):
"""
Global DVC (works only with 3D images).
Parameters
----------
im1 : 3D array
Reference image in which the mesh is defined
im2 : 3D array
Deformed image, should be same size as im1
points : M x 3 array
M nodal coordinates in reference configuration
connectivity : N x 4 array
connectivityhedral connectivity generated by spam.mesh.triangulate() for example
regularisationMatrix : 3N x 3N array (optional)
Mechanical regularisation stiffness matrix. If None no mechanical regularisation is applied.
First output of `spam.DIC.regularisationMatrix`
regularisationField : N x 3 array (optional)
Isochoric displacement field used to compute the normalisation energies.
Second output of `spam.DIC.regularisationMatrix`
initialDisplacements : M x 3 array of floats (optional)
Initial guess for nodal displacements, must be coherent with input mesh
Default = None
convergenceCriterion : float
Convergence criterion for change in displacements in px
Default = 0.01
maxIterations : int
Number of iterations to stop after if convergence has not been reached
Default = 20
debugFiles : bool
Write temporary results to file for debugging?
Default = 'globalCorrelation'
prefix : string
Output file prefix for debugFiles
Default = None
Returns
-------
displacements : N x 3 array of floats
(converged?) Nodal displacements
Example
-------
>>> import spam.DIC
>>> spam.DIC.globalCorrelation(
imRef,
imDef
)
"""
import multiprocessing
try:
multiprocessing.set_start_method("fork")
except RuntimeError:
pass
import spam.helpers
import spam.mesh
# Global number of processes
nThreads = multiprocessing.cpu_count() if nThreads is None else nThreads
print(f"[globalCorrelation] C++ parallelisation on {nThreads} threads")
print(f"[globalCorrelation] Convergence criterion = {convergenceCriterion}")
print(f"[globalCorrelation] Max iterations = {maxIterations}")
print("[globalCorrelation] Converting im1 to 32-bit float")
im1 = im1.astype("<f4")
points = points.astype("<f8")
connectivity = connectivity.astype("<u4")
maxCoord = numpy.amax(points, axis=0).astype("<u2")
minCoord = numpy.amin(points, axis=0).astype("<u2")
print(f"[globalCorrelation] Mesh box: min = {minCoord} max = {maxCoord}")
meshPaddingSlice = (
slice(minCoord[0], maxCoord[0]),
slice(minCoord[1], maxCoord[1]),
slice(minCoord[2], maxCoord[2]),
)
displacements = numpy.zeros((points.shape[0], 3), dtype="<f8")
print(f"[globalCorrelation] Points: {points.shape}")
print(f"[globalCorrelation] Displacements: {displacements.shape}")
print(f"[globalCorrelation] Cells: {connectivity.shape}")
print(f"[globalCorrelation] Padding: {meshPaddingSlice}")
###############################################################
# Step 2-1 Apply deformation and interpolate pixels
###############################################################
print("[globalCorrelation] Allocating 3D data (deformed image)")
if initialDisplacements is None:
im1Def = im1.copy()
imTetLabel = spam.label.labelTetrahedra(im1.shape, points, connectivity, nThreads=nThreads)
else:
print("[globalCorrelation] Applying initial deformation to image")
displacements = initialDisplacements.copy()
tic = time.perf_counter()
imTetLabel = spam.label.labelTetrahedra(im1.shape, points + displacements, connectivity, nThreads=nThreads)
print(f"[globalCorrelation] Running labelTetrahedra: {time.perf_counter()-tic:.3f} seconds.")
im1Def = spam.DIC.applyMeshTransformation(
im1,
points,
connectivity,
displacements,
imTetLabel=imTetLabel,
nThreads=nThreads,
)
if debugFiles:
print("[globalCorrelation] Saving initial images")
for name, image in [
[f"{prefix}-def-init.tif", im1Def],
[f"{prefix}-imTetLabel-init.tif", imTetLabel],
]:
print(f"[globalCorrelation]\t{name}: {image.shape}")
tifffile.imwrite(name, image)
# print("[globalCorrelation] Correlating (MF)!")
print("[globalCorrelation] Calculating gradient of IM TWO...")
im2Grad = numpy.array(numpy.gradient(im2), dtype="<f4")
print("[globalCorrelation] Computing global matrix")
# This generates the globalMatrix (big Mc matrix) with imGrad as input
Mc = numpy.zeros((3 * points.shape[0], 3 * points.shape[0]), dtype="<f8")
if debugFiles:
print("[globalCorrelation] Computing debug files fields")
gradientPerTet = numpy.zeros((connectivity.shape[0], 3), dtype="<f8")
IDPerTet = numpy.array([_ for _ in range(connectivity.shape[0])])
computeGradientPerTet(
imTetLabel.astype("<u4"),
im2Grad.astype("<f4"),
connectivity.astype("<u4"),
(points + displacements).astype("<f8"),
gradientPerTet,
)
spam.helpers.writeUnstructuredVTK(
(points + displacements),
connectivity,
cellData={"meanGradient": gradientPerTet, "id": IDPerTet},
fileName=f"{prefix}-gradient.vtk",
)
del gradientPerTet
computeDICglobalMatrix(
imTetLabel.astype("<u4"),
im2Grad.astype("<f4"),
connectivity.astype("<u4"),
(points + displacements).astype("<f8"),
Mc,
)
###############################################################
# Setup left hand vector
###############################################################
if regularisationMatrix:
regularisationField = isochoricField(points) if regularisationField is None else regularisationField
Ec = _computeFunctional(regularisationField, Mc)
regularisationMatrix *= Ec
print(f"[regularisation] Wc/Ec = {1/Ec:.3e}")
left_hand_inverse = numpy.linalg.inv(Mc + regularisationMatrix)
else:
print("[globalCorrelation] Skip regularisation")
left_hand_inverse = numpy.linalg.inv(Mc)
del Mc
# error = _errorCalc(im2, im1Def, im1, meshPaddingSlice)
# print("\[globalCorrelation] Initial Error (abs) = ", error)
# We try to solve Md=F
# while error > 0.1 and error < errorIn:
# while error > 0.1 and i <= maxIterations and error < errorIn:
dxNorm = numpy.inf
i = 0
while dxNorm > convergenceCriterion and i < maxIterations:
i += 1
# This function returns globalVector (F) taking in im1Def and im2 and the gradients
tic = time.perf_counter()
# print("[globalCorrelation] [newton] run computeDICglobalVector: ", end="")
right_hand_vector = numpy.zeros((3 * points.shape[0]), dtype="<f8")
computeDICglobalVector(
imTetLabel.astype("<u4"),
im2Grad.astype("<f4"),
im1Def.astype("<f4"),
im2.astype("<f4"),
connectivity.astype("<u4"),
(points + displacements).astype("<f8"),
right_hand_vector,
)
# print(f"{time.perf_counter()-tic:.3f} seconds.")
tic = time.perf_counter()
# print("[globalCorrelation] [newton] run solve: ", end="")
# solve: we can use solve here for sake of precision (over computing
# M^-1). However solve takes quite a lot of time for "small" meshes).
if regularisationMatrix:
right_hand_vector -= numpy.matmul(regularisationMatrix, displacements.ravel())
dx = numpy.matmul(left_hand_inverse, right_hand_vector).astype("<f8")
# dx_solve = numpy.linalg.solve(
# Mc,
# right_hand_vector
# ).astype('<f8')
# print(numpy.linalg.norm(dx - dx_solve))
displacements += dx.reshape(points.shape[0], 3)
dxNorm = numpy.linalg.norm(dx)
# print(f"{time.perf_counter()-tic:.3f} seconds.")
if medianFilterEachIteration:
# use connectivity to filter
print("[globalCorrelation] [newton] Median filter of displacements...")
for nodeNumber in range(points.shape[0]):
# get rows of connectivity (i.e., tets) which include this point
connectedTets = numpy.where(connectivity == nodeNumber)[0]
neighbourPoints = numpy.unique(connectivity[connectedTets])
diff = numpy.median(displacements[neighbourPoints], axis=0) - displacements[nodeNumber]
displacements[nodeNumber] += 0.5 * diff
tic = time.perf_counter()
# print("[globalCorrelation] [newton] run labelTetrahedra: ", end="")
imTetLabel = spam.label.labelTetrahedra(im1.shape, points + displacements, connectivity, nThreads=nThreads)
# print(f"{time.perf_counter()-tic:.3f} seconds.")
tic = time.perf_counter()
# print("[globalCorrelation] [newton] run applyMeshTransformation: ", end="")
im1Def = spam.DIC.applyMeshTransformation(
im1,
points,
connectivity,
displacements,
imTetLabel=imTetLabel,
nThreads=nThreads,
)
# print(f"{time.perf_counter()-tic:.3f} seconds.")
if debugFiles:
tifffile.imwrite(f"{prefix}-def-i{i:03d}.tif", im1Def)
tifffile.imwrite(
f"{prefix}-residual-cropped-i{i:03d}.tif",
im1Def[meshPaddingSlice] - im2[meshPaddingSlice],
)
# tifffile.imwrite(f"{prefix}-imTetLabel-i{i:03d}.tif", imTetLabel)
pointData = {
"displacements": displacements,
"initialDisplacements": initialDisplacements,
"fluctuations": numpy.subtract(displacements, initialDisplacements),
}
# compute strain for each fields
cellData = {}
components = ["vol", "dev", "volss", "devss"]
for fieldName, field in pointData.items():
Ffield = spam.deformation.FfieldBagi(points, connectivity, field, verbose=False)
decomposedFfield = spam.deformation.decomposeFfield(Ffield, components)
for c in components:
cellData[f"{fieldName}-{c}"] = decomposedFfield[c]
spam.helpers.writeUnstructuredVTK(
points.copy(),
connectivity.copy(),
pointData=pointData,
cellData=cellData,
fileName=f"{prefix}-displacementFE-i{i:03d}.vtk",
)
# print("\t\[globalCorrelation] Error Out = %0.5f%%" % (error))
# reshapedDispl = displacements.reshape(points.shape[0], 3)
dMin = numpy.min(displacements, axis=0)
dMed = numpy.median(displacements, axis=0)
dMax = numpy.max(displacements, axis=0)
strMin = f"Min={dMin[0]: .3f} {dMin[1]: .3f} {dMin[2]: .3f}"
strMed = f"Med={dMed[0]: .3f} {dMed[1]: .3f} {dMed[2]: .3f}"
strMax = f"Max={dMax[0]: .3f} {dMax[1]: .3f} {dMax[2]: .3f}"
print(f"[globalCorrelation] [newton] i={i:03d}, displacements {strMin}, {strMed}, {strMax}, dx={dxNorm:.2f}")
return displacements
if __name__ == "__main__":
pass
# create mesh
print("Create mesh")
# points, connectivity = spam.mesh.createCuboid([32.55, 72.1, 15.7], 20, origin=[-4.2, 12.5, 78.01])
points, connectivity = spam.mesh.createCylinder([-24, 27], 12.5, 97.3, 10, zOrigin=-4.2)
configuration = {
# Information about the bulk regularisation
"young": 10, # mandatory (Young modulus)
"poisson": 0.25, # mandatory (Poisson ratio)
# "xi": 30, # optional
# "periods": 3, # optionnal (whatever...)
# Information about the surface regularisation
# (Dirichlet boundary conditions)
# Each surface of the cuboid is labelled by the keywords
# z_start: z == 0, z_end, y_start, y_end, x_start and x_end)
# If a keyword is ommited the surface is not regularised.
"dirichlet": {
"z_start": {"xi": 30, "dof": [0, 1, 2]}, # optional # mandatory
"z_end": {"xi": 30, "dof": [0]}, # xi normalisation is 30
"z_lateral": {"xi": 30, "dof": [1, 2]}, # xi normalisation is 30
},
}
p = regularisationParameters(configuration)
# STEP 1: get labels
labels = surfaceLabels(points, surfaces=p["surfaces"])
# STEP 2: build regularisation matrix
Mreg, v = regularisationMatrix(points, connectivity, p["young"], p["poisson"], xiBulk=p["xi"], dirichlet=p["dirichlet"], labels=labels, periods=p["periods"])