Coverage for /usr/local/lib/python3.10/site-packages/spam/DIC/globalDVC.py: 40%
306 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-24 17:26 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-24 17:26 +0000
1# Library of SPAM image correlation functions.
2# Copyright (C) 2020 SPAM Contributors
3#
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU General Public License as published by the Free
6# Software Foundation, either version 3 of the License, or (at your option)
7# any later version.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12# more details.
13#
14# You should have received a copy of the GNU General Public License along with
15# this program. If not, see <http://www.gnu.org/licenses/>.
17"""
18Set of functions for performing mechanically regularised global DVC.
19The regularisation implementation is taken from [Mendoza2019]_.
21Here is how it can be used:
23.. code-block:: python
25 import spam.mesh
26 import spam.DIC
28 # Mesh
29 points, connectivity = spam.mesh.createCylinder([-24, 27], 12.5, 97.3, 10, zOrigin=-4.2)
31 # Regularisation: parameters
32 myParameters = {
33 "young": 10,
34 "poisson": 0.25,
35 "dirichlet": {
36 "z_start": {"dof": [0]},
37 "z_end": {"dof": [0]},
38 },
39 }
40 p = spam.DIC.regularisationParameters(myParameters)
42 # Regularisation step 1: get labels
43 labels = spam.DIC.surfaceLabels(points, surfaces=p["surfaces"])
45 # Regularisation step 2: build regularisation matrix
46 regularisationMatrix, regularisationField = spam.DIC.regularisationMatrix(
47 points,
48 connectivity,
49 young=p["young"],
50 poisson=p["poisson"],
51 xiBulk=p["xi"],
52 dirichlet=p["dirichlet"],
53 periods=p["periods"],
54 labels=labels,
55 )
57 # Global DVC
58 globalCorrelation(
59 im1,
60 im2,
61 points,
62 connectivity,
63 regularisationMatrix=regularisationMatrix,
64 regularisationField=regularisationField,
65 )
67.. [Mendoza2019] A. Mendoza, J. Neggers, F. Hild, S Roux (2019). Complete mechanical regularization applied to digital image and volume correlation.
68 *Computer Methods in Applied Mechanics and Engineering*, Volume 355, 1 October 2019, Pages 27-43
69 https://doi.org/10.1016/j.cma.2019.06.005
71.. _RegularisationParameter: _static/gdvc-regularisation-parameters.yaml
73 Note
74 ----
75 Make a link to the script
77"""
79import time # for debug
81import numpy
82import spam.DIC
83import spam.label
84import spam.mesh
85import tifffile
87# 2017-05-29 ER and EA
88# This is spam's C++ DIC toolkit, but since we're in the tools/ directory we can import it directly
89from spambind.DIC.DICToolkit import (
90 computeDICglobalMatrix,
91 computeDICglobalVector,
92 computeGradientPerTet,
93)
96def _check_symmetric(a, rtol=1e-05, atol=1e-08):
97 """Helper function to check if a matrix is symmetric."""
98 return numpy.allclose(a, a.T, rtol=rtol, atol=atol)
101def _errorCalc(im1, im2, im2ref, meshPaddingSlice):
102 """Helper function to compute the error between two images."""
103 errorInitial = numpy.sqrt(numpy.square(im2ref[meshPaddingSlice] - im1[meshPaddingSlice]).sum())
104 errorCurrent = numpy.sqrt(numpy.square(im2[meshPaddingSlice] - im1[meshPaddingSlice]).sum())
105 return errorCurrent / errorInitial
108def _computeFunctional(u, K, L=None):
109 """Helper function to compute global DVC functional"""
110 u = numpy.ravel(u)
111 if L is None:
112 return numpy.matmul(u.T, numpy.matmul(K.T, numpy.matmul(K, u)))
113 else:
114 return numpy.matmul(u.T, numpy.matmul(K.T, numpy.matmul(L, numpy.matmul(K, u))))
117def _normalisedEnergies(v, Km, Ks, Ls):
118 """Helper function to compute globale DVC normalised energies"""
119 Em = _computeFunctional(v, Km)
120 Es = [_computeFunctional(v, K, L=L) for K, L in zip(Ks, Ls)]
121 return Em, Es
124def _computeWeights(kMag, xiBulk, xiDirichlet):
125 """Helper function to compute global DVC weights"""
126 print(f"[regularisation] xi bulk = {xiBulk:.2f}")
127 Wm = (xiBulk * kMag) ** 4
128 Ws = []
129 for i, xi in enumerate(xiDirichlet):
130 print(f"[regularisation] xi dirichlet {i} = {xi:.2f}")
131 Ws.append((xi * kMag) ** 4)
133 return Wm, Ws
136def surfaceLabels(points, surfaces=[], connectivity=None):
137 """Creates a label for each points based on a list of keywords that corresponds to surfaces (`ie.` ``["z_start", "z_end"]``).
138 The label value is based on the position of the surface in the list.
140 Parameters
141 ----------
142 points: Nx3 array
143 List of coordinates of the mesh nodes.
145 surfaces: list of string
146 A list of keywords corresponding to surfaces.
148 - ``z_start``: plane at ``z == min(points[:,0])``
149 - ``z_end``: plane at ``z == max(points[:,0])``
150 - ``y_start``: plane at ``y == min(points[:,1])``
151 - ``y_end``: plane at ``y == max(points[:,1])``
152 - ``x_start``: plane at ``x == min(points[:,2])``
153 - ``x_end``: plane at ``x == max(points[:,2])``
154 - ``z_lateral``: lateral surface of a cylinder oriented in the first direction.
155 - ``y_lateral``: lateral surface of a cylinder oriented in the second direction.
156 - ``x_lateral``: lateral surface of a cylinder oriented in the third direction.
158 connectivity: array (only for debug purposes)
159 Connectivity matrix. If set, creates a VTK file with labels.
161 Returns
162 -------
163 N x 1 array:
164 Surface label for each points.
166 Note
167 ----
168 - Surface labels start at 1, 0 corresponding to bulk or not specified surfaces.
169 - Points belong to a single surface. The first surface in `surfaces` prevails.
171 Example
172 -------
173 >>> import spam.mesh
174 >>> import spam.DIC
175 >>>
176 >>> # create mesh
177 >>> points, connectivity = spam.mesh.createCylinder([-24, 27], 12.5, 97.3, 10, zOrigin=-4.2)
178 >>> # compute labels for bottom and top surface only
179 >>> labels = spam.DIC.surfaceLabels(points, surfaces=["z_start", "z_end"], connectivity=connectivity)
181 """
183 def _belongs_to_lateral_surface(point, centre, radius, epsilon=1e-6):
184 """Returns True if point belongs to the lateral surface of a cylinder"""
185 d = ((centre[0] - point[1]) ** 2 + (centre[1] - point[2]) ** 2) ** 0.5
186 return abs(d - radius) < epsilon
188 def _belongs_to_plane(point, direction, coordinate, epsilon=1e-6):
189 """Returns True if point belongs to a surface of position `coordinate` in direction `direction`"""
190 return abs(point[direction] - coordinate) < epsilon
192 # Get geometrical data from the points coordinates
193 maxCoord = numpy.max(points, axis=0)
194 minCoord = numpy.min(points, axis=0)
195 centre = [0.0] * 3
196 radii = [0.0] * 3
197 for dz in range(3):
198 dy = (dz + 1) % 3
199 dx = (dz + 2) % 3
200 centre[dz] = [0.5 * (minCoord[d] + maxCoord[d]) for d in [dy, dx]]
201 radii[dz] = 0.25 * (maxCoord[dx] + maxCoord[dy] - minCoord[dx] - minCoord[dy])
203 labels = numpy.zeros(points.shape[0], dtype=int)
205 # Loop over the points
206 for A, point in enumerate(points):
208 # Loop over the surfaces to enforce order for edges and vertices
209 for i, surface in enumerate(surfaces):
211 direction, position = surface.split("_")
212 direction = {"z": 0, "y": 1, "x": 2}[direction] # direction as integer z: 0, y: 1, x: 2
214 if position == "start" and _belongs_to_plane(point, direction, minCoord[direction]):
215 labels[A] = i + 1
216 break
218 elif position == "end" and _belongs_to_plane(point, direction, maxCoord[direction]):
219 labels[A] = i + 1
220 break
222 elif position == "lateral" and _belongs_to_lateral_surface(point, centre[direction], radii[direction]):
223 labels[A] = i + 1
224 break
226 if connectivity is not None: # debug
227 spam.helpers.writeUnstructuredVTK(points, connectivity, pointData={"label": labels}, fileName="gdvc-labels.vtk")
229 return labels
232def projectionMatrices(points, connectivity, labels, dirichlet=[]):
233 """Compute binary diagonal matrices and Laplace-Beltrami operator.
234 Details on the meaning of these matrices can be found in [Mendoza2019]_ `eq. (12) to (14)` and `eq. (17) to (19)`.
237 Parameters
238 ----------
239 points: Nx3 array
240 List of coordinates of the mesh nodes.
242 connectivity: Mx4 numpay array
243 Connectivity matrice of the mesh (tetrahedra).
245 labels: Nx1 array
246 Surface labels for each points as defined in ``spam.DIC.surfaceLabels()``
248 dirichlet: list of (int, list, )
249 Each element of the list defines a surface with Dirichlet boundary conditions by a tuple.
251 - The first element of the tuple is the surface label which should belong to `labels`.
252 - The second element of the tuple is a list of degrees of freedom directions to consider for the Dirichlet boundary conditions.
253 - Extra elements in the tuple are ignored.
254 .. code-block:: python
256 dirichlet = [
257 # surface 1, dof in x, y
258 (
259 1,
260 [1, 2],
261 ),
262 # surface 3, dof in z
263 (
264 3,
265 [0],
266 ),
267 ]
269 Returns
270 -------
271 3Nx3N array:
272 :math:`D_m` The binary diagonal matrix corresponding to all dofs of the bulk and neumann surfaces nodes (ie the non dirichlet)
273 List of 3Nx3N array:
274 :math:`D_{S_i}` Binary diagonal matrices corresponding to the dofs of each Dirichlet surfaces
275 List of 3Nx3N array:
276 :math:`L_{S_i}` List of Laplace-Beltrami operators corresponding to the same Dirichlet surfaces.
278 """
279 if dirichlet is None:
280 dirichlet = []
282 # all matrices out here are of shape ndof x ndof (same as stiffness matrix)
283 ndof = 3 * points.shape[0]
285 print(f"[projectionMatrices] number of dirichlet surfaces: {len(dirichlet)} ({dirichlet})")
287 # initialise list of A and B matrices in order to compute the L operator (size ndof x ndof)
288 matricesA = [numpy.zeros((ndof, ndof)) for _ in dirichlet]
289 matricesB = [numpy.zeros((ndof, ndof)) for _ in dirichlet]
290 matricesD = [numpy.zeros((ndof, ndof)) for _ in dirichlet]
292 # list of all nodes and dof numbers on dirichlet surfaces
293 # NOTE: taking label == 0 (ie background) leads to singlular sub_A matrix
294 dirichletSurfaces = [(numpy.where(labels == d[0])[0], d[1]) for d in dirichlet if d[0]]
296 DEBUG_dirichlet_connectivity_all = []
297 DEBUG_dirichlet_points_all = []
298 for si, _ in enumerate(dirichlet):
299 DEBUG_dirichlet_connectivity_all.append([])
300 DEBUG_dirichlet_points_all.append([])
302 # # loop over dirichlet matrix
303 for si, (surfacePoints, dofDirections) in enumerate(dirichletSurfaces):
304 dofs = [3 * A + d for A in surfacePoints for d in dofDirections]
306 print(f"[projectionMatrices] Dirichlet #{si} label {dirichlet[si][0]}) #points = {len(surfacePoints)} dofs directions = {dofDirections} {len(dofs)} dofs")
308 surfacePoints = set(surfacePoints)
309 for tetra_connectivity in connectivity:
310 tri_con = list(surfacePoints.intersection(tetra_connectivity))
311 tri_nodes = [list(points[i]) for i in tri_con]
312 if len(tri_con) != 3:
313 # the element doesn't have a triangle a this surface
314 continue
316 # get 4th point of the tetrahedron to compute 3D shape functions
317 point_4_n = list(set(tri_con).symmetric_difference(set(tetra_connectivity)))[0]
318 _, tri_coefficients = spam.mesh.shapeFunctions(tri_nodes + [list(points[point_4_n])])
319 # we've got a triangle on the surface!!!
320 DEBUG_dirichlet_connectivity_all[si].append(list(tri_con))
321 DEBUG_dirichlet_points_all[si].append(tri_nodes)
323 # assemble the dirichlet connectivity matrix
324 a = numpy.array(tri_coefficients[:3, 0])
325 bcd = numpy.array(tri_coefficients[:3, 1:])
326 B = numpy.array(tri_nodes).T
328 def phi(i, L):
329 return a[i] + numpy.matmul(bcd[i], numpy.matmul(B, L.T))
331 def dphi(i):
332 return bcd[i]
334 # gauss points
335 L_gp = numpy.array([[0.5, 0.5, 0.0], [0.5, 0.0, 0.5], [0.0, 0.5, 0.5]])
337 # STEP 3: compute the area
338 area = 0.5 * numpy.linalg.norm(
339 numpy.cross(
340 numpy.subtract(tri_nodes[1], tri_nodes[0]),
341 numpy.subtract(tri_nodes[2], tri_nodes[0]),
342 )
343 )
345 # STEP 3: compute inner products of the shape functions
346 for i in range(3):
347 for j in range(3):
348 inner = 0.0
349 for L in L_gp:
350 # print(inner, i, j, L, phi(i, L), phi(j, L))
351 inner += phi(i, L) * phi(j, L)
352 inner *= area / 3.0 # the 1/3. comes from the weight
353 dinner = area * numpy.inner(dphi(i), dphi(j))
354 for d in dofDirections:
355 P = int(3 * tri_con[i] + d)
356 Q = int(3 * tri_con[j] + d)
357 matricesA[si][P, Q] += inner
358 matricesB[si][P, Q] += dinner
360 # # invert matrix A
361 # for si, (surfacePoints, dofDirections) in enumerate(dirichletSurfaces):
362 dofs = [3 * A + d for A in surfacePoints for d in dofDirections]
363 # 1. extract submatrix A from full size matricesA and invert
364 sub_A = matricesA[si][:, dofs]
365 sub_A = sub_A[dofs, :]
367 sub_A = numpy.linalg.inv(sub_A)
368 # 2. push back inverted submatrix into full size matricesA
369 for i, P in enumerate(dofs):
370 matricesD[si][P, P] = 1
371 for j, Q in enumerate(dofs):
372 matricesA[si][P, Q] = sub_A[i, j]
374 # return also bulk + neumann binary diagonal projection matrices
375 # Dm = Db + Dn = I - Sum(Dd)
376 Dm = numpy.eye(ndof)
377 for Ds in matricesD:
378 Dm -= Ds
380 # return a list of D and L = AxB for each dirichlet surface
381 return Dm, matricesD, [numpy.matmul(A, B) for A, B in zip(matricesA, matricesB)]
384def isochoricField(points, periods=3, connectivity=None):
385 r"""Helper function to build the isochoric test function used to normalised the mechanical regularisation functionals.
386 The function is a shear displacement field with its wave vector in the direction of the longest dimension of the mesh.
388 .. math::
390 \textbf{v}(\textbf{x}) = \sin(2 \pi \textbf{k}\cdot \textbf{x} )
392 [Mendoza2019]_ `eq. (25)`.
394 Parameters
395 ----------
396 points: Nx3 array
397 List of coordinates of the mesh nodes.
399 periods: int
400 The number of periods of the shear wave.
402 connectivity: array (only for debug purposes)
403 Connectivity matrix. If set, creates a VTK file with the field.
405 Returns
406 -------
407 float:
408 The magnitude of the wave vector :math:`\bf{k}`.
409 Nx3 array:
410 The field :math:`\bf{v}` at each nodes.
411 """
413 sizes = [ma - mi for ma, mi in zip(numpy.max(points, axis=0), numpy.min(points, axis=0))]
414 d = numpy.argmax(sizes) # direction of largest size
415 size = float(max(sizes)) # largest size
416 mag = periods / size
417 v = numpy.zeros_like(points)
418 for i, point in enumerate(points):
419 kdotx = mag * point[d]
420 v[i][(d + 0) % 3] = 0
421 v[i][(d + 1) % 3] = numpy.sqrt(0.5) * numpy.sin(2 * numpy.pi * kdotx)
422 v[i][(d + 2) % 3] = numpy.sqrt(0.5) * numpy.sin(2 * numpy.pi * kdotx)
424 if connectivity is not None:
425 print("[isochoricField] plot vtk")
426 spam.helpers.writeUnstructuredVTK(
427 points,
428 connectivity,
429 elementType="tetra",
430 pointData={"v": v},
431 cellData={},
432 fileName="gdvc-v.vtk",
433 )
435 return mag, v
438def regularisationMatrix(points, connectivity, young=1, poisson=0.25, voxelSize=1, xiBulk=None, dirichlet=[], labels=[], periods=3):
439 r"""Computes the mechanical regularisation matrix :math:`M_{reg}` for the global DVC:
440 $$(M_{reg} + M_{c}) \\delta\\textbf{u} = \\textbf{b} - M_{reg} \\textbf{u} $$
441 where
442 $$M_{reg} = M_m + \\sum_{i} M_{S_i}$$
443 corresponds to both bulk/Neuman and Dirichlet surfaces contributions ([Mendoza2019]_ `eq. (29) and (30)`).
445 Parameters
446 ----------
447 points: Nx3 array
448 List of coordinates of the mesh nodes.
450 connectivity: Mx4 array
451 Connectivity matrix of the mesh.
453 young: float (optional)
454 The Young modulus used to build the stiffness matrix :math:`K` from which :math:`M_m` and :math:`M_{S_i}` are derived.
455 Default = 1 (it is useless to change this value if you don't impose forces with meaningful units on the Neumann surfaces)
457 poisson: float (optional)
458 The Poisson ratio used to build the stiffness matrix :math:`K` from which :math:`M_m` and :math:`M_{S_i}` are derived.
459 Default = 0.25
461 voxelSize: float (optional)
462 The size of a voxel in the same unit used to set the Young modulus assuming that the mesh geometry is in voxels.
463 This voxel size :math:`v_s` is used to convert the Young modulus in Newton per voxels squared.
464 Default = 1 (it is useless to change this value if you don't impose forces with meaningful units on the Neumann surfaces).
466 xiBulk: float (optional)
467 The regularisation length :math:`\xi_m` of the bulk/Neumann contribution :math:`M_{reg}`.
468 It has to be compared to the characteristic length of the mesh.
469 The bigger the regularisation length the more important the regularisation contribution.
470 If None it is taken to be equal to the mesh characteristic length.
471 Default = None
473 dirichlet: list of (int, list, float) (optional)
474 Each element of the list defines a surface with Dirichlet boundary conditions by a tuple.
476 - The first element of the tuple is the surface label which should belong to `labels`.
477 - The second element of the tuple is a list of degrees of freedom directions to consider for the Dirichlet boundary conditions.
478 - The third element of the tuple correspond to the regularisation length of the surface :math:`\xi_{S_i}`
480 .. code-block:: python
482 dirichlet = [
483 # surface 1, dof in x, y, xi = 0.1
484 [1, [1, 2], 0.1],
485 # surface 3, dof in z, xi not set
486 [3, [0], None],
487 ]
489 labels: Nx1 array (optional)
490 Surface labels for each points as defined in ``spam.DIC.surfaceLabels()``. Mandatory only if ``dirichlet`` is set.
492 periods: float (optional)
493 Number of periods of the isochoric function :math:`v` used to compute the normalized energy ([Mendoza2019]_ `eq. (27)`).
494 :math:`v` is computed with ``spam.DIC.isochoricField()`` with a default number of periods of 3.
496 Returns
497 -------
498 3Nx3N array:
499 :math:`M_{req}` the regularisation matrix.
500 Nx3 array:
501 :math:`v` the isochoric field at each nodes of the mesh.
503 Note
504 ----
505 - The ``dirichlet`` argument is compatible with the one in ``spam.DIC.projectionMatrices()``
506 - 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.
507 - Imposing forces is not implemented yet :)
509 Warning
510 -------
511 This function is not tested yet
512 """
514 def _imshow(matlist, title):
515 from matplotlib import pyplot as plt
517 if not isinstance(matlist, list):
518 matlist = [matlist]
520 for i, mat in enumerate(matlist):
521 plt.imshow(mat)
522 plt.title(f"{title} {i}")
523 # plt.show()
525 print(f"[regularisation] Young modulus using si: {young}")
526 young *= voxelSize**2
527 print(f"[regularisation] Young modulus using voxels: {young}")
529 print("[regularisation] Build projection matrices")
530 Dm, Ds, Ls = projectionMatrices(points, connectivity, labels, dirichlet=dirichlet)
531 _imshow(Dm, "Dm")
532 _imshow(Ds, "Ds")
533 _imshow(Ls, "Ls")
535 print("[regularisation] Build global stiffness matrix")
536 K = spam.mesh.globalStiffnessMatrix(points, connectivity, young, poisson)
537 print("[regularisation] Build bulk stiffness matrix")
538 Km = numpy.matmul(Dm, K)
539 print("[regularisation] Build dirichlet stiffness matrices")
540 Ks = [numpy.matmul(Ds_i, K) for Ds_i in Ds]
541 _imshow(K, "K")
542 _imshow(Km, "Km")
543 _imshow(Ks, "Ks")
544 del K
546 print("[regularisation] Build isochoric function")
547 kMag, v = isochoricField(points, periods=periods)
549 print("[regularisation] Compute normalised energy and weight")
550 Em, Es = _normalisedEnergies(v, Km, Ks, Ls)
551 print(f'[regularisation] Em = {Em:.3e}, Es = {" ".join([f"{_:.3e}" for _ in Es])}')
553 lc = spam.mesh.getMeshCharacteristicLength(points, connectivity)
554 print(f"[regularisation] mesh characteristic length lc = {lc}")
556 xiBulk = lc if xiBulk is None else xiBulk
557 xiDirichlet = [lc if _[2] is None else _[2] for _ in dirichlet]
558 Wm, Ws = _computeWeights(kMag, xiBulk, xiDirichlet)
559 print(f'[regularisation] Wm = {Wm:.2f}, Ws = {" ".join([f"{_:.2f}" for _ in Ws])}')
561 print(f'[regularisation] Wm/Em = {Wm/Em:.3e} Ws/Es = {" ".join([f"{a/b:.3e}" for a, b in zip(Ws, Es)])}')
563 # 5.4 compute Mreg
564 Mreg = numpy.zeros_like(Km)
565 Mreg = Wm * numpy.matmul(Km.T, Km) / Em
566 for W, E, K, L in [(W, E, K, L) for W, E, K, L in zip(Ws, Es, Ks, Ls) if E]:
567 Mreg += W * numpy.matmul(K.T, numpy.matmul(L, K)) / E
569 _imshow(Mreg, "Mreg")
570 return Mreg, v
573def regularisationParameters(userParameters):
574 """
575 Convert a user friendly dictionary of parameters into a dictionary of variables compatible with the regularisation functions of this module.
577 Parameters
578 ----------
579 userParameters: (str | dict)
580 The user parameters for the mechanical regularisation scheme. It can be:
582 - if ``str``: the path to a YAML file. A dummy example can be downloaded here: `RegularisationParameter`_
583 - if ``dict``: a dictionary containing the following keys and values:
585 .. code-block:: python
587 {
588 # bulk / Neumann regularisation
589 "young": 10, # mandatory (the Young modulus)
590 "poisson": 0.25, # mandatory (the Poisson ratio)
591 "xiBulk": 30, # optional (the bulk/Neumann regularisation lentgh)
592 "periods": 3, # the number of periods of the isochoric function (optional)
593 "voxelSize": 0.01, # the voxel size (optional)
594 # Information about the Dirichlet surface regularisation
595 # Each surface is defined by a search keyword among
596 # z_start, z_end, y_start, y_end, x_start and x_end for plane lookups
597 # z_lateral, y_lateral, x_lateral for cylinder lateral surface lookups
598 "dirichlet": {
599 "z_start": { # surface label 1
600 "xi": 5, # the regularisation length (optional)
601 "dof": [0, 1, 2], # mandatory
602 },
603 "z_end": {"dof": [0]}, # surface label 2 # mandatory
604 },
605 }
607 Returns
608 -------
609 dict:
610 A dictionary with the variables needed for the regularisation functions.
611 The dictionary keys are named to match the function's signatures:
613 - ``surface``: the dirichlet surfaces for ``spam.DIC.surfaceLabels()``
614 - ``dirichlet``: the dirichlet surfaces for ``spam.DIC.regularisationMatrix()``
615 - ``young``: the Young modulus for ``spam.DIC.regularisationMatrix()``
616 - ``poisson``: the Poisson ratio for ``spam.DIC.regularisationMatrix()``
617 - ``xiBulk``: the bulk characteristic lentgh for ``spam.DIC.regularisationMatrix()``
618 - ``periods``: the Poisson ratio for ``spam.DIC.regularisationMatrix()``
619 - ``voxelSize``: the Poisson ratio for ``spam.DIC.regularisationMatrix()``
620 """
622 surfaces = [k for k in userParameters.get("dirichlet", {})]
623 dirichlet = [(i + 1, surf["dof"], surf.get("xi")) for i, surf in enumerate(userParameters.get("dirichlet", {}).values())]
624 young = userParameters["young"]
625 poisson = userParameters["poisson"]
626 xiBulk = userParameters.get("xiBulk")
627 periods = userParameters.get("periods", 3)
628 voxelSize = userParameters.get("voxelSize", 1.0)
630 parameters = {"surfaces": surfaces, "young": young, "poisson": poisson, "xiBulk": xiBulk, "dirichlet": dirichlet, "periods": periods, "voxelSize": voxelSize}
632 for k, v in parameters.items():
633 print(f"[regularisation parameters] {k:<10}: {v}")
634 return parameters
637def globalCorrelation(
638 im1,
639 im2,
640 points,
641 connectivity,
642 regularisationMatrix=None,
643 regularisationField=None,
644 initialDisplacements=None,
645 convergenceCriterion=0.01,
646 maxIterations=20,
647 medianFilterEachIteration=False,
648 debugFiles=False,
649 prefix="globalCorrelation",
650 nThreads=None,
651):
652 """
653 Global DVC (works only with 3D images).
655 Parameters
656 ----------
657 im1 : 3D array
658 Reference image in which the mesh is defined
660 im2 : 3D array
661 Deformed image, should be same size as im1
663 points : M x 3 array
664 M nodal coordinates in reference configuration
666 connectivity : N x 4 array
667 connectivityhedral connectivity generated by spam.mesh.triangulate() for example
669 regularisationMatrix : 3N x 3N array (optional)
670 Mechanical regularisation stiffness matrix. If None no mechanical regularisation is applied.
671 First output of `spam.DIC.regularisationMatrix`
674 regularisationField : N x 3 array (optional)
675 Isochoric displacement field used to compute the normalisation energies.
676 Second output of `spam.DIC.regularisationMatrix`
678 initialDisplacements : M x 3 array of floats (optional)
679 Initial guess for nodal displacements, must be coherent with input mesh
680 Default = None
682 convergenceCriterion : float
683 Convergence criterion for change in displacements in px
684 Default = 0.01
686 maxIterations : int
687 Number of iterations to stop after if convergence has not been reached
688 Default = 20
690 debugFiles : bool
691 Write temporary results to file for debugging?
692 Default = 'globalCorrelation'
694 prefix : string
695 Output file prefix for debugFiles
696 Default = None
698 Returns
699 -------
700 displacements : N x 3 array of floats
701 (converged?) Nodal displacements
703 Example
704 -------
705 >>> import spam.DIC
706 >>> spam.DIC.globalCorrelation(
707 imRef,
708 imDef
709 )
710 """
711 import multiprocessing
713 try:
714 multiprocessing.set_start_method("fork")
715 except RuntimeError:
716 pass
718 import spam.helpers
719 import spam.mesh
721 # Global number of processes
722 nThreads = multiprocessing.cpu_count() if nThreads is None else nThreads
723 print(f"[globalCorrelation] C++ parallelisation on {nThreads} threads")
725 print(f"[globalCorrelation] Convergence criterion = {convergenceCriterion}")
726 print(f"[globalCorrelation] Max iterations = {maxIterations}")
727 print("[globalCorrelation] Converting im1 to 32-bit float")
728 im1 = im1.astype("<f4")
730 points = points.astype("<f8")
731 connectivity = connectivity.astype("<u4")
733 maxCoord = numpy.amax(points, axis=0).astype("<u2")
734 minCoord = numpy.amin(points, axis=0).astype("<u2")
735 print(f"[globalCorrelation] Mesh box: min = {minCoord} max = {maxCoord}")
737 meshPaddingSlice = (
738 slice(minCoord[0], maxCoord[0]),
739 slice(minCoord[1], maxCoord[1]),
740 slice(minCoord[2], maxCoord[2]),
741 )
743 displacements = numpy.zeros((points.shape[0], 3), dtype="<f8")
745 print(f"[globalCorrelation] Points: {points.shape}")
746 print(f"[globalCorrelation] Displacements: {displacements.shape}")
747 print(f"[globalCorrelation] Cells: {connectivity.shape}")
748 print(f"[globalCorrelation] Padding: {meshPaddingSlice}")
750 ###############################################################
751 # Step 2-1 Apply deformation and interpolate pixels
752 ###############################################################
754 print("[globalCorrelation] Allocating 3D data (deformed image)")
755 if initialDisplacements is None:
756 im1Def = im1.copy()
757 imTetLabel = spam.label.labelTetrahedra(im1.shape, points, connectivity, nThreads=nThreads)
758 else:
759 print("[globalCorrelation] Applying initial deformation to image")
760 displacements = initialDisplacements.copy()
761 tic = time.perf_counter()
762 imTetLabel = spam.label.labelTetrahedra(im1.shape, points + displacements, connectivity, nThreads=nThreads)
763 print(f"[globalCorrelation] Running labelTetrahedra: {time.perf_counter()-tic:.3f} seconds.")
765 im1Def = spam.DIC.applyMeshTransformation(
766 im1,
767 points,
768 connectivity,
769 displacements,
770 imTetLabel=imTetLabel,
771 nThreads=nThreads,
772 )
773 if debugFiles:
774 print("[globalCorrelation] Saving initial images")
775 for name, image in [
776 [f"{prefix}-def-init.tif", im1Def],
777 [f"{prefix}-imTetLabel-init.tif", imTetLabel],
778 ]:
779 print(f"[globalCorrelation]\t{name}: {image.shape}")
780 tifffile.imwrite(name, image)
782 # print("[globalCorrelation] Correlating (MF)!")
783 print("[globalCorrelation] Calculating gradient of IM TWO...")
784 im2Grad = numpy.array(numpy.gradient(im2), dtype="<f4")
786 print("[globalCorrelation] Computing global matrix")
787 # This generates the globalMatrix (big Mc matrix) with imGrad as input
788 Mc = numpy.zeros((3 * points.shape[0], 3 * points.shape[0]), dtype="<f8")
790 if debugFiles:
791 print("[globalCorrelation] Computing debug files fields")
792 gradientPerTet = numpy.zeros((connectivity.shape[0], 3), dtype="<f8")
793 IDPerTet = numpy.array([_ for _ in range(connectivity.shape[0])])
795 computeGradientPerTet(
796 imTetLabel.astype("<u4"),
797 im2Grad.astype("<f4"),
798 connectivity.astype("<u4"),
799 (points + displacements).astype("<f8"),
800 gradientPerTet,
801 )
803 spam.helpers.writeUnstructuredVTK(
804 (points + displacements),
805 connectivity,
806 cellData={"meanGradient": gradientPerTet, "id": IDPerTet},
807 fileName=f"{prefix}-gradient.vtk",
808 )
809 del gradientPerTet
811 computeDICglobalMatrix(
812 imTetLabel.astype("<u4"),
813 im2Grad.astype("<f4"),
814 connectivity.astype("<u4"),
815 (points + displacements).astype("<f8"),
816 Mc,
817 )
819 ###############################################################
820 # Setup left hand vector
821 ###############################################################
822 if regularisationMatrix:
823 regularisationField = isochoricField(points) if regularisationField is None else regularisationField
824 Ec = _computeFunctional(regularisationField, Mc)
825 regularisationMatrix *= Ec
826 print(f"[regularisation] Wc/Ec = {1/Ec:.3e}")
827 left_hand_inverse = numpy.linalg.inv(Mc + regularisationMatrix)
828 else:
829 print("[globalCorrelation] Skip regularisation")
830 left_hand_inverse = numpy.linalg.inv(Mc)
831 del Mc
833 # error = _errorCalc(im2, im1Def, im1, meshPaddingSlice)
834 # print("\[globalCorrelation] Initial Error (abs) = ", error)
836 # We try to solve Md=F
837 # while error > 0.1 and error < errorIn:
838 # while error > 0.1 and i <= maxIterations and error < errorIn:
839 dxNorm = numpy.inf
840 i = 0
841 while dxNorm > convergenceCriterion and i < maxIterations:
842 i += 1
844 # This function returns globalVector (F) taking in im1Def and im2 and the gradients
845 tic = time.perf_counter()
846 # print("[globalCorrelation] [newton] run computeDICglobalVector: ", end="")
847 right_hand_vector = numpy.zeros((3 * points.shape[0]), dtype="<f8")
848 computeDICglobalVector(
849 imTetLabel.astype("<u4"),
850 im2Grad.astype("<f4"),
851 im1Def.astype("<f4"),
852 im2.astype("<f4"),
853 connectivity.astype("<u4"),
854 (points + displacements).astype("<f8"),
855 right_hand_vector,
856 )
857 # print(f"{time.perf_counter()-tic:.3f} seconds.")
859 tic = time.perf_counter()
860 # print("[globalCorrelation] [newton] run solve: ", end="")
862 # solve: we can use solve here for sake of precision (over computing
863 # M^-1). However solve takes quite a lot of time for "small" meshes).
865 if regularisationMatrix:
866 right_hand_vector -= numpy.matmul(regularisationMatrix, displacements.ravel())
867 dx = numpy.matmul(left_hand_inverse, right_hand_vector).astype("<f8")
868 # dx_solve = numpy.linalg.solve(
869 # Mc,
870 # right_hand_vector
871 # ).astype('<f8')
872 # print(numpy.linalg.norm(dx - dx_solve))
874 displacements += dx.reshape(points.shape[0], 3)
875 dxNorm = numpy.linalg.norm(dx)
876 # print(f"{time.perf_counter()-tic:.3f} seconds.")
878 if medianFilterEachIteration:
879 # use connectivity to filter
880 print("[globalCorrelation] [newton] Median filter of displacements...")
881 for nodeNumber in range(points.shape[0]):
882 # get rows of connectivity (i.e., tets) which include this point
883 connectedTets = numpy.where(connectivity == nodeNumber)[0]
884 neighbourPoints = numpy.unique(connectivity[connectedTets])
885 diff = numpy.median(displacements[neighbourPoints], axis=0) - displacements[nodeNumber]
886 displacements[nodeNumber] += 0.5 * diff
888 tic = time.perf_counter()
889 # print("[globalCorrelation] [newton] run labelTetrahedra: ", end="")
891 imTetLabel = spam.label.labelTetrahedra(im1.shape, points + displacements, connectivity, nThreads=nThreads)
892 # print(f"{time.perf_counter()-tic:.3f} seconds.")
894 tic = time.perf_counter()
895 # print("[globalCorrelation] [newton] run applyMeshTransformation: ", end="")
896 im1Def = spam.DIC.applyMeshTransformation(
897 im1,
898 points,
899 connectivity,
900 displacements,
901 imTetLabel=imTetLabel,
902 nThreads=nThreads,
903 )
904 # print(f"{time.perf_counter()-tic:.3f} seconds.")
906 if debugFiles:
907 tifffile.imwrite(f"{prefix}-def-i{i:03d}.tif", im1Def)
908 tifffile.imwrite(
909 f"{prefix}-residual-cropped-i{i:03d}.tif",
910 im1Def[meshPaddingSlice] - im2[meshPaddingSlice],
911 )
912 # tifffile.imwrite(f"{prefix}-imTetLabel-i{i:03d}.tif", imTetLabel)
914 pointData = {
915 "displacements": displacements,
916 "initialDisplacements": initialDisplacements,
917 "fluctuations": numpy.subtract(displacements, initialDisplacements),
918 }
920 # compute strain for each fields
921 cellData = {}
922 components = ["vol", "dev", "volss", "devss"]
923 for fieldName, field in pointData.items():
924 Ffield = spam.deformation.FfieldBagi(points, connectivity, field, verbose=False)
925 decomposedFfield = spam.deformation.decomposeFfield(Ffield, components)
926 for c in components:
927 cellData[f"{fieldName}-{c}"] = decomposedFfield[c]
929 spam.helpers.writeUnstructuredVTK(
930 points.copy(),
931 connectivity.copy(),
932 pointData=pointData,
933 cellData=cellData,
934 fileName=f"{prefix}-displacementFE-i{i:03d}.vtk",
935 )
937 # print("\t\[globalCorrelation] Error Out = %0.5f%%" % (error))
938 # reshapedDispl = displacements.reshape(points.shape[0], 3)
939 dMin = numpy.min(displacements, axis=0)
940 dMed = numpy.median(displacements, axis=0)
941 dMax = numpy.max(displacements, axis=0)
942 strMin = f"Min={dMin[0]: .3f} {dMin[1]: .3f} {dMin[2]: .3f}"
943 strMed = f"Med={dMed[0]: .3f} {dMed[1]: .3f} {dMed[2]: .3f}"
944 strMax = f"Max={dMax[0]: .3f} {dMax[1]: .3f} {dMax[2]: .3f}"
945 print(f"[globalCorrelation] [newton] i={i:03d}, displacements {strMin}, {strMed}, {strMax}, dx={dxNorm:.2f}")
947 return displacements
950if __name__ == "__main__":
951 pass
953 # create mesh
954 print("Create mesh")
955 # points, connectivity = spam.mesh.createCuboid([32.55, 72.1, 15.7], 20, origin=[-4.2, 12.5, 78.01])
956 points, connectivity = spam.mesh.createCylinder([-24, 27], 12.5, 97.3, 10, zOrigin=-4.2)
958 configuration = {
959 # Information about the bulk regularisation
960 "young": 10, # mandatory (Young modulus)
961 "poisson": 0.25, # mandatory (Poisson ratio)
962 # "xi": 30, # optional
963 # "periods": 3, # optionnal (whatever...)
964 # Information about the surface regularisation
965 # (Dirichlet boundary conditions)
966 # Each surface of the cuboid is labelled by the keywords
967 # z_start: z == 0, z_end, y_start, y_end, x_start and x_end)
968 # If a keyword is ommited the surface is not regularised.
969 "dirichlet": {
970 "z_start": {"xi": 30, "dof": [0, 1, 2]}, # optional # mandatory
971 "z_end": {"xi": 30, "dof": [0]}, # xi normalisation is 30
972 "z_lateral": {"xi": 30, "dof": [1, 2]}, # xi normalisation is 30
973 },
974 }
976 p = regularisationParameters(configuration)
978 # STEP 1: get labels
979 labels = surfaceLabels(points, surfaces=p["surfaces"])
981 # STEP 2: build regularisation matrix
982 Mreg, v = regularisationMatrix(points, connectivity, p["young"], p["poisson"], xiBulk=p["xi"], dirichlet=p["dirichlet"], labels=labels, periods=p["periods"])