Skip to main content

faer/linalg/evd/
tridiag.rs

1use crate::assert;
2use crate::internal_prelude::*;
3use crate::utils::cost::{ParThreshold, rate};
4use crate::utils::thread::with_width;
5use linalg::householder;
6use linalg::matmul::triangular::BlockStructure;
7use linalg::matmul::{self, dot};
8/// tridiagonalization tuning parameters
9#[derive(Copy, Clone, Debug)]
10pub struct TridiagParams {
11	/// floor of the parallel regions: [`ParThreshold::Derived`] (the default) runs a step in
12	/// parallel when its serial time pays for one section; [`ParThreshold::Fixed`] counts
13	/// elements `rem*rem/2` of the triangle
14	pub par_threshold: ParThreshold,
15	#[doc(hidden)]
16	pub non_exhaustive: NonExhaustive,
17}
18impl<T: ComplexField> Auto<T> for TridiagParams {
19	fn auto() -> Self {
20		Self {
21			par_threshold: ParThreshold::Derived,
22			non_exhaustive: NonExhaustive(()),
23		}
24	}
25}
26/// computes the layout of the workspace required to compute a self-adjoint
27/// matrix's tridiagonalization
28pub fn tridiag_in_place_scratch<T: ComplexField>(
29	dim: usize,
30	par: ParShape,
31	params: Spec<TridiagParams, T>,
32) -> StackReq {
33	_ = par;
34	_ = params;
35	StackReq::all_of(&[
36		temp_mat_scratch::<T>(dim, 1).array(2),
37		temp_mat_scratch::<T>(dim, par.degree()),
38	])
39}
40fn tridiag_fused_op_simd<T: ComplexField>(
41	A: MatMut<'_, T, usize, usize, ContiguousFwd>,
42	y2: ColMut<'_, T, usize>,
43	z2: ColMut<'_, T, usize, ContiguousFwd>,
44	ry2: ColRef<'_, T, usize>,
45	rz2: ColRef<'_, T, usize, ContiguousFwd>,
46	u0: ColRef<'_, T, usize, ContiguousFwd>,
47	u1: ColRef<'_, T, usize>,
48	u2: ColRef<'_, T, usize>,
49	v2: ColRef<'_, T, usize, ContiguousFwd>,
50	f: T,
51	align: usize,
52) {
53	struct Impl<'a, 'M, 'N, T: ComplexField> {
54		A: MatMut<'a, T, Dim<'M>, Dim<'N>, ContiguousFwd>,
55		y2: ColMut<'a, T, Dim<'N>>,
56		z2: ColMut<'a, T, Dim<'M>, ContiguousFwd>,
57		ry2: ColRef<'a, T, Dim<'N>>,
58		rz2: ColRef<'a, T, Dim<'M>, ContiguousFwd>,
59		u0: ColRef<'a, T, Dim<'M>, ContiguousFwd>,
60		u1: ColRef<'a, T, Dim<'N>>,
61		u2: ColRef<'a, T, Dim<'N>>,
62		v2: ColRef<'a, T, Dim<'M>, ContiguousFwd>,
63		f: T,
64		align: usize,
65	}
66	impl<'a, 'M, 'N, T: ComplexField> pulp::WithSimd for Impl<'a, 'M, 'N, T> {
67		type Output = ();
68
69		#[inline(always)]
70		fn with_simd<S: pulp::Simd>(self, simd: S) -> Self::Output {
71			let Self {
72				mut A,
73				mut y2,
74				mut z2,
75				ry2,
76				rz2,
77				u0,
78				u1,
79				u2,
80				v2,
81				ref f,
82				mut align,
83			} = self;
84			let simd = T::simd_ctx(simd);
85			let (m, n) = A.shape();
86			{
87				let simd = SimdCtx::<T, S>::new_align(simd, m, align);
88				simd_iter!(for i in [simd.indices()] {
89					simd.write(z2.rb_mut(), i, simd.zero());
90				});
91			}
92			for j in n.indices() {
93				let i = m.idx_inc(*j);
94				with_dim!(m, *m - *j);
95				let simd = SimdCtx::<T, S>::new_align(simd, m, align);
96				align -= 1;
97				let mut A = A.rb_mut().col_mut(j).subrows_mut(i, m);
98				let mut z = z2.rb_mut().subrows_mut(i, m);
99				let rz = rz2.subrows(i, m);
100				let ua = u0.subrows(i, m);
101				let v = v2.subrows(i, m);
102				let y = y2.rb_mut().at_mut(j);
103				let ry = simd.splat(&(-&ry2[j]));
104				let ub = simd.splat(&(-&u1[j]));
105				let uc = simd.splat(&(f * &u2[j]));
106				let mut acc = [simd.zero(); 4];
107				simd_iter!(for (IDX, i) in [simd.batch_indices(); 4] {
108					let mut a = simd.read(A.rb(), i);
109					a = simd.conj_mul_add(ry, simd.read(ua, i), a);
110					a = simd.conj_mul_add(ub, simd.read(rz, i), a);
111					simd.write(A.rb_mut(), i, a);
112					let tmp = simd.read(z.rb(), i);
113					simd.write(z.rb_mut(), i, simd.mul_add(a, uc, tmp));
114					acc[IDX] = simd.conj_mul_add(a, simd.read(v, i), acc[IDX]);
115				});
116				let acc0 = simd.add(acc[0], acc[1]);
117				let acc2 = simd.add(acc[2], acc[3]);
118				let acc0 = simd.add(acc0, acc2);
119				let acc0 = simd.reduce_sum(acc0);
120				let i0 = m.idx(0);
121				*y = f * (acc0 - &A[i0] * &v[i0]);
122			}
123		}
124	}
125	with_dim!(M, A.nrows());
126	with_dim!(N, A.ncols());
127	dispatch!(
128		Impl {
129			A: A.as_shape_mut(M, N),
130			y2: y2.as_row_shape_mut(N),
131			z2: z2.as_row_shape_mut(M),
132			ry2: ry2.as_row_shape(N),
133			rz2: rz2.as_row_shape(M),
134			u0: u0.as_row_shape(M),
135			u1: u1.as_row_shape(N),
136			u2: u2.as_row_shape(N),
137			v2: v2.as_row_shape(M),
138			f,
139			align,
140		},
141		Impl,
142		T
143	)
144}
145fn tridiag_fused_op<T: ComplexField>(
146	A: MatMut<'_, T>,
147	y2: ColMut<'_, T>,
148	z2: ColMut<'_, T>,
149	ry2: ColRef<'_, T>,
150	rz2: ColRef<'_, T>,
151	u0: ColRef<'_, T>,
152	u1: ColRef<'_, T>,
153	u2: ColRef<'_, T>,
154	v2: ColRef<'_, T>,
155	f: T,
156	align: usize,
157) {
158	let mut A = A;
159	let mut z2 = z2;
160	if const { T::SIMD_CAPABILITIES.is_simd() } {
161		if let (Some(A), Some(z2), Some(rz2), Some(u0), Some(v2)) = (
162			A.rb_mut().try_as_col_major_mut(),
163			z2.rb_mut().try_as_col_major_mut(),
164			rz2.try_as_col_major(),
165			u0.try_as_col_major(),
166			v2.try_as_col_major(),
167		) {
168			tridiag_fused_op_simd(
169				A, y2, z2, ry2, rz2, u0, u1, u2, v2, f, align,
170			);
171		} else {
172			tridiag_fused_op_fallback(A, y2, z2, ry2, rz2, u0, u1, u2, v2, f);
173		}
174	} else {
175		tridiag_fused_op_fallback(A, y2, z2, ry2, rz2, u0, u1, u2, v2, f);
176	}
177}
178fn tridiag_fused_op_fallback<T: ComplexField>(
179	A: MatMut<'_, T>,
180	y2: ColMut<'_, T>,
181	z2: ColMut<'_, T>,
182	ry2: ColRef<'_, T>,
183	rz2: ColRef<'_, T>,
184	u0: ColRef<'_, T>,
185	u1: ColRef<'_, T>,
186	u2: ColRef<'_, T>,
187	v2: ColRef<'_, T>,
188	f: T,
189) {
190	let mut par = Par::Seq;
191	let mut A = A;
192	let mut y2 = y2;
193	let n = A.ncols();
194	let (mut A0, mut A1) = A.rb_mut().split_at_row_mut(n);
195	let (u00, u01) = u0.split_at_row(n);
196	let (v20, v21) = v2.split_at_row(n);
197	let (mut z20, mut z21) = z2.split_at_row_mut(n);
198	let (rz20, rz21) = rz2.split_at_row(n);
199	matmul::triangular::matmul(
200		A0.rb_mut(),
201		BlockStructure::TriangularLower,
202		Accum::Add,
203		u00,
204		BlockStructure::Rectangular,
205		ry2.adjoint(),
206		BlockStructure::Rectangular,
207		-one::<T>(),
208		par.reborrow(),
209	);
210	matmul::triangular::matmul(
211		A0.rb_mut(),
212		BlockStructure::TriangularLower,
213		Accum::Add,
214		rz20,
215		BlockStructure::Rectangular,
216		u1.adjoint(),
217		BlockStructure::Rectangular,
218		-one::<T>(),
219		par.reborrow(),
220	);
221	matmul::matmul(
222		A1.rb_mut(),
223		Accum::Add,
224		u01,
225		ry2.adjoint(),
226		-one::<T>(),
227		par.reborrow(),
228	);
229	matmul::matmul(
230		A1.rb_mut(),
231		Accum::Add,
232		rz21,
233		u1.adjoint(),
234		-one::<T>(),
235		par.reborrow(),
236	);
237	matmul::triangular::matmul(
238		z20.rb_mut(),
239		BlockStructure::Rectangular,
240		Accum::Replace,
241		A0.rb(),
242		BlockStructure::TriangularLower,
243		u2,
244		BlockStructure::Rectangular,
245		f.clone(),
246		par.reborrow(),
247	);
248	matmul::triangular::matmul(
249		y2.rb_mut(),
250		BlockStructure::Rectangular,
251		Accum::Replace,
252		A0.rb().adjoint(),
253		BlockStructure::StrictTriangularUpper,
254		v20,
255		BlockStructure::Rectangular,
256		f.clone(),
257		par.reborrow(),
258	);
259	matmul::matmul(z21.rb_mut(), Accum::Replace, A1.rb(), u2, f.clone(), par.reborrow());
260	matmul::matmul(
261		y2.rb_mut(),
262		Accum::Add,
263		A1.rb().adjoint(),
264		v21,
265		f.clone(),
266		par,
267	);
268}
269/// computes a self-adjoint matrix $A$'s tridiagonalization such that $A = Q T
270/// Q^H$
271///
272/// $T$ is a self-adjoint tridiagonal matrix stored in $A$'s diagonal and
273/// subdiagonal
274///
275/// $Q$ is a sequence of householder reflections stored in the unit lower
276/// triangular half of $A$ (excluding the diagonal), with the householder
277/// coefficients being stored in `householder`
278pub fn tridiag_in_place<T: ComplexField>(
279	A: MatMut<'_, T>,
280	householder: MatMut<'_, T>,
281	par: Par,
282	stack: &mut MemStack,
283	params: Spec<TridiagParams, T>,
284) {
285	let params = params.config;
286	let mut A = A;
287	let mut H = householder;
288	let mut par = par;
289	// `Never` keeps the whole call sequential (the steps decide their own width otherwise)
290	if matches!(params.par_threshold, ParThreshold::Never) {
291		par = Par::Seq;
292	}
293	let n = A.nrows();
294	let b = H.nrows();
295	assert!(H.ncols() == n.saturating_sub(1));
296	if n == 0 {
297		return;
298	}
299	alloca!('stack: {
300		let mut y = unsafe { col![uninit::<T>, n] };
301		let mut w = unsafe { col![uninit::<T>, n] };
302		let mut z = unsafe { mat![uninit::<T>, n, par.degree()] };
303	});
304	{
305		let mut H = H.rb_mut().row_mut(0);
306		for k in 0..n {
307			let (_, A01, A10, A11) = A.rb_mut().split_at_mut(k, k);
308			let (_, _) = A01.split_first_col().unwrap();
309			let (_, A20) = A10.split_first_row_mut().unwrap();
310			let (mut A11, _, A21, mut A22) = A11.split_at_mut(1, 1);
311			let mut A21 = A21.col_mut(0);
312			let a11 = &mut A11[(0, 0)];
313			let (y1, mut y2) =
314				y.rb_mut().split_at_row_mut(k).1.split_at_row_mut(1);
315			if k > 0 {
316				let y1 = y1[0].copy();
317				let p = k - 1;
318				let u2 = A20.rb().col(p);
319				*a11 -= &y1 + y1.conj();
320				z!(A21.rb_mut(), u2, y2.rb()).for_each(
321					|uz!(a, u, y): Zip!(&mut T, &T, &T)| {
322						*a -= y1.conj() * u + y;
323					},
324				);
325			}
326			if k + 1 == n {
327				break;
328			}
329			let rem = n - k - 1;
330			// One section per step: the symmetric fused update over column blocks; the unit is
331			// an element of the triangle. The step runs at the widest width that pays.
332			let width = params.par_threshold.region_width::<T>(&par, rem * rem / 2, rate::tridiag(), 1);
333			let k1 = k + 1;
334			let tau_inv;
335			{
336				let (mut a11, mut x2) = A21.rb_mut().split_at_row_mut(1);
337				let a11 = &mut a11[0];
338				let householder::HouseholderInfo { tau, .. } =
339					householder::make_householder_in_place(a11, x2.rb_mut());
340				tau_inv = tau.real().recip();
341				H[k] = tau.to_cplx();
342				let mut z2 = z.rb_mut().split_at_row_mut(k + 2).1;
343				let mut w2 = w.rb_mut().split_at_row_mut(k + 2).1;
344				let (mut y1, mut y2) = y2.rb_mut().split_at_row_mut(1);
345				let y1 = &mut y1[0];
346				let (A1, A2) = A22.rb_mut().split_at_row_mut(1);
347				let A1 = A1.row_mut(0);
348				let (mut a11, _) = A1.split_at_col_mut(1);
349				let a11 = &mut a11[0];
350				let (A21, mut A22) = A2.split_at_col_mut(1);
351				let mut A21 = A21.col_mut(0);
352				if k > 0 {
353					let p = k - 1;
354					let (u1, u2) = A20.rb().col(p).split_at_row(1);
355					let ref u1 = u1[0].copy();
356					*a11 -= u1 * y1.conj() + &*y1 * u1.conj();
357					z!(A21.rb_mut(), u2.rb(), y2.rb()).for_each(
358						|uz!(a, u, y): Zip!(&mut T, &T, &T)| {
359							*a -= u * y1.conj() + &*y * u1.conj();
360						},
361					);
362					w2.copy_from(y2.rb());
363					with_width(par.reborrow(), width, |par| {
364						match par {
365							Par::Seq => {
366								let mut z2 = z2.rb_mut().col_mut(0);
367								tridiag_fused_op(
368									A22.rb_mut(),
369									y2.rb_mut(),
370									z2.rb_mut(),
371									w2.rb(),
372									w2.rb(),
373									u2.rb(),
374									u2.rb(),
375									x2.rb(),
376									x2.rb(),
377									tau_inv.to_cplx(),
378									simd_align(k1 + 1),
379								);
380								z!(y2.rb_mut(), z2.rb()).for_each(
381									|uz!(y, z): Zip!(&mut T, &T)| *y += z,
382								);
383							},
384		#[cfg(feature = "rayon")]
385							_ => {
386							let nthreads = core::num::NonZeroUsize::new(par.degree()).unwrap();
387								use rayon::prelude::*;
388								let nthreads = nthreads.get();
389								let mut z2 = z2.rb_mut().subcols_mut(0, nthreads);
390								let n2 = A22.ncols();
391								assert!((n2 as u64) < (1u64 << 50));
392								let idx_to_col_start = |idx: usize| {
393									let idx_as_percent =
394										idx as f64 / nthreads as f64;
395									let col_start_percent = 1.0f64
396										- libm::sqrt(1.0f64 - idx_as_percent);
397									(col_start_percent * n2 as f64) as usize
398								};
399								{
400									let A22 = A22.rb();
401									let y2 = y2.rb();
402									let ref f = tau_inv.to_cplx::<T>();
403									crate::utils::thread::for_each(par, 
404										nthreads,
405										z2.rb_mut().par_col_iter_mut().enumerate(),
406										|(idx, mut z2)| {
407											let first = idx_to_col_start(idx);
408											let last_col =
409												idx_to_col_start(idx + 1);
410											let nrows = n2 - first;
411											let ncols = last_col - first;
412											let mut A = unsafe {
413												A22.rb()
414													.subcols(first, ncols)
415													.subrows(first, nrows)
416													.const_cast()
417											};
418											{
419												let y2 = unsafe {
420													y2.subrows(first, ncols)
421														.const_cast()
422												};
423												let mut z2 = z2
424													.rb_mut()
425													.subrows_mut(first, nrows);
426												let ry2 =
427													w2.rb().subrows(first, ncols);
428												let rz2 =
429													w2.rb().subrows(first, nrows);
430												let u0 = u2.subrows(first, nrows);
431												let u1 = u2.subrows(first, ncols);
432												let u2 =
433													x2.rb().subrows(first, ncols);
434												let v2 =
435													x2.rb().subrows(first, nrows);
436												tridiag_fused_op(
437													A.rb_mut(),
438													y2,
439													z2.rb_mut(),
440													ry2,
441													rz2,
442													u0,
443													u1,
444													u2,
445													v2,
446													f.copy(),
447													n.next_power_of_two()
448														- (k1 + 1) - first,
449												);
450											}
451											z2.rb_mut()
452												.subrows_mut(0, first)
453												.fill(zero());
454										},
455									);
456								}
457								for z2 in z2.rb().col_iter() {
458									z!(y2.rb_mut(), z2.rb())
459										.for_each(|uz!(y, z)| *y += z);
460								}
461							},
462						}
463					});
464				} else {
465					with_width(par.reborrow(), width, |par| {
466						matmul::triangular::matmul(
467							y2.rb_mut(),
468							BlockStructure::Rectangular,
469							Accum::Replace,
470							A22.rb(),
471							BlockStructure::TriangularLower,
472							x2.rb(),
473							BlockStructure::Rectangular,
474							tau_inv.to_cplx(),
475							par,
476						);
477					});
478					with_width(par.reborrow(), width, |par| {
479						matmul::triangular::matmul(
480							y2.rb_mut(),
481							BlockStructure::Rectangular,
482							Accum::Add,
483							A22.rb().adjoint(),
484							BlockStructure::StrictTriangularUpper,
485							x2.rb(),
486							BlockStructure::Rectangular,
487							tau_inv.to_cplx(),
488							par,
489						);
490					});
491				}
492				z!(y2.rb_mut(), A21.rb()).for_each(
493					|uz!(y, a): Zip!(&mut T, &T)| {
494						*y += a.mul_real(&tau_inv);
495					},
496				);
497				*y1 = (&*a11
498					+ dot::inner_prod(
499						A21.rb().transpose(),
500						Conj::Yes,
501						x2.rb(),
502						Conj::No,
503					))
504				.mul_real(&tau_inv);
505				let ref b = (&*y1
506					+ dot::inner_prod(
507						x2.rb().transpose(),
508						Conj::Yes,
509						y2.rb(),
510						Conj::No,
511					))
512				.mul_pow2(from_f64::<T::Real>(0.5))
513				.mul_real(&tau_inv);
514				*y1 -= b;
515				z!(y2.rb_mut(), x2.rb()).for_each(
516					|uz!(y, u): Zip!(&mut T, &T)| {
517						*y -= b * u;
518					},
519				);
520			}
521		}
522	}
523	if n > 0 {
524		let n = n - 1;
525		let A = A.rb().submatrix(1, 0, n, n);
526		let mut H = H.rb_mut().subcols_mut(0, n);
527		let mut j = 0;
528		while j < n {
529			let b = Ord::min(b, n - j);
530			let mut H = H.rb_mut().submatrix_mut(0, j, b, b);
531			for k in 0..b {
532				H[(k, k)] = H[(0, k)].copy();
533			}
534			householder::upgrade_householder_factor(
535				H.rb_mut(),
536				A.submatrix(j, j, n - j, b),
537				b,
538				1,
539				par.reborrow(),
540			);
541			j += b;
542		}
543	}
544}
545#[cfg(test)]
546mod tests {
547	use super::*;
548	use crate::stats::prelude::*;
549	use crate::utils::approx::*;
550	use crate::{Mat, assert, c64};
551	use dyn_stack::MemBuffer;
552	#[test]
553	fn test_tridiag_real() {
554		let rng = &mut StdRng::seed_from_u64(0);
555		for n in [2, 3, 4, 8, 16] {
556			let A = CwiseMatDistribution {
557				nrows: n,
558				ncols: n,
559				dist: StandardNormal,
560			}
561			.rand::<Mat<f64>>(rng);
562			let A = A.rb() + A.adjoint();
563			let b = 3;
564			let mut H = Mat::zeros(b, n - 1);
565			let mut V = A.clone();
566			let mut V = V.as_mut();
567			tridiag_in_place(
568				V.rb_mut(),
569				H.rb_mut(),
570				Par::Seq,
571				MemStack::new(&mut MemBuffer::new(StackReq::all_of(&[
572					householder::apply_block_householder_sequence_transpose_on_the_left_in_place_scratch::<f64>(n - 1, b, n),
573					tridiag_in_place_scratch::<f64>(n, ParShape::Seq, default()),
574				]))),
575				default(),
576			);
577			let mut A = A.clone();
578			let mut A = A.as_mut();
579			for iter in 0..2 {
580				let mut A = if iter == 0 {
581					A.rb_mut()
582				} else {
583					A.rb_mut().transpose_mut()
584				};
585				let n = n - 1;
586				let V = V.rb().submatrix(1, 0, n, n);
587				let mut A = A.rb_mut().subrows_mut(1, n);
588				let H = H.as_ref();
589				householder::apply_block_householder_sequence_transpose_on_the_left_in_place_with_conj(
590					V,
591					H.as_ref(),
592					if iter == 0 { Conj::Yes } else { Conj::No },
593					A.rb_mut(),
594					Par::Seq,
595					MemStack::new(&mut MemBuffer::new(
596						householder::apply_block_householder_sequence_transpose_on_the_left_in_place_scratch::<f64>(n, b, n + 1),
597					)),
598				);
599			}
600			let approx_eq = CwiseMat(ApproxEq::<f64>::eps());
601			for j in 0..n {
602				for i in 0..n {
603					if i > j + 1 || j > i + 1 {
604						V[(i, j)] = 0.0;
605					}
606				}
607			}
608			for i in 0..n {
609				if i + 1 < n {
610					V[(i, i + 1)] = V[(i + 1, i)];
611				}
612			}
613			assert!(V ~ A);
614		}
615	}
616	#[test]
617	fn test_tridiag_cplx() {
618		let rng = &mut StdRng::seed_from_u64(0);
619		for n in [2, 3, 4, 8, 16] {
620			let A = CwiseMatDistribution {
621				nrows: n,
622				ncols: n,
623				dist: ComplexDistribution::new(StandardNormal, StandardNormal),
624			}
625			.rand::<Mat<c64>>(rng);
626			let A = A.rb() + A.adjoint();
627			let b = 3;
628			let mut H = Mat::zeros(b, n - 1);
629			let mut V = A.clone();
630			let mut V = V.as_mut();
631			tridiag_in_place(
632				V.rb_mut(),
633				H.as_mut(),
634				Par::Seq,
635				MemStack::new(&mut MemBuffer::new(tridiag_in_place_scratch::<
636					c64,
637				>(
638					n, ParShape::Seq, default()
639				))),
640				default(),
641			);
642			let mut A = A.clone();
643			let mut A = A.as_mut();
644			for iter in 0..2 {
645				let mut A = if iter == 0 {
646					A.rb_mut()
647				} else {
648					A.rb_mut().transpose_mut()
649				};
650				let n = n - 1;
651				let V = V.rb().submatrix(1, 0, n, n);
652				let mut A = A.rb_mut().subrows_mut(1, n);
653				let H = H.as_ref();
654				householder::apply_block_householder_sequence_transpose_on_the_left_in_place_with_conj(
655					V,
656					H.as_ref(),
657					if iter == 0 { Conj::Yes } else { Conj::No },
658					A.rb_mut(),
659					Par::Seq,
660					MemStack::new(&mut MemBuffer::new(
661						householder::apply_block_householder_sequence_transpose_on_the_left_in_place_scratch::<c64>(n, b, n + 1),
662					)),
663				);
664			}
665			let approx_eq = CwiseMat(ApproxEq::eps());
666			for j in 0..n {
667				for i in 0..n {
668					if i > j + 1 || j > i + 1 {
669						V[(i, j)] = c64::ZERO;
670					}
671				}
672			}
673			for i in 0..n {
674				if i + 1 < n {
675					V[(i, i + 1)] = V[(i + 1, i)].conj();
676				}
677			}
678			assert!(V ~ A);
679		}
680	}
681}