Skip to main content

conspire/domain/fem/block/element/solid/elastic/internal_variables/
mod.rs

1use crate::math::{Erase, Quantity};
2use crate::units::{Dimensionless, Stress, UnitDiv};
3use std::ops::{Div, Mul};
4
5use crate::{
6    constitutive::{ConstitutiveError, solid::elastic::internal_variables::ElasticIV},
7    fem::block::element::{
8        Element, ElementNodalCoordinates, FiniteElement, FiniteElementError, GradientVectors,
9        IntegrationWeights,
10        solid::{ElementNodalForcesSolid, ElementNodalStiffnessesSolid, SolidFiniteElement},
11    },
12    math::{
13        ContractSecondFourthWithFirst, HessianBlock, Jacobian, Matrix, Scalar, Solution,
14        SquareMatrix, Tensor, TensorList, Vector,
15        optimize::{EqualityConstraint, FirstOrderRootFinding, NewtonRaphson},
16    },
17    mechanics::{
18        DeformationGradient, FirstPiolaKirchhoffStress, FirstPiolaKirchhoffStressList,
19        FirstPiolaKirchhoffTangentStiffness, FirstPiolaKirchhoffTangentStiffnessList,
20    },
21    units::Volume,
22};
23
24/// The indices of the internal variables that are free to move.
25fn free_indices<C, V>(constitutive_model: &C, size: usize) -> Vec<usize>
26where
27    C: ElasticIV<V>,
28{
29    let mut free = vec![true; size];
30    constitutive_model
31        .internal_variables_fixed()
32        .iter()
33        .for_each(|&index| free[index] = false);
34    (0..size).filter(|&index| free[index]).collect()
35}
36
37/// The internal variables held at each integration point of an element.
38pub type InternalVariables<const G: usize, V> = TensorList<V, G>;
39
40pub trait ElasticIVFiniteElement<
41    C,
42    const G: usize,
43    const M: usize,
44    const N: usize,
45    const P: usize,
46    V,
47    E,
48> where
49    C: ElasticIV<V>,
50    C::Residual: Erase<Erased = E>,
51    Self: SolidFiniteElement<G, M, N, P>,
52    V: Erase<Erased = E> + Jacobian + Solution,
53    <V as Tensor>::Unit: UnitDiv<<V as Tensor>::Unit, Output = Dimensionless>,
54    E: Tensor,
55    for<'a> &'a C::Residual: Div<C::TangentVv, Output = V>,
56    for<'a> &'a V: Mul<Quantity<Dimensionless>, Output = V> + Mul<Scalar, Output = V>,
57    for<'a> &'a Matrix: Mul<&'a V, Output = Vector>,
58{
59    /// The internal variables an element starts from at every integration point.
60    fn internal_variables_initial(&self, constitutive_model: &C) -> InternalVariables<G, V>;
61    /// Solves the internal variables at every integration point, holding the
62    /// deformation fixed.
63    ///
64    /// The integration points are independent of one another, the internal
65    /// variables of one never entering the residual of another.
66    fn internal_variables_root(
67        &self,
68        local_solver: &NewtonRaphson,
69        constitutive_model: &C,
70        nodal_coordinates: &ElementNodalCoordinates<N>,
71        internal_variables: &InternalVariables<G, V>,
72    ) -> Result<InternalVariables<G, V>, FiniteElementError>;
73    /// Steps the internal variables alongside a decrement of the nodal
74    /// coordinates, rather than solving them where the coordinates now are.
75    fn internal_variables_increment(
76        &self,
77        constitutive_model: &C,
78        nodal_coordinates: &ElementNodalCoordinates<N>,
79        internal_variables: &InternalVariables<G, V>,
80        nodal_decrement: &ElementNodalCoordinates<N>,
81        step: Scalar,
82    ) -> Result<InternalVariables<G, V>, FiniteElementError>;
83    fn nodal_forces(
84        &self,
85        constitutive_model: &C,
86        nodal_coordinates: &ElementNodalCoordinates<N>,
87        internal_variables: &InternalVariables<G, V>,
88    ) -> Result<ElementNodalForcesSolid<N>, FiniteElementError>;
89    /// The nodal forces with the residual of the internal variables eliminated
90    /// into them, for when they are carried rather than solved.
91    fn nodal_forces_eliminated(
92        &self,
93        constitutive_model: &C,
94        nodal_coordinates: &ElementNodalCoordinates<N>,
95        internal_variables: &InternalVariables<G, V>,
96    ) -> Result<ElementNodalForcesSolid<N>, FiniteElementError>;
97    /// The tangent stiffnesses with the internal variables condensed out.
98    ///
99    /// ```math
100    /// \mathcal{C} = \mathcal{K}_{uu} - \mathcal{K}_{uv}\mathcal{K}_{vv}^{-1}\mathcal{K}_{vu}
101    /// ```
102    fn nodal_stiffnesses(
103        &self,
104        constitutive_model: &C,
105        nodal_coordinates: &ElementNodalCoordinates<N>,
106        internal_variables: &InternalVariables<G, V>,
107    ) -> Result<ElementNodalStiffnessesSolid<N>, FiniteElementError>;
108}
109
110/// Solves the internal variables at one integration point.
111///
112/// The gauge freedom is fixed at the initial values, which are zero over those
113/// indices, so the constraint is imposed by leaving them out of the system
114/// rather than by a multiplier.
115fn root_at_point<C, V, E>(
116    local_solver: &NewtonRaphson,
117    constitutive_model: &C,
118    deformation_gradient: &DeformationGradient,
119    internal_variables: &V,
120) -> Result<V, ConstitutiveError>
121where
122    C: ElasticIV<V>,
123    C::Residual: Erase<Erased = E>,
124    V: Erase<Erased = E> + Jacobian + Solution,
125    <V as Tensor>::Unit: UnitDiv<<V as Tensor>::Unit, Output = Dimensionless>,
126    E: Tensor,
127    for<'a> &'a C::Residual: Div<C::TangentVv, Output = V>,
128    for<'a> &'a V: Mul<Quantity<Dimensionless>, Output = V> + Mul<Scalar, Output = V>,
129    for<'a> &'a Matrix: Mul<&'a V, Output = Vector>,
130{
131    local_solver
132        .root(
133            |root: &V| {
134                Ok(constitutive_model.internal_variables_residual(deformation_gradient, root)?)
135            },
136            |root: &V| Ok(constitutive_model.tangents(deformation_gradient, root)?.3),
137            internal_variables.clone(),
138            EqualityConstraint::Fixed(constitutive_model.internal_variables_fixed().to_vec()),
139            None,
140        )
141        .map_err(|error| ConstitutiveError::custom(error, deformation_gradient))
142}
143
144/// The nodal forces a list of stresses integrates to.
145fn assemble_forces<const G: usize, const N: usize>(
146    stresses: FirstPiolaKirchhoffStressList<G>,
147    gradient_vectors: &GradientVectors<3, G, N>,
148    integration_weights: &IntegrationWeights<G, Volume>,
149) -> ElementNodalForcesSolid<N> {
150    stresses
151        .iter()
152        .zip(gradient_vectors.iter().zip(integration_weights))
153        .map(|(stress, (gradient_vectors_point, integration_weight))| {
154            gradient_vectors_point
155                .iter()
156                .map(|gradient_vector| (stress * gradient_vector) * integration_weight)
157                .collect()
158        })
159        .sum()
160}
161
162/// The residual of the internal variables at one integration point, laid flat.
163fn local_residual<C, V>(
164    constitutive_model: &C,
165    deformation_gradient: &DeformationGradient,
166    internal_variables: &V,
167) -> Result<Vector, ConstitutiveError>
168where
169    C: ElasticIV<V>,
170    V: Jacobian + Solution,
171{
172    let mut residual = Vector::zero(internal_variables.size());
173    constitutive_model
174        .internal_variables_residual(deformation_gradient, internal_variables)?
175        .fill_into(&mut residual);
176    Ok(residual)
177}
178
179/// The local block over the free internal variables, the fixed ones leaving it
180/// through their rows and columns rather than through the inverse.
181fn local_block<K>(tangent_vv: &K, size: usize, unmap: &[usize]) -> SquareMatrix
182where
183    K: HessianBlock,
184{
185    let mut block = SquareMatrix::zero(size);
186    tangent_vv.fill_into_block(&mut block, 0, 0);
187    let mut local = SquareMatrix::zero(unmap.len());
188    unmap.iter().enumerate().for_each(|(a, &i)| {
189        unmap
190            .iter()
191            .enumerate()
192            .for_each(|(b, &j)| local[a][b] = block[i][j])
193    });
194    local
195}
196
197/// The stress at one integration point with the residual of the internal
198/// variables eliminated into it.
199///
200/// ```math
201/// \mathbf{P} - \mathcal{K}_{uv}\mathcal{K}_{vv}^{-1}\mathbf{r}_v
202/// ```
203///
204/// The internal variables are carried rather than solved, so this residual
205/// only agrees with the stress alone once they have converged.
206fn eliminated_at_point<C, V>(
207    constitutive_model: &C,
208    deformation_gradient: &DeformationGradient,
209    internal_variables: &V,
210) -> Result<FirstPiolaKirchhoffStress, ConstitutiveError>
211where
212    C: ElasticIV<V>,
213    V: Jacobian + Solution,
214{
215    let mut stress = constitutive_model
216        .first_piola_kirchhoff_stress(deformation_gradient, internal_variables)?;
217    let size = internal_variables.size();
218    let unmap = free_indices(constitutive_model, size);
219    let residual = local_residual(constitutive_model, deformation_gradient, internal_variables)?;
220    let mut reduced = Vector::zero(unmap.len());
221    unmap
222        .iter()
223        .enumerate()
224        .for_each(|(a, &i)| reduced[a] = residual[i]);
225    let (_, _, tangent_uv, tangent_vv) =
226        constitutive_model.tangents(deformation_gradient, internal_variables)?;
227    let eliminated = local_block(&tangent_vv, size, &unmap)
228        .solve_lu(&reduced)
229        .map_err(|error| ConstitutiveError::custom(error, deformation_gradient))?;
230    let mut cross = SquareMatrix::zero(size);
231    tangent_uv.fill_into_block(&mut cross, 0, 0);
232    (0..3).for_each(|i| {
233        (0..3).for_each(|j| {
234            stress[i][j] -= unmap
235                .iter()
236                .enumerate()
237                .map(|(a, &v)| Quantity::new(cross[3 * i + j][v] * eliminated[a]))
238                .sum::<Quantity<Stress>>()
239        })
240    });
241    Ok(stress)
242}
243
244/// The internal variables at one integration point stepped alongside a
245/// decrement of the deformation.
246///
247/// ```math
248/// \Delta\mathbf{v} = -\mathcal{K}_{vv}^{-1}\left(\mathbf{r}_v + \mathcal{K}_{vu}\Delta\mathbf{u}\right)
249/// ```
250fn increment_at_point<C, V>(
251    constitutive_model: &C,
252    deformation_gradient: &DeformationGradient,
253    deformation_gradient_decrement: &DeformationGradient,
254    internal_variables: &V,
255    step: Scalar,
256) -> Result<V, ConstitutiveError>
257where
258    C: ElasticIV<V>,
259    V: Jacobian + Solution,
260{
261    let size = internal_variables.size();
262    let unmap = free_indices(constitutive_model, size);
263    let residual = local_residual(constitutive_model, deformation_gradient, internal_variables)?;
264    let (_, tangent_vu, _, tangent_vv) =
265        constitutive_model.tangents(deformation_gradient, internal_variables)?;
266    let mut coupling = SquareMatrix::zero(size);
267    tangent_vu.fill_into_block(&mut coupling, 0, 0);
268    //
269    // The deformation is handed over as a decrement, so its contribution to
270    // the local residual enters with the opposite sign.
271    //
272    let mut reduced = Vector::zero(unmap.len());
273    unmap.iter().enumerate().for_each(|(a, &i)| {
274        reduced[a] = residual[i]
275            - (0..3)
276                .map(|k| {
277                    (0..3)
278                        .map(|l| coupling[i][3 * k + l] * deformation_gradient_decrement[k][l])
279                        .sum::<Quantity>()
280                })
281                .sum::<Quantity>()
282                .value()
283    });
284    let solution = local_block(&tangent_vv, size, &unmap)
285        .solve_lu(&reduced)
286        .map_err(|error| ConstitutiveError::custom(error, deformation_gradient))?;
287    let mut decrement = Vector::zero(size);
288    unmap
289        .iter()
290        .enumerate()
291        .for_each(|(a, &i)| decrement[i] = solution[a] * step);
292    //
293    // Eliminating solves one direction for these and the nodal coordinates
294    // together, so a shortened step is the same fraction of both. Solving
295    // against an already shortened decrement would be a different direction
296    // instead of less of this one, and would leave these where the whole of
297    // their own residual had been taken out even where nothing else moved.
298    //
299    let mut incremented = internal_variables.clone();
300    incremented.decrement_from(&decrement);
301    Ok(incremented)
302}
303
304/// The tangent stiffness at one integration point with the internal variables
305/// condensed out.
306fn condensed_at_point<C, V>(
307    constitutive_model: &C,
308    deformation_gradient: &DeformationGradient,
309    internal_variables: &V,
310) -> Result<FirstPiolaKirchhoffTangentStiffness, ConstitutiveError>
311where
312    C: ElasticIV<V>,
313    V: Tensor,
314{
315    let (tangent_uu, tangent_vu, tangent_uv, tangent_vv) =
316        constitutive_model.tangents(deformation_gradient, internal_variables)?;
317    let size = internal_variables.size();
318    let unmap = free_indices(constitutive_model, size);
319    let factorization = local_block(&tangent_vv, size, &unmap)
320        .factorize_lu()
321        .map_err(|error| ConstitutiveError::custom(error, deformation_gradient))?;
322    let mut coupling = SquareMatrix::zero(size);
323    tangent_vu.fill_into_block(&mut coupling, 0, 0);
324    let mut cross = SquareMatrix::zero(size);
325    tangent_uv.fill_into_block(&mut cross, 0, 0);
326    let mut column = Vector::zero(unmap.len());
327    let mut eliminated = vec![Vector::zero(unmap.len()); size];
328    (0..size).for_each(|c| {
329        unmap
330            .iter()
331            .enumerate()
332            .for_each(|(a, &i)| column[a] = coupling[i][c]);
333        factorization.solve_into(&column, &mut eliminated[c])
334    });
335    let mut condensed = tangent_uu;
336    (0..3).for_each(|i| {
337        (0..3).for_each(|j| {
338            (0..3).for_each(|k| {
339                (0..3).for_each(|l| {
340                    condensed[i][j][k][l] -= unmap
341                        .iter()
342                        .enumerate()
343                        .map(|(a, &v)| {
344                            Quantity::new(cross[3 * i + j][v] * eliminated[3 * k + l][a])
345                        })
346                        .sum::<Quantity<Stress>>()
347                })
348            })
349        })
350    });
351    Ok(condensed)
352}
353
354impl<C, const G: usize, const N: usize, const O: usize, const P: usize, V, E>
355    ElasticIVFiniteElement<C, G, 3, N, P, V, E> for Element<3, G, N, O>
356where
357    C: ElasticIV<V>,
358    C::Residual: Erase<Erased = E>,
359    Self: SolidFiniteElement<G, 3, N, P>,
360    V: Erase<Erased = E> + Jacobian + Solution,
361    <V as Tensor>::Unit: UnitDiv<<V as Tensor>::Unit, Output = Dimensionless>,
362    E: Tensor,
363    for<'a> &'a C::Residual: Div<C::TangentVv, Output = V>,
364    for<'a> &'a V: Mul<Quantity<Dimensionless>, Output = V> + Mul<Scalar, Output = V>,
365    for<'a> &'a Matrix: Mul<&'a V, Output = Vector>,
366{
367    fn internal_variables_initial(&self, constitutive_model: &C) -> InternalVariables<G, V> {
368        std::array::from_fn(|_| constitutive_model.internal_variables_initial()).into()
369    }
370    fn internal_variables_root(
371        &self,
372        local_solver: &NewtonRaphson,
373        constitutive_model: &C,
374        nodal_coordinates: &ElementNodalCoordinates<N>,
375        internal_variables: &InternalVariables<G, V>,
376    ) -> Result<InternalVariables<G, V>, FiniteElementError> {
377        self.deformation_gradients(nodal_coordinates)
378            .iter()
379            .zip(internal_variables)
380            .map(|(deformation_gradient, internal_variables_point)| {
381                root_at_point(
382                    local_solver,
383                    constitutive_model,
384                    deformation_gradient,
385                    internal_variables_point,
386                )
387            })
388            .collect::<Result<InternalVariables<G, V>, _>>()
389            .map_err(|error| FiniteElementError::upstream(error, self))
390    }
391    fn internal_variables_increment(
392        &self,
393        constitutive_model: &C,
394        nodal_coordinates: &ElementNodalCoordinates<N>,
395        internal_variables: &InternalVariables<G, V>,
396        nodal_decrement: &ElementNodalCoordinates<N>,
397        step: Scalar,
398    ) -> Result<InternalVariables<G, V>, FiniteElementError> {
399        self.deformation_gradients(nodal_coordinates)
400            .iter()
401            .zip(self.deformation_gradients(nodal_decrement).iter())
402            .zip(internal_variables)
403            .map(
404                |((deformation_gradient, decrement), internal_variables_point)| {
405                    increment_at_point(
406                        constitutive_model,
407                        deformation_gradient,
408                        decrement,
409                        internal_variables_point,
410                        step,
411                    )
412                },
413            )
414            .collect::<Result<InternalVariables<G, V>, _>>()
415            .map_err(|error| FiniteElementError::upstream(error, self))
416    }
417    fn nodal_forces(
418        &self,
419        constitutive_model: &C,
420        nodal_coordinates: &ElementNodalCoordinates<N>,
421        internal_variables: &InternalVariables<G, V>,
422    ) -> Result<ElementNodalForcesSolid<N>, FiniteElementError> {
423        let stresses = self
424            .deformation_gradients(nodal_coordinates)
425            .iter()
426            .zip(internal_variables)
427            .map(|(deformation_gradient, internal_variables_point)| {
428                constitutive_model
429                    .first_piola_kirchhoff_stress(deformation_gradient, internal_variables_point)
430            })
431            .collect::<Result<FirstPiolaKirchhoffStressList<G>, _>>()
432            .map_err(|error| FiniteElementError::upstream(error, self))?;
433        Ok(assemble_forces(
434            stresses,
435            self.gradient_vectors(),
436            self.integration_weights(),
437        ))
438    }
439    fn nodal_forces_eliminated(
440        &self,
441        constitutive_model: &C,
442        nodal_coordinates: &ElementNodalCoordinates<N>,
443        internal_variables: &InternalVariables<G, V>,
444    ) -> Result<ElementNodalForcesSolid<N>, FiniteElementError> {
445        let stresses = self
446            .deformation_gradients(nodal_coordinates)
447            .iter()
448            .zip(internal_variables)
449            .map(|(deformation_gradient, internal_variables_point)| {
450                eliminated_at_point(
451                    constitutive_model,
452                    deformation_gradient,
453                    internal_variables_point,
454                )
455            })
456            .collect::<Result<FirstPiolaKirchhoffStressList<G>, _>>()
457            .map_err(|error| FiniteElementError::upstream(error, self))?;
458        Ok(assemble_forces(
459            stresses,
460            self.gradient_vectors(),
461            self.integration_weights(),
462        ))
463    }
464    fn nodal_stiffnesses(
465        &self,
466        constitutive_model: &C,
467        nodal_coordinates: &ElementNodalCoordinates<N>,
468        internal_variables: &InternalVariables<G, V>,
469    ) -> Result<ElementNodalStiffnessesSolid<N>, FiniteElementError> {
470        let condensed = self
471            .deformation_gradients(nodal_coordinates)
472            .iter()
473            .zip(internal_variables)
474            .map(|(deformation_gradient, internal_variables_point)| {
475                condensed_at_point(
476                    constitutive_model,
477                    deformation_gradient,
478                    internal_variables_point,
479                )
480            })
481            .collect::<Result<FirstPiolaKirchhoffTangentStiffnessList<G>, _>>()
482            .map_err(|error| FiniteElementError::upstream(error, self))?;
483        Ok(condensed
484            .iter()
485            .zip(
486                self.gradient_vectors()
487                    .iter()
488                    .zip(self.integration_weights()),
489            )
490            .map(|(tangent, (gradient_vectors, integration_weight))| {
491                gradient_vectors
492                    .iter()
493                    .map(|gradient_vector_a| {
494                        gradient_vectors
495                            .iter()
496                            .map(|gradient_vector_b| {
497                                tangent.contract_second_fourth_with_first(
498                                    gradient_vector_a,
499                                    gradient_vector_b,
500                                ) * integration_weight
501                            })
502                            .collect()
503                    })
504                    .collect()
505            })
506            .sum())
507    }
508}