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, TypedDict, Tuple
#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 Field:
def __init__(self, validators, default):
self.validators = validators
self.default = default
def __set_name__(self, owner, name):
self.private_name = '_' + name
setattr(owner, self.private_name, self.default)
def __get__(self, obj, objtype=None):
return getattr(obj, self.private_name)
def __set__(self, obj, value):
if self.validators is not None:
self.validators(obj, value)
setattr(obj, self.private_name, value)
def field(doc="", validators=None, default=None):
return Field(validators, default)
class SolverMesh(Mesh):
__save_parameters__ = ["nodes", "tetrahedra", "displacements", "forces",
"displacements_fixed", "displacements_target", "displacements_target_mask",
"forces_target", "forces_border", "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",
energy: np.ndarray = 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",
volume: np.ndarray = 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)
movable: np.ndarray = 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: np.ndarray = 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",
displacements: np.ndarray = 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_fixed: np.ndarray = 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: np.ndarray = field(validators=check_node_vector_field, default=None)
#displacements_target_mask: NDArray[Shape["N_c"], Bool] = field(validators=check_node_scalar_field, default=None)
displacements_target_mask: np.ndarray = field(validators=check_node_scalar_field, default=None)
#regularisation_mask: NDArray[Shape["N_c"], Bool] = field(validators=check_node_scalar_field, default=None)
regularisation_mask: np.ndarray = field(validators=check_node_scalar_field, default=None)
#cell_boundary_mask: NDArray[Shape["N_c"], Bool] = field(validators=check_node_scalar_field, default=None)
cell_boundary_mask: np.ndarray = 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",
forces: np.ndarray = 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",
forces_target: np.ndarray = field(doc="the external forces on each node, dimensions: N_c x 3",
validators=check_node_vector_field, default=None)
forces_border: np.ndarray = field(doc="the forces at the border (that are target my regularisation mask), 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
stiffness_tensor: np.ndarray = 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: np.ndarray = None
connections_valid = False
class RegularisationParameterDict(TypedDict):
step_size: float
solver_precision: float
max_iterations: int
rel_conv_crit: float
alpha: float
method: str
[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
s: np.ndarray = 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: np.ndarray = None
regularisation_parameters: RegularisationParameterDict = None
verbose = False
preprocessing = None
def __init__(self, **kwargs):
self.mesh = SolverMesh()
super().__init__(**kwargs)
#def set_nodes(self, data: NDArray[Shape["N_c, 3"], Float]):
[docs] def set_nodes(self, data: np.ndarray):
"""
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))
#def set_boundary_condition(self, displacements: NDArray[Shape["N_c, 3"], Float] = None,
# forces: NDArray[Shape["N_c, 3"], Float] = None):
[docs] def set_boundary_condition(self, displacements: np.ndarray = None,
forces: np.ndarray = 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)
#def set_initial_displacements(self, displacements: NDArray[Shape["N_c, 3"], Float]):
[docs] def set_initial_displacements(self, displacements: np.ndarray):
"""
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]):
def _set_external_forces(self, forces: np.ndarray):
"""
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)
#def set_tetrahedra(self, data: NDArray[Shape["N_t, 4"], Int]):
[docs] def set_tetrahedra(self, data: np.ndarray):
"""
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()
#def set_beams(self, beams: Union[int, NDArray[Shape["N_b, 3"], Float]] = 300):
[docs] def set_beams(self, beams: Union[int, np.ndarray] = 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))
#def get_max_tet_stiffness(self) -> NDArray[Shape["N_t"], Float]:
[docs] def get_max_tet_stiffness(self) -> np.ndarray:
"""
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]:
def _get_s_bar(self, t: slice) -> np.ndarray:
# 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 """
#def set_target_displacements(self, displacement: NDArray[Shape["N_c, 3"], Float], reg_mask: NDArray[Shape["N_c"], Bool] = None):
[docs] def set_target_displacements(self, displacement: np.ndarray, reg_mask: np.ndarray = 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 + alpha*|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, cancel_signal= 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(int(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
if cancel_signal is not None and getattr(cancel_signal, "cancel", False):
self.regularisation_results = relrec
return relrec
self.regularisation_results = np.asarray(relrec)
self.mesh.forces_border = self.mesh.forces.copy()
self.mesh.forces_border[self.mesh.regularisation_mask] = 0
self.mesh.forces[~self.mesh.regularisation_mask] = 0
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]:
def get_center(self, mode="force", border=None) -> np.ndarray:
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
if isinstance(center_mode, str):
Rcms = self.get_center(mode=center_mode)
else:
Rcms = 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
from saenopy.gui.orientation.modules.result import ResultOrientation
if str(filename).endswith(".saenopySpheroid"):
return ResultSpheroid.load(filename)
if str(filename).endswith(".saenopy2D"):
return Result2D.load(filename)
if str(filename).endswith(".saenopyOrientation"):
return ResultOrientation.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 different modes
if len(U) > 2:
xpos2 = U
if mode == "cumul.":
xpos2 = np.cumsum(U, axis=0)
elif mode == "median":
xpos2 = np.cumsum(U, axis=0)
xpos2 -= np.nanmedian(xpos2, axis=0)
elif mode == "last":
xpos2 = np.cumsum(U, axis=0)
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