Source code for saenopy.solver

import sys
import time

import numpy as np
import scipy.sparse as ssp

from numba import njit
from pyfields import field
from typing import Union
from nptyping import NDArray, Shape, Float, Int, Bool

from saenopy.build_beams import build_beams
from saenopy.materials import Material, SemiAffineFiberMaterial
from saenopy.conjugate_gradient import cg
from saenopy.mesh import Mesh, check_tetrahedra_scalar_field, check_node_scalar_field, \
    check_node_vector_field
from saenopy.saveable import Saveable
from typing import List


class SolverMesh(Mesh):
    __save_parameters__ = ["nodes", "tetrahedra", "displacements", "forces",
                           "displacements_fixed", "displacements_target", "displacements_target_mask",
                           "forces_target", "strain_energy", "movable", "cell_boundary_mask",
                           "regularisation_mask"]
    number_tetrahedra = 0  # the number of tetrahedra
    number_nodes = 0  # the number of vertices

    energy: NDArray[Shape["N_t"], Float] = field(doc="the energy stored in each tetrahedron, dimensions: N_T",
                                                 validators=check_tetrahedra_scalar_field, default=None)
    volume: NDArray[Shape["N_t"], Float] = field(doc="the volume of each tetrahedron, dimensions: N_T",
                                                 validators=check_tetrahedra_scalar_field, default=None)
    movable: NDArray[Shape["N_c"], Bool] = field(doc="a bool if a node is movable", validators=check_node_scalar_field, default=None)

    Phi: NDArray[Shape["N_t, 4, 3"], Float] = field(doc="the shape tensor of each tetrahedron, dimensions: N_T x 4 x 3", default=None)
    Phi_valid = False
    displacements: NDArray[Shape["N_c, 3"], Float] = field(doc="the displacements of each node, dimensions: N_c x 3",
                                                           validators=check_node_vector_field, default=None)
    displacements_fixed: NDArray[Shape["N_c, 3"], Float] = field(validators=check_node_vector_field, default=None)
    displacements_target: NDArray[Shape["N_c, 3"], Float] = field(validators=check_node_vector_field, default=None)
    displacements_target_mask: NDArray[Shape["N_c"], Bool] = field(validators=check_node_scalar_field, default=None)
    regularisation_mask: NDArray[Shape["N_c"], Bool] = field(validators=check_node_scalar_field, default=None)
    cell_boundary_mask: NDArray[Shape["N_c"], Bool] = field(validators=check_node_scalar_field, default=None)

    forces: NDArray[Shape["N_c, 3"], Float] = field(doc="the global forces on each node, dimensions: N_c x 3",
                                                    validators=check_node_vector_field, default=None)
    forces_target: NDArray[Shape["N_c, 3"], Float] = field(doc="the external forces on each node, dimensions: N_c x 3",
                                                           validators=check_node_vector_field, default=None)
    stiffness_tensor: NDArray[Shape["N_c, N_c, 3, 3"], Float] = None  # the global stiffness tensor, dimensions: N_c x N_c x 3 x 3

    strain_energy: float = 0  # the global energy

    # a list of all vertices are connected via a tetrahedron, stored as pairs: dimensions: N_connections x 2
    connections: NDArray[Shape["N_con, 2"], Int] = None
    connections_valid = False


[docs]class Solver(Saveable): __save_parameters__ = ["mesh", "regularisation_results", "regularisation_parameters", "material_model"] mesh: SolverMesh s: NDArray[Shape["N_b, 3"], Float] = None # the beams, dimensions N_b x 3 N_b = 0 # the number of beams material_model: SemiAffineFiberMaterial = None # the function specifying the material model material_parameters = None regularisation_results: list = None regularisation_parameters: dict = None verbose = False preprocessing = None ''' preprocessing = [ "load_stack": ["1_*.tif", "2_*.tif", voxel_sixe], "3D_piv": [overlap, windowsize, signoise, drifcorrection], "iterpolate_mesh": [], ] ''' def __init__(self, **kwargs): self.mesh = SolverMesh() super().__init__(**kwargs)
[docs] def set_nodes(self, data: NDArray[Shape["N_c, 3"], Float]): """ Provide mesh coordinates. Parameters ---------- data : ndarray The coordinates of the vertices. Dimensions Nx3 """ self.mesh.set_nodes(data) # schedule to recalculate the shape tensors self.mesh.Phi_valid = False # store the number of vertices self.mesh.number_nodes = data.shape[0] self.mesh.movable = np.ones(self.mesh.number_nodes, dtype=bool) self.mesh.displacements = np.zeros((self.mesh.number_nodes, 3)) self.mesh.forces = np.zeros((self.mesh.number_nodes, 3)) self.mesh.forces_target = np.zeros((self.mesh.number_nodes, 3))
[docs] def set_boundary_condition(self, displacements: NDArray[Shape["N_c, 3"], Float] = None, forces: NDArray[Shape["N_c, 3"], Float] = None): """ Provide the boundary condition for the mesh, to be used with :py:meth:`~.Solver.solve_nonregularized`. Parameters ---------- displacements : ndarray, optional If the displacement of a node is not nan, it is treated as a Dirichlet boundary condition and the displacement of this node is kept fixed during solving. Dimensions Nx3 forces : ndarray, optional If the force of a node is not nan, it is treated as a von Neumann boundary condition and the solver tries to match the force on the node with the here given force. Dimensions Nx3 """ # initialize 0 displacement for each node if displacements is None: self.mesh.displacements = np.zeros((self.mesh.number_nodes, 3)) else: displacements = np.asarray(displacements, dtype=np.float64) assert displacements.shape == (self.mesh.number_nodes, 3) self.mesh.movable = np.any(np.isnan(displacements), axis=1) self.mesh.displacements_fixed = displacements self.mesh.displacements[~self.mesh.movable] = displacements[~self.mesh.movable] # initialize global and external forces if forces is None: self.mesh.forces_target = np.zeros((self.mesh.number_nodes, 3)) else: self._set_external_forces(forces) # if no displacements where given, take the variable nodes from the nans in the force list if displacements is None: self.mesh.movable = ~np.any(np.isnan(forces), axis=1) # if not, check if the fixed displacements have no force elif np.all(np.isnan(self.mesh.forces_target[~self.mesh.movable])) is False: print("WARNING: Forces for fixed vertices were specified. These boundary conditions cannot be" "fulfilled", file=sys.stderr)
[docs] def set_initial_displacements(self, displacements: NDArray[Shape["N_c, 3"], Float]): """ Provide initial displacements of the nodes. For fixed nodes these displacements are ignored. Parameters ---------- displacements : ndarray The list of displacements. Dimensions Nx3 """ # check the input displacements = np.asarray(displacements) assert displacements.shape == (self.mesh.number_nodes, 3) self.mesh.displacements[self.mesh.movable] = displacements[self.mesh.movable].astype(np.float64)
def _set_external_forces(self, forces: NDArray[Shape["N_c, 3"], Float]): """ Provide external forces that act on the vertices. The forces can also be set with :py:meth:`~.Solver.setNodes` directly with the vertices. Parameters ---------- forces : ndarray The list of forces. Dimensions Nx3 """ # check the input forces = np.asarray(forces) assert forces.shape == (self.mesh.number_nodes, 3) self.mesh.forces_target = forces.astype(np.float64)
[docs] def set_tetrahedra(self, data: NDArray[Shape["N_t, 4"], Int]): """ Provide mesh connectivity. Nodes have to be connected by tetrahedra. Each tetraherdon consts of the indices of the 4 vertices which it connects. Parameters ---------- data : ndarray The node indices of the 4 corners. Dimensions Nx4 """ self.mesh.set_tetrahedra(data) # the number of tetrahedra self.mesh.number_tetrahedra = data.shape[0] # Phi is a 4x3 tensor for every tetrahedron self.mesh.Phi = np.zeros((self.mesh.number_tetrahedra, 4, 3)) # initialize the volume and energy of each tetrahedron self.mesh.volume = np.zeros(self.mesh.number_tetrahedra) self.mesh.energy = np.zeros(self.mesh.number_tetrahedra) # schedule to recalculate the shape tensors self.mesh.Phi_valid = False # schedule to recalculate the connections self.connections_valid = False
[docs] def set_material_model(self, material: Material, generate_lookup=True): """ Provides the material model. Parameters ---------- material : :py:class:`~.materials.Material` The material, must be of a subclass of Material. """ self.material_model = material if generate_lookup is True: self.material_model_look_up = self.material_model.generate_look_up_table()
[docs] def set_beams(self, beams: Union[int, NDArray[Shape["N_b, 3"], Float]] = 300): """ Sets the beams for the calculation over the whole solid angle. Parameters ---------- beams : int, ndarray Either an integer which defines in how many beams to discretize the whole solid angle or an ndarray providing the beams, dimensions Nx3, default 300 """ if isinstance(beams, int): beams = build_beams(beams) self.s = beams self.N_b = beams.shape[0]
def _compute_connections(self): # current scipy versions do not have the sputils anymore try: from scipy.sparse._sputils import get_index_dtype except ImportError: from scipy.sparse.sputils import get_index_dtype # calculate the indices for "update_f_glo" y, x = np.meshgrid(np.arange(3), self.mesh.tetrahedra.ravel()) self.mesh.force_distribute_coordinates = (x.ravel(), y.ravel()) self.mesh.force_distribute_coordinates = tuple(self.mesh.force_distribute_coordinates[i].astype(dtype=get_index_dtype(maxval=max(self.mesh.force_distribute_coordinates[i].shape))) for i in range(2)) # calculate the indices for "update_K_glo" @njit() def numba_get_pair_coordinates(T, var): stiffness_distribute_coordinates2 = [] filter_in = np.zeros((T.shape[0], 4, 4, 3, 3)) == 1 # iterate over all tetrahedra for t in range(T.shape[0]): #if t % 1000: # print(t, T.shape[0]) tet = T[t] # over all corners for t1 in range(4): c1 = tet[t1] if not var[c1]: continue filter_in[t, t1, :, :, :] = True for t2 in range(4): # get two vertices of the tetrahedron c2 = tet[t2] for i in range(3): for j in range(3): # add the connection to the set stiffness_distribute_coordinates2.append((c1*3+i, c2*3+j)) stiffness_distribute_coordinates2 = np.array(stiffness_distribute_coordinates2) return filter_in.ravel(), (stiffness_distribute_coordinates2[:, 0], stiffness_distribute_coordinates2[:, 1]) self.mesh.filter_in, self.stiffness_distribute_coordinates2 = numba_get_pair_coordinates(self.mesh.tetrahedra, self.mesh.movable) self.stiffness_distribute_coordinates2 = np.array(self.stiffness_distribute_coordinates2, dtype=get_index_dtype(maxval=max(self.stiffness_distribute_coordinates2[0].shape))) # remember that for the current configuration the connections have been calculated self.mesh.connections_valid = True def _compute_phi(self): """ Calculate the shape tensors of the tetrahedra (see page 49) """ # define the helper matrix chi Chi = np.zeros((4, 3)) Chi[0, :] = [-1, -1, -1] Chi[1, :] = [1, 0, 0] Chi[2, :] = [0, 1, 0] Chi[3, :] = [0, 0, 1] # tetrahedron matrix B (linear map of the undeformed tetrahedron T onto the primitive tetrahedron P) B = self.mesh.nodes[self.mesh.tetrahedra[:, 1:4]] - self.mesh.nodes[self.mesh.tetrahedra[:, 0]][:, None, :] B = B.transpose(0, 2, 1) # calculate the volume of the tetrahedron self.mesh.volume = np.abs(np.linalg.det(B)) / 6.0 sum_zero = np.sum(self.mesh.volume == 0) if np.sum(self.mesh.volume == 0): print("WARNING: found %d elements with volume of 0. Removing those elements." % sum_zero) self.set_tetrahedra(self.mesh.tetrahedra[self.mesh.volume != 0]) return self._compute_phi() # the shape tensor of the tetrahedron is defined as Chi * B^-1 self.mesh.Phi = Chi @ np.linalg.inv(B) # remember that for the current configuration the shape tensors have been calculated self.mesh.Phi_valid = True """ relaxation """ def _prepare_temporary_quantities(self): # test if one node of the tetrahedron is variable # only count the energy if not the whole tetrahedron is fixed self._countEnergy = np.any(self.mesh.movable[self.mesh.tetrahedra], axis=1) # and the shape tensor with the beam # s*_tmb = Phi_tmj * s_jb (t in [0, N_T], i,j in {x,y,z}, m in {1,2,3,4}), b in [0, N_b]) self._s_star = self.mesh.Phi @ self.s.T self._V_over_Nb = np.expand_dims(self.mesh.volume, axis=1) / self.N_b def _update_glo_f_and_k(self): """ Calculates the stiffness matrix K_ij, the force F_i and the energy E of each node. """ t_start = time.time() batchsize = 1000 self.mesh.strain_energy = 0 f_glo = np.zeros((self.mesh.number_tetrahedra, 4, 3)) K_glo = np.zeros((self.mesh.number_tetrahedra, 4, 4, 3, 3)) for i in range(int(np.ceil(self.mesh.tetrahedra.shape[0] / batchsize))): if self.verbose: print("updating forces and stiffness matrix %d%%" % (i / int(np.ceil(self.mesh.tetrahedra.shape[0] / batchsize)) * 100), end="\r") t = slice(i*batchsize, (i+1)*batchsize) s_bar = self._get_s_bar(t) epsilon_b, dEdsbar, dEdsbarbar = self._get_applied_epsilon(s_bar, self.material_model_look_up, self._V_over_Nb[t]) self._update_energy(epsilon_b, t) self._update_f_glo(self._s_star[t], s_bar, dEdsbar, out=f_glo[t]) self._update_K_glo(self._s_star[t], s_bar, dEdsbar, dEdsbarbar, out=K_glo[t]) # store the global forces in self.mesh.f_glo # transform from N_T x 4 x 3 -> N_v x 3 ssp.coo_matrix((f_glo.ravel(), self.mesh.force_distribute_coordinates), shape=self.mesh.forces.shape).toarray(out=self.mesh.forces) # store the stiffness matrix K in self.K_glo # transform from N_T x 4 x 4 x 3 x 3 -> N_v * 3 x N_v * 3 self.K_glo = ssp.coo_matrix((K_glo.ravel()[self.mesh.filter_in], self.stiffness_distribute_coordinates2), shape=(self.mesh.number_nodes * 3, self.mesh.number_nodes * 3)).tocsr() if self.verbose: print("updating forces and stiffness matrix finished %.2fs" % (time.time() - t_start))
[docs] def get_max_tet_stiffness(self) -> NDArray[Shape["N_t"], Float]: """ Calculates the stiffness matrix K_ij, the force F_i and the energy E of each node. """ t_start = time.time() batchsize = 10000 tetrahedra_stiffness = np.zeros(self.mesh.tetrahedra.shape[0]) for i in range(int(np.ceil(self.mesh.tetrahedra.shape[0] / batchsize))): if self.verbose: print("updating forces and stiffness matrix %d%%" % (i / int(np.ceil(self.mesh.tetrahedra.shape[0] / batchsize)) * 100), end="\r") t = slice(i*batchsize, (i+1)*batchsize) s_bar = self._get_s_bar(t) s = np.linalg.norm(s_bar, axis=1) epsbarbar_b = self.material_model.stiffness(s - 1) tetrahedra_stiffness[t] = np.max(epsbarbar_b, axis=1) return tetrahedra_stiffness
def _get_s_bar(self, t: slice) -> NDArray[Shape["N_t, 3, N_b"], Float]: # get the displacements of all corners of the tetrahedron (N_Tx3x4) # u_tim (t in [0, N_T], i in {x,y,z}, m in {1,2,3,4}) # F is the linear map from T (the undeformed tetrahedron) to T' (the deformed tetrahedron) # F_tij = d_ij + u_tmi * Phi_tmj (t in [0, N_T], i,j in {x,y,z}, m in {1,2,3,4}) F = np.eye(3) + np.einsum("tmi,tmj->tij", self.mesh.displacements[self.mesh.tetrahedra[t]], self.mesh.Phi[t]) # multiply the F tensor with the beam # s'_tib = F_tij * s_jb (t in [0, N_T], i,j in {x,y,z}, b in [0, N_b]) s_bar = F @ self.s.T return s_bar @staticmethod #@njit()#nopython=True, cache=True) def _get_applied_epsilon(s_bar: np.ndarray, lookUpEpsilon: callable, _V_over_Nb: np.ndarray): # the "deformation" amount # p 54 equ 2 part in the parentheses # s_tb = |s'_tib| (t in [0, N_T], i in {x,y,z}, b in [0, N_b]) s = np.linalg.norm(s_bar, axis=1) epsilon_b, epsbar_b, epsbarbar_b = lookUpEpsilon(s - 1) # eps'_tb 1 # dEdsbar_tb = - ------- * --- * V_t # s_tb N_b dEdsbar = - (epsbar_b / s) * _V_over_Nb # s_tb * eps''_tb - eps'_tb 1 # dEdsbarbar_tb = --------------------------- * --- * V_t # s_tb**3 N_b dEdsbarbar = ((s * epsbarbar_b - epsbar_b) / (s ** 3)) * _V_over_Nb return epsilon_b, dEdsbar, dEdsbarbar def _update_energy(self, epsilon_b: np.ndarray, t: np.ndarray): # sum the energy of this tetrahedron # E_t = eps_tb * V_t self.mesh.energy[t] = np.mean(epsilon_b, axis=1) * self.mesh.volume[t] # only count the energy of the tetrahedron to the global energy if the tetrahedron has at least one # variable node self.mesh.strain_energy += np.sum(self.mesh.energy[t][self._countEnergy[t]]) def _update_f_glo(self, s_star: np.ndarray, s_bar: np.ndarray, dEdsbar: np.ndarray, out: np.ndarray): # f_tmi = s*_tmb * s'_tib * dEds'_tb (t in [0, N_T], i in {x,y,z}, m in {1,2,3,4}, b in [0, N_b]) np.einsum("tmb,tib,tb->tmi", s_star, s_bar, dEdsbar, out=out) def _update_K_glo(self, s_star: np.ndarray, s_bar: np.ndarray, dEdsbar: np.ndarray, dEdsbarbar: np.ndarray, out: np.ndarray): # / | | \ / | | \ / | | \ # ___ / s' w"| |s'| - 1 | - w' | |s'| - 1 | w' | | s' | - 1 | \ # 1 \ * * | b \ | b| / \ | b| / \ | b | / | # - > s * s * | ------------------------------------ * s' * s' + ---------------------- * delta | # N / bm br | |s'|Âł ib lb |s' | li | # b --- \ | b| | b | / # # (t in [0, N_T], i,l in {x,y,z}, m,r in {1,2,3,4}, b in [0, N_b]) s_bar_s_bar = 0.5 * (np.einsum("tb,tib,tlb->tilb", dEdsbarbar, s_bar, s_bar) - np.einsum("il,tb->tilb", np.eye(3), dEdsbar)) np.einsum("tmb,trb,tilb->tmril", s_star, s_star, s_bar_s_bar, out=out, optimize=['einsum_path', (0, 1), (0, 1)]) def _check_relax_ready(self): """ Checks whether everything is loaded to start a relaxation process. """ # check if we have nodes if self.mesh.nodes is None or self.mesh.nodes.shape[0] == 0: raise ValueError("No nodes have yet been set. Call setNodes first.") self.mesh.number_nodes = self.mesh.nodes.shape[0] # check if we have tetrahedra if self.mesh.tetrahedra is None or self.mesh.tetrahedra.shape[0] == 0: raise ValueError("No tetrahedra have yet been set. Call setTetrahedra first.") self.mesh.number_tetrahedra = self.mesh.tetrahedra.shape[0] self.mesh.energy = np.zeros(self.mesh.number_tetrahedra) # check if we have a material model if self.material_model is None: raise ValueError("No material model has been set. Call setMaterialModel first.") # if the beams have not been set yet, initialize them with the default configuration if self.s is None: self.set_beams() # if the shape tensors are not valid, calculate them if self.mesh.Phi_valid is False: self._compute_phi() # if the connections are not valid, calculate them if self.mesh.connections_valid is False: self._compute_connections()
[docs] def solve_boundarycondition(self, step_size: float = 0.066, max_iterations: int = 300, i_min: int = 12, rel_conv_crit: float = 0.01, relrecname: str = None, verbose: bool = False, callback: callable = None): """ Solve the displacement of the free nodes constraint to the boundary conditions. Parameters ---------- step_size : float, optional How much of the displacement of each conjugate gradient step to apply. Default 0.066 max_iterations : int, optional The maximal number of iterations for the relaxation. Default 300 i_min : int, optional The minimal number of iterations for the relaxation. Minimum value is 6. Default is 12 rel_conv_crit : float, optional If the relative standard deviation of the last 6 energy values is below this threshold, finish the iteration. Default 0.01 relrecname : string, optional If a filename is provided, for every iteration the displacement of the conjugate gradient step, the global energy and the residuum are stored in this file. verbose : bool, optional If true print status during optimisation callback : callable, optional A function to call after each iteration (e.g. for a live plot of the convergence) """ # set the verbosity level self.verbose = verbose # check if everything is prepared self._check_relax_ready() self._prepare_temporary_quantities() # update the forces and stiffness matrix self._update_glo_f_and_k() relrec = [[0, self.mesh.strain_energy, np.sum(self.mesh.forces[self.mesh.movable] ** 2)]] start = time.time() # start the iteration for i in range(max_iterations): # move the displacements in the direction of the forces one step # but while moving the stiffness tensor is kept constant du = self._solve_cg(step_size) # update the forces on each tetrahedron and the global stiffness tensor self._update_glo_f_and_k() # sum all squared forces of non fixed nodes ff = np.sum((self.mesh.forces[self.mesh.movable] - self.mesh.forces_target[self.mesh.movable]) ** 2) #ff = np.sum(self.mesh.f[self.mesh.var] ** 2) # print and store status if self.verbose: print("Newton ", i, ": du=", du, " Energy=", self.mesh.strain_energy, " Residuum=", ff) # log and store values (if a target file was provided) relrec.append([du, self.mesh.strain_energy, ff]) if relrecname is not None: np.savetxt(relrecname, relrec) if callback is not None: callback(self, relrec) # if we have passed i_min iterations we calculate average and std of the last 6 iteration if i > i_min: # calculate the average energy over the last 6 iterations last_Es = np.array(relrec)[:-6:-1, 1] Emean = np.mean(last_Es) Estd = np.std(last_Es) # if the iterations converge, stop the iteration # coefficient of variation below "rel_conv_crit" if Estd / Emean < rel_conv_crit: break # print the elapsed time finish = time.time() if self.verbose: print("| time for relaxation was", finish - start) self.boundary_results = relrec return relrec
def _solve_cg(self, step_size: float): """ Solve the displacements from the current stiffness tensor using conjugate gradient. """ # calculate the difference between the current forces on the nodes and the desired forces ff = self.mesh.forces - self.mesh.forces_target # ignore the force deviations on fixed nodes ff[~self.mesh.movable, :] = 0 # solve the conjugate gradient which solves the equation A x = b for x # where A is the stiffness matrix K_glo and b is the vector of the target forces uu = cg(self.K_glo, ff.ravel(), maxiter=3 * self.mesh.number_nodes, tol=0.00001, verbose=self.verbose).reshape(ff.shape) # add the new displacements to the stored displacements self.mesh.displacements[self.mesh.movable] += uu[self.mesh.movable] * step_size # sum the applied displacements du = np.sum(uu[self.mesh.movable] ** 2) * step_size * step_size # return the total applied displacement return du """ regularization """
[docs] def set_target_displacements(self, displacement: NDArray[Shape["N_c, 3"], Float], reg_mask: NDArray[Shape["N_c"], Bool] = None): """ Provide the displacements that should be fitted by the regularization. Parameters ---------- displacement : ndarray If the displacement of a node is not nan, it is The displacements for each node. Dimensions N x 3 """ displacement = np.asarray(displacement) assert displacement.shape == (self.mesh.number_nodes, 3) self.mesh.displacements_target = displacement # only use displacements that are not nan self.mesh.displacements_target_mask = np.any(~np.isnan(displacement), axis=1) # regularisation mask if reg_mask is not None: assert reg_mask.shape == (self.mesh.number_nodes,), f"reg_mask should have the shape {(self.mesh.number_nodes,)} but has {reg_mask.shape}." assert reg_mask.dtype == bool, f"reg_mask should have the type bool but has {reg_mask.dtype}." self.mesh.regularisation_mask = reg_mask else: self.mesh.regularisation_mask = np.ones_like(displacement[:, 0]).astype(np.bool)
def _update_local_regularization_weigth(self, method: str): self.localweight[:] = 1 Fvalues = np.linalg.norm(self.mesh.forces, axis=1) #Fmedian = np.median(Fvalues[self.mesh.var]) Fmedian = np.median(Fvalues[self.mesh.movable & self.mesh.regularisation_mask]) if method == "singlepoint": self.localweight[int(self.CFG["REG_FORCEPOINT"])] = 1.0e-10 if method == "bisquare": k = 4.685 index = Fvalues < k * Fmedian self.localweight[index * self.mesh.movable] *= (1 - (Fvalues[index * self.mesh.movable] / k / Fmedian) * (Fvalues[index * self.mesh.movable] / k / Fmedian)) * ( 1 - (Fvalues[index * self.mesh.movable] / k / Fmedian) * (Fvalues[index * self.mesh.movable] / k / Fmedian)) self.localweight[~index * self.mesh.movable] *= 1e-10 if method == "cauchy": k = 2.385 if Fmedian > 0: self.localweight[self.mesh.movable] *= 1.0 / (1.0 + np.power((Fvalues / k / Fmedian), 2.0)) else: self.localweight *= 1.0 if method == "huber": k = 1.345 index = (Fvalues > (k * Fmedian)) & self.mesh.movable self.localweight[index] = k * Fmedian / Fvalues[index] if method == "L1": if Fmedian > 0: self.localweight[:] = 1 / Fvalues[:] else: self.localweight *= 1.0 index = self.localweight < 1e-10 self.localweight[index & self.mesh.movable] = 1e-10 if self.mesh.cell_boundary_mask is not None: self.localweight[:] = 0.03*100 self.localweight[self.mesh.cell_boundary_mask] = 0.003*0.001 self.localweight[~self.mesh.regularisation_mask] = 0 counter = np.sum(1.0 - self.localweight[self.mesh.movable]) counterall = np.sum(self.mesh.movable) if self.verbose: print("total weight: ", counter, "/", counterall) def _compute_regularization_a_and_b(self, alpha: float): KA = self.K_glo.multiply(np.repeat(self.localweight * alpha, 3)[None, :]) self.KAK = KA @ self.K_glo self.A = self.I + self.KAK self.b = (KA @ self.mesh.forces.ravel()).reshape(self.mesh.forces.shape) index = self.mesh.movable & self.mesh.displacements_target_mask self.b[index] += self.mesh.displacements_target[index] - self.mesh.displacements[index] def _record_regularization_status(self, relrec: list, alpha: float, relrecname: str = None): indices = self.mesh.movable & self.mesh.displacements_target_mask btemp = self.mesh.displacements_target[indices] - self.mesh.displacements[indices] uuf2 = np.sum(btemp ** 2) suuf = np.sum(np.linalg.norm(btemp, axis=1)) bcount = btemp.shape[0] u2 = np.sum(self.mesh.displacements[self.mesh.movable] ** 2) f = np.zeros((self.mesh.number_nodes, 3)) f[self.mesh.movable] = self.mesh.forces[self.mesh.movable] ff = np.sum(np.sum(f**2, axis=1) * self.localweight * self.mesh.movable) L = alpha*ff + uuf2 if self.verbose: print("|u-uf|^2 =", uuf2) print("|w*f|^2 =", ff, "\t\t|u|^2 =", u2) print("L = |u-uf|^2 + lambda*|w*f|^2 = ", L) relrec.append((L, uuf2, ff)) if relrecname is not None: np.savetxt(relrecname, relrec)
[docs] def solve_regularized(self, step_size: float = 0.33, solver_precision: float = 1e-18, max_iterations: int = 300, i_min: int = 12, rel_conv_crit: float = 0.01, alpha: float = 1e10, method: str = "huber", relrecname: str = None, verbose: bool = False, callback: callable = None): """ Fit the provided displacements. Displacements can be provided with :py:meth:`~.Solver.setTargetDisplacements`. Parameters ---------- step_size : float, optional How much of the displacement of each conjugate gradient step to apply. Default 0.33 solver_precision : float, optional The tolerance for the conjugate gradient step. Will be multiplied by the number of nodes. Default 1e-18. max_iterations : int, optional The maximal number of iterations for the regularisation. Default 300 i_min : int, optional The minimal number of iterations for the relaxation. Minimum value is 6. Default is 12. rel_conv_crit : float, optional If the relative standard deviation of the last 6 energy values is below this threshold, finish the iteration. Default 0.01 alpha : float, optional The regularisation parameter. How much to weight the suppression of forces against the fitting of the measured displacement. Default 3e9 method : string, optional The regularisation method to use: "huber" "bisquare" "cauchy" "singlepoint" relrecname : string, optional The filename where to store the output. Default is to not store the output, just to return it. verbose : bool, optional If true print status during optimisation callback : callable, optional A function to call after each iteration (e.g. for a live plot of the convergence) """ self.regularisation_parameters = { "step_size": step_size, "solver_precision": solver_precision, "max_iterations": max_iterations, "rel_conv_crit": rel_conv_crit, "alpha": alpha, "method": method, } # set the verbosity level self.verbose = verbose self.I = ssp.lil_matrix((self.mesh.displacements_target_mask.shape[0] * 3, self.mesh.displacements_target_mask.shape[0] * 3)) self.I.setdiag(np.repeat(self.mesh.displacements_target_mask, 3)) # check if everything is prepared self._check_relax_ready() self._prepare_temporary_quantities() self.localweight = np.ones(self.mesh.number_nodes) # update the forces on each tetrahedron and the global stiffness tensor if self.verbose: print("going to update glo f and K") self._update_glo_f_and_k() # log and store values (if a target file was provided) relrec = [] self.relrec = relrec if callback is not None: callback(self, relrec, 0, max_iterations) self._record_regularization_status(relrec, alpha, relrecname) if self.verbose: print("check before relax !") # start the iteration for i in range(max_iterations): # compute the weight matrix if method != "normal": self._update_local_regularization_weigth(method) # compute A and b for the linear equation that solves the regularisation problem self._compute_regularization_a_and_b(alpha) # get and apply the displacements that solve the regularisation term uu = self._solve_regularization_cg(step_size, solver_precision) # update the forces on each tetrahedron and the global stiffness tensor self._update_glo_f_and_k() if self.verbose: print("Round", i+1, " |du|=", uu) # log and store values (if a target file was provided) self._record_regularization_status(relrec, alpha, relrecname) if callback is not None: callback(self, relrec, i, max_iterations) # if we have passed i_min iterations we calculate average and std of the last 6 iteration if i > i_min: # calculate the average energy over the last 6 iterations last_Ls = np.array(relrec)[:-6:-1, 1] Lmean = np.mean(last_Ls) Lstd = np.std(last_Ls) # Use Coefficient of Variation; in saeno there was the additional factor "/ np.sqrt(5)" behind # if the iterations converge, stop the iteration if Lstd / Lmean < rel_conv_crit: break self.regularisation_results = relrec return relrec
def _solve_regularization_cg(self, step_size: float = 0.33, solver_precision: float = 1e-18): """ Solve the displacements from the current stiffness tensor using conjugate gradient. """ # solve the conjugate gradient which solves the equation A x = b for x # where A is (I - KAK) (K: stiffness matrix, A: weight matrix) and b is (u_meas - u - KAf) uu = cg(self.A, self.b.flatten(), maxiter=25*int(pow(self.mesh.number_nodes, 0.33333) + 0.5), tol=self.mesh.number_nodes * solver_precision).reshape((self.mesh.number_nodes, 3)) # add the new displacements to the stored displacements self.mesh.displacements += uu * step_size # sum the applied displacements du = np.sum(uu ** 2) * step_size * step_size # return the total applied displacement return np.sqrt(du / self.mesh.number_nodes) """ helper methods """ def get_polarity(self) -> float: inner = self.mesh.regularisation_mask f = self.mesh.forces[inner] R = self.mesh.nodes[inner] fsum = np.sum(f, axis=0) # B1 += self.mesh.R[c] * np.sum(f**2) B1 = np.einsum("ni,ni,nj->j", f, f, R) # B2 += f * (self.mesh.R[c] @ f) B2 = np.einsum("ki,ki,kj->j", f, R, f) # A += I * np.sum(f**2) - np.outer(f, f) A = np.sum(np.einsum("ij,kl,kl->kij", np.eye(3), f, f) - np.einsum("ki,kj->kij", f, f), axis=0) B = B1 - B2 try: Rcms = np.linalg.inv(A) @ B except np.linalg.LinAlgError: Rcms = np.array([0, 0, 0]) RR = R - Rcms contractility = np.sum(np.einsum("ki,ki->k", RR, f) / np.linalg.norm(RR, axis=1)) vecs = build_beams(150) eR = RR / np.linalg.norm(RR, axis=1)[:, None] f = self.mesh.forces[inner] # (eR @ vecs[b]) * (vecs[b] @ self.mesh.f_glo[c]) ff = np.sum(np.einsum("ni,bi->nb", eR, vecs) * np.einsum("bi,ni->nb", vecs, f), axis=0) # (RR @ vecs[b]) * (vecs[b] @ self.mesh.f_glo[c]) mm = np.sum(np.einsum("ni,bi->nb", RR, vecs) * np.einsum("bi,ni->nb", vecs, f), axis=0) bmax = np.argmax(mm) fmax = ff[bmax] return fmax / contractility def get_center(self, mode="force", border=None) -> NDArray[Shape["3"], Float]: f = self.mesh.forces R = self.mesh.nodes U = self.mesh.displacements if self.mesh.regularisation_mask is not None: f = self.mesh.forces * self.mesh.regularisation_mask[:, None] if mode.lower() == "deformation": # B1 += self.mesh.R[c] * np.sum(f**2) B1 = np.einsum("ni,ni,nj->j", U, U, R) # B2 += f * (self.mesh.R[c] @ f) B2 = np.einsum("ki,ki,kj->j", U, R, U) # A += I * np.sum(f**2) - np.outer(f, f) A = np.sum(np.einsum("ij,kl,kl->kij", np.eye(3), U, U) - np.einsum("ki,kj->kij", U, U), axis=0) B = B1 - B2 #print (U,R) Rcms = np.linalg.inv(A) @ B if mode.lower() == "deformation_target": # mode to calculate from PIV data directly U = self.mesh.displacements_target # remove nanas from 3D PIV here to be able to do the calculations U[np.isnan(U)] = 0 # B1 += self.mesh.R[c] * np.sum(f**2) B1 = np.einsum("ni,ni,nj->j", U, U, R) # B2 += f * (self.mesh.R[c] @ f) B2 = np.einsum("ki,ki,kj->j", U, R, U) # A += I * np.sum(f**2) - np.outer(f, f) A = np.sum(np.einsum("ij,kl,kl->kij", np.eye(3), U, U) - np.einsum("ki,kj->kij", U, U), axis=0) B = B1 - B2 #print (U,R) Rcms = np.linalg.inv(A) @ B if mode.lower() == "force": # calculate Force center # B1 += self.mesh.R[c] * np.sum(f**2) B1 = np.einsum("ni,ni,nj->j", f, f, R) # B2 += f * (self.mesh.R[c] @ f) B2 = np.einsum("ki,ki,kj->j", f, R, f) # A += I * np.sum(f**2) - np.outer(f, f) A = np.sum(np.einsum("ij,kl,kl->kij", np.eye(3), f, f) - np.einsum("ki,kj->kij", f, f), axis=0) B = B1 - B2 try: Rcms = np.linalg.inv(A) @ B except np.linalg.LinAlgError: Rcms = np.array([0, 0, 0]) return Rcms def get_contractility(self, center_mode="force", r_max=None, width_outer=None) -> str: f = self.mesh.forces R = self.mesh.nodes if self.mesh.regularisation_mask is not None: f = self.mesh.forces * self.mesh.regularisation_mask[:, None] if width_outer is not None: # mask the data points in the outer layer (closer to the outer border than # width_outer and for the inner shell the remaining inner volume outer_layer = (((R[:, 0]) > (R[:, 0].max() - width_outer)) | ((R[:, 0]) < (R[:, 0].min() + width_outer))) \ | (((R[:, 1]) > (R[:, 1].max() - width_outer)) | ((R[:, 1]) < (R[:, 1].min() + width_outer))) \ | (((R[:, 2]) > (R[:, 2].max() - width_outer)) | ((R[:, 2]) < (R[:, 2].min() + width_outer))) # now ignore the forces in the border region f[outer_layer] = np.nan # get center of force field Rcms = self.get_center(mode=center_mode) RR = R - Rcms #if r_max specified only use forces within this distance to cell for contractility if r_max: inner = np.linalg.norm(RR, axis=1) < r_max f = f[inner] RR = RR[inner] #mag = np.linalg.norm(f, axis=1) RRnorm = RR / np.linalg.norm(RR, axis=1)[:, None] contractility = np.nansum(np.einsum("ki,ki->k", RRnorm, f)) return contractility def plot(self, name="displacements", scale=None, vmin=None, vmax=None, cmap="turbo", export=None, camera_position=None, window_size=None, shape=None, pyvista_theme="document"): import pyvista as pv import matplotlib.pyplot as plt if export is not None: pv.set_plot_theme(pyvista_theme) if isinstance(name, str): name = [name] if shape is None: shape = (1, len(name)) plotter = pv.Plotter(off_screen=export is not None, shape=shape, window_size=window_size) for i, n in enumerate(name): plotter.subplot(i // shape[1], i % shape[1]) # if the scale is not defined use default values if scale is None: if n == "forces" or n == "forces_target": s = 3e4 else: s = 5 else: s = scale point_cloud = pv.PolyData(self.mesh.nodes) point_cloud.point_arrays[n] = getattr(self, n) point_cloud.point_arrays[n+"_mag"] = np.linalg.norm(getattr(self, n), axis=1) # generate the arrows arrows = point_cloud.glyph(orient=n, scale=n+"_mag", factor=s) print(n, s) # select the colormap, if "turbo" should be used but is not defined, use "jet" instead if cmap == "turbo": try: cmap = plt.get_cmap(cmap) except ValueError: cmap = "jet" # add the mesh plotter.add_mesh(point_cloud, colormap=cmap, scalars=n+"_mag") # color range if specified if vmin is not None and vmax is not None: plotter.add_mesh(arrows, colormap=cmap, name="arrows",clim=[vmin, vmax]) plotter.update_scalar_bar_range([vmin, vmax]) else: plotter.add_mesh(arrows, colormap=cmap, name="arrows") plotter.show_grid() plotter.link_views() if camera_position is not None: plotter.camera_position = camera_position campos = plotter.show(screenshot=export) return plotter, campos
# needs to stay here instead of top to prevent circular import from saenopy.result_file import Result def load_solver(filename: str) -> Solver: return Solver.load(filename) def load(filename: str) -> Result: from saenopy.gui.spheroid.modules.result import ResultSpheroid from saenopy.gui.tfm2d.modules.result import Result2D if filename.endswith(".saenopySpheroid"): return ResultSpheroid.load(filename) if filename.endswith(".saenopy2D"): return Result2D.load(filename) return Result.load(filename) def load_results(filename: str) -> List[Result]: import glob return [Result.load(file) for file in glob.glob(filename, recursive=True)] def subtract_reference_state(mesh_piv, mode): U = [M.displacements_measured for M in mesh_piv] # correct for the median position if len(U) > 2: xpos2 = np.cumsum(U, axis=0) if mode == "first": xpos2 -= xpos2[0] elif mode == "median": xpos2 -= np.nanmedian(xpos2, axis=0) elif mode == "last": xpos2 -= xpos2[-1] elif mode == "next": xpos2 = U else: xpos2 = U return xpos2 def get_cell_boundary(result, channel=1, thershold=20, smooth=2, element_size=14.00e-6, boundary=True, pos=None): from scipy.ndimage import gaussian_filter import matplotlib.pyplot as plt import numpy as np for i in range(len(result.stacks)): stack_deformed = result.stacks[i] voxel_size1 = stack_deformed.voxel_size im = stack_deformed[:, :, 0, :, channel] im = gaussian_filter(im, sigma=smooth, truncate=2.0) im_thresh = (im[:, :, :] > thershold).astype(np.uint8) from skimage.morphology import erosion if boundary: im_thresh = (im_thresh - erosion(im_thresh)).astype(bool) else: im_thresh = im_thresh.astype(bool) du, dv, dw = voxel_size1 u = im_thresh y, x, z = np.indices(u.shape) y, x, z = (y * stack_deformed.shape[0] * dv / u.shape[0] * 1e-6, x * stack_deformed.shape[1] * du / u.shape[1] * 1e-6, z * stack_deformed.shape[2] * dw / u.shape[2] * 1e-6) z -= np.max(z)/2 x -= np.max(x)/2 y -= np.max(y)/2 x = x[im_thresh] y = y[im_thresh] z = z[im_thresh] yxz = np.vstack([y, x, z]) dist_to_cell = np.min(np.linalg.norm(result.solvers[0].mesh.nodes[:, :, None] - yxz[None, :, :], axis=1), axis=1) included = dist_to_cell < element_size/2 result.solvers[i].mesh.cell_boundary_mask = included from saenopy.get_deformations import PivMesh def interpolate_mesh(mesh: PivMesh, xpos2: np.ndarray, params: dict) -> Solver: import saenopy from saenopy.multigrid_helper import get_scaled_mesh, get_nodes_with_one_face x, y, z = (mesh.nodes.max(axis=0) - mesh.nodes.min(axis=0)) * 1e6 if params["mesh_size"] == "piv": mesh_size = (x, y, z) else: mesh_size = params["mesh_size"] if mesh_size[0] < params["element_size"]*2 or \ mesh_size[1] < params["element_size"]*2 or \ mesh_size[2] < params["element_size"]*2: raise ValueError("Mesh size needs to be at least twice the element size.") points, cells = saenopy.multigrid_helper.get_scaled_mesh(params["element_size"] * 1e-6, params.get("inner_region", mesh_size[0]) * 1e-6, np.array(mesh_size) * 1e-6 / 2, [0, 0, 0], params.get("thinning_factor", 0)) R = (mesh.nodes - np.min(mesh.nodes, axis=0)) - (np.max(mesh.nodes, axis=0) - np.min(mesh.nodes, axis=0)) / 2 U_target = saenopy.get_deformations.interpolate_different_mesh(R, xpos2, points) border_idx = get_nodes_with_one_face(cells) inside_mask = np.ones(points.shape[0], dtype=bool) inside_mask[border_idx] = False M = saenopy.Solver() M.set_nodes(points) M.set_tetrahedra(cells) M.set_target_displacements(U_target, inside_mask) return M