1use super::{vec_dyad_vec, Mandel, Tensor2, SQRT_2, SQRT_3, SQRT_6};
2use crate::StrError;
3use russell_lab::{mat_eigen_sym_jacobi, Matrix, Vector};
4
5pub struct Spectral2 {
7 mandel: Mandel,
9
10 pub lambda: Vector,
12
13 pub projectors: Vec<Tensor2>,
15}
16
17impl Spectral2 {
18 pub fn new(two_dim: bool) -> Self {
22 let mandel = if two_dim {
23 Mandel::Symmetric2D
24 } else {
25 Mandel::Symmetric
26 };
27 Spectral2 {
28 mandel,
29 lambda: Vector::new(3),
30 projectors: vec![Tensor2::new(mandel), Tensor2::new(mandel), Tensor2::new(mandel)],
31 }
32 }
33
34 pub fn decompose(&mut self, tt: &Tensor2) -> Result<(), StrError> {
40 if tt.mandel != self.mandel {
41 return Err("the mandel representation is incompatible");
42 }
43 let dim = tt.vec.dim();
44 if dim == 4 {
45 let (t22, mut a) = tt.as_matrix_2d();
47 let mut l = Vector::new(2);
48 let mut v = Matrix::new(2, 2);
49 mat_eigen_sym_jacobi(&mut l, &mut v, &mut a)?;
50 self.lambda[0] = l[0];
51 self.lambda[1] = l[1];
52 self.lambda[2] = t22;
53
54 let u0 = Vector::from(&[v.get(0, 0), v.get(1, 0)]);
56 let u1 = Vector::from(&[v.get(0, 1), v.get(1, 1)]);
57
58 vec_dyad_vec(&mut self.projectors[0], 1.0, &u0, &u0).unwrap();
60 vec_dyad_vec(&mut self.projectors[1], 1.0, &u1, &u1).unwrap();
61 self.projectors[2].clear();
62 self.projectors[2].vec[2] = 1.0;
63 } else {
64 let mut a = tt.as_matrix();
66 let mut v = Matrix::new(3, 3);
67 mat_eigen_sym_jacobi(&mut self.lambda, &mut v, &mut a)?;
68
69 let u0 = Vector::from(&[v.get(0, 0), v.get(1, 0), v.get(2, 0)]);
71 let u1 = Vector::from(&[v.get(0, 1), v.get(1, 1), v.get(2, 1)]);
72 let u2 = Vector::from(&[v.get(0, 2), v.get(1, 2), v.get(2, 2)]);
73
74 vec_dyad_vec(&mut self.projectors[0], 1.0, &u0, &u0).unwrap();
76 vec_dyad_vec(&mut self.projectors[1], 1.0, &u1, &u1).unwrap();
77 vec_dyad_vec(&mut self.projectors[2], 1.0, &u2, &u2).unwrap();
78 }
79 Ok(())
80 }
81
82 pub fn compose(&self, composed: &mut Tensor2, lambda: &Vector) -> Result<(), StrError> {
84 if composed.mandel != self.mandel {
85 return Err("the mandel representation is incompatible");
86 }
87 if lambda.dim() != 3 {
88 return Err("lambda.dim must be equal to 3");
89 }
90 let n = self.projectors[0].vec.dim();
91 for i in 0..n {
92 composed.vec[i] = lambda[0] * self.projectors[0].vec[i]
93 + lambda[1] * self.projectors[1].vec[i]
94 + lambda[2] * self.projectors[2].vec[i];
95 }
96 Ok(())
97 }
98
99 pub fn octahedral_basis(&self) -> (f64, f64, f64) {
103 let (s1, s2, s3) = (self.lambda[0], self.lambda[1], self.lambda[2]);
104 let ls1 = (2.0 * s1 - s2 - s3) / SQRT_6;
105 let ls2 = (s1 + s2 + s3) / SQRT_3;
106 let ls3 = (s3 - s2) / SQRT_2;
107 (ls1, ls2, ls3)
108 }
109}
110
111#[cfg(test)]
114mod tests {
115 use super::Spectral2;
116 use crate::{Mandel, SampleTensor2, SamplesTensor2, Tensor2, SQRT_3, SQRT_3_BY_2};
117 use russell_lab::{approx_eq, mat_approx_eq, vec_approx_eq, Matrix, Vector};
118
119 #[test]
120 fn decompose_captures_errors() {
121 let mut spec = Spectral2::new(false);
122 let tt = Tensor2::new(Mandel::General);
123 assert_eq!(
124 spec.decompose(&tt).err(),
125 Some("the mandel representation is incompatible")
126 );
127 }
128
129 #[test]
130 fn compose_capture_errors() {
131 let spec = Spectral2::new(false);
132 let mut tt = Tensor2::new(Mandel::Symmetric2D);
133 assert_eq!(
134 spec.compose(&mut tt, &spec.lambda).err(),
135 Some("the mandel representation is incompatible")
136 );
137 let mut tt = Tensor2::new(Mandel::Symmetric);
138 let lambda = Vector::new(1);
139 assert_eq!(
140 spec.compose(&mut tt, &lambda).err(),
141 Some("lambda.dim must be equal to 3")
142 );
143 }
144
145 fn check(spec: &mut Spectral2, sample: &SampleTensor2, tol_lambda: f64, tol_proj: f64, tol_spectral: f64) {
146 let correct_lambda = sample.eigenvalues.unwrap();
147 let correct_projectors = sample.eigenprojectors.unwrap();
148
149 let mandel = spec.projectors[0].mandel;
151 let tt = Tensor2::from_matrix(&sample.matrix, mandel).unwrap();
152 spec.decompose(&tt).unwrap();
153
154 vec_approx_eq(&spec.lambda, &correct_lambda, tol_lambda);
163
164 let pp0 = spec.projectors[0].as_matrix();
166 let pp1 = spec.projectors[1].as_matrix();
167 let pp2 = spec.projectors[2].as_matrix();
168 let correct0 = Matrix::from(&correct_projectors[0]);
169 let correct1 = Matrix::from(&correct_projectors[1]);
170 let correct2 = Matrix::from(&correct_projectors[2]);
171 mat_approx_eq(&correct0, &pp0, tol_proj);
172 mat_approx_eq(&correct1, &pp1, tol_proj);
173 mat_approx_eq(&correct2, &pp2, tol_proj);
174
175 let mut tt_new = Tensor2::new(mandel);
177 spec.compose(&mut tt_new, &spec.lambda).unwrap();
178 let a_new = tt_new.as_matrix();
179 let a = Matrix::from(&sample.matrix);
180 mat_approx_eq(&a, &a_new, tol_spectral);
181 }
182
183 #[test]
184 fn decompose_and_compose_work_3d() {
185 let mut spec = Spectral2::new(false);
186 check(&mut spec, &SamplesTensor2::TENSOR_O, 1e-15, 1e-15, 1e-15);
187 check(&mut spec, &SamplesTensor2::TENSOR_I, 1e-15, 1e-15, 1e-15);
188 check(&mut spec, &SamplesTensor2::TENSOR_X, 1e-15, 1e-15, 1e-15);
189 check(&mut spec, &SamplesTensor2::TENSOR_Y, 1e-13, 1e-15, 1e-15);
190 check(&mut spec, &SamplesTensor2::TENSOR_Z, 1e-14, 1e-15, 1e-15);
191 check(&mut spec, &SamplesTensor2::TENSOR_U, 1e-13, 1e-15, 1e-14);
192 check(&mut spec, &SamplesTensor2::TENSOR_S, 1e-13, 1e-14, 1e-14);
193 }
194
195 #[test]
196 fn decompose_and_compose_work_2d() {
197 let mut spec = Spectral2::new(true);
198 check(&mut spec, &SamplesTensor2::TENSOR_O, 1e-15, 1e-15, 1e-15);
199 check(&mut spec, &SamplesTensor2::TENSOR_I, 1e-15, 1e-15, 1e-15);
200 check(&mut spec, &SamplesTensor2::TENSOR_X, 1e-15, 1e-15, 1e-15);
201 check(&mut spec, &SamplesTensor2::TENSOR_Y, 1e-13, 1e-15, 1e-15);
202 check(&mut spec, &SamplesTensor2::TENSOR_Z, 1e-14, 1e-15, 1e-15);
203 }
204
205 #[test]
206 fn octahedral_basis_works() {
207 #[rustfmt::skip]
209 let principal_stresses_and_lode = [
210 ( 3.0 , 0.0 , 0.0 , 1.0 ),
211 ( 0.0 , 3.0 , 0.0 , 1.0 ),
212 ( 0.0 , 0.0 , 3.0 , 1.0 ),
213 ( 1.0 + SQRT_3 , 1.0 - SQRT_3 , 1.0 , 0.0 ),
214 ( 1.0 + SQRT_3 , 1.0 , 1.0 - SQRT_3 , 0.0 ),
215 ( 1.0 , 1.0 + SQRT_3 , 1.0 - SQRT_3 , 0.0 ),
216 ( 1.0 - SQRT_3 , 1.0 + SQRT_3 , 1.0 , 0.0 ),
217 ( 1.0 , 1.0 - SQRT_3 , 1.0 + SQRT_3 , 0.0 ),
218 ( 1.0 - SQRT_3 , 1.0 , 1.0 + SQRT_3 , 0.0 ),
219 ( 2.0 , -1.0 , 2.0 , -1.0 ),
220 ( 2.0 , 2.0 , -1.0 , -1.0 ),
221 (-1.0 , 2.0 , 2.0 , -1.0 ),
222 ];
223 let two_dim = true;
224 let mut spec = Spectral2::new(two_dim);
225 let mut tt = Tensor2::new_sym(two_dim);
226 for (sigma_1, sigma_2, sigma_3, lode_correct) in &principal_stresses_and_lode {
227 tt.vec[0] = *sigma_1;
228 tt.vec[1] = *sigma_2;
229 tt.vec[2] = *sigma_3;
230 spec.decompose(&tt).unwrap();
231 let (ls1, ls2, ls3) = spec.octahedral_basis();
232 let radius = f64::sqrt(ls3 * ls3 + ls1 * ls1);
233 let distance = ls2;
234 approx_eq(distance / SQRT_3, 1.0, 1e-15);
235 approx_eq(radius * SQRT_3_BY_2, 3.0, 1e-15);
236 if radius > 0.0 {
237 let cos_theta = ls1 / radius;
238 let lode = 4.0 * f64::powf(cos_theta, 3.0) - 3.0 * cos_theta;
239 approx_eq(lode, *lode_correct, 1e-15);
240 }
241 }
242 }
243}