Skip to main content

sparse_ir/sve/
utils.rs

1//! Utility functions for SVE computation
2
3use crate::gauss::Rule;
4use crate::interpolation1d::legendre_collocation_matrix;
5use crate::kernel::SymmetryType;
6use crate::numeric::CustomNumeric;
7use crate::poly::{PiecewiseLegendrePoly, PiecewiseLegendrePolyVector};
8use mdarray::DTensor;
9
10/// Remove Gauss weights from SVD matrix
11///
12/// This function removes the square root of Gauss quadrature weights that were
13/// applied before SVD computation. This is the inverse operation of
14/// `DiscretizedKernel::apply_weights_for_sve()`.
15///
16/// # Arguments
17///
18/// * `matrix` - SVD result matrix (U or V^T)
19/// * `weights` - Gauss quadrature weights
20/// * `is_row` - If true, remove from rows; if false, remove from columns
21///
22/// # Returns
23///
24/// Matrix with weights removed
25pub fn remove_weights<T: CustomNumeric>(
26    matrix: &DTensor<T, 2>,
27    weights: &[T],
28    is_row: bool,
29) -> DTensor<T, 2> {
30    let mut result = matrix.clone();
31
32    let shape = *result.shape();
33    if is_row {
34        // Remove weights from rows (for U matrix)
35        for i in 0..shape.0 {
36            let sqrt_weight = weights[i].sqrt();
37            for j in 0..shape.1 {
38                result[[i, j]] = result[[i, j]] / sqrt_weight;
39            }
40        }
41    } else {
42        // Remove weights from columns (for V matrix)
43        for j in 0..shape.1 {
44            let sqrt_weight = weights[j].sqrt();
45            for i in 0..shape.0 {
46                result[[i, j]] = result[[i, j]] / sqrt_weight;
47            }
48        }
49    }
50
51    result
52}
53
54/// Extend polynomials from [0, xmax] to [-xmax, xmax] using symmetry
55///
56/// Following the C++ implementation logic from sve.rs.bak:856-888
57///
58/// # Arguments
59///
60/// * `polys` - Polynomials defined on [0, xmax]
61/// * `symmetry` - Even or Odd symmetry type
62/// * `xmax` - Maximum value of the domain
63///
64/// # Returns
65///
66/// Polynomials extended to full domain [-xmax, xmax]
67///
68/// # Mathematical Background
69///
70/// For Even symmetry (sign = +1): f(-x) = f(x)
71/// For Odd symmetry (sign = -1): f(-x) = -f(x)
72///
73/// Legendre polynomial parity: P_n(-x) = (-1)^n P_n(x)
74pub fn extend_to_full_domain(
75    polys: Vec<PiecewiseLegendrePoly>,
76    symmetry: SymmetryType,
77    _xmax: f64,
78) -> Vec<PiecewiseLegendrePoly> {
79    let sign = symmetry.sign() as f64;
80    let symm = symmetry.sign(); // Preserve symmetry: +1 for even, -1 for odd
81
82    // Create poly_flip_x: alternating signs for Legendre polynomials
83    // This accounts for P_n(-x) = (-1)^n P_n(x)
84    let n_poly_coeffs = if !polys.is_empty() {
85        polys[0].data.shape().0
86    } else {
87        return Vec::new();
88    };
89
90    let poly_flip_x: Vec<f64> = (0..n_poly_coeffs)
91        .map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
92        .collect();
93
94    polys
95        .into_iter()
96        .map(|poly| {
97            // Create full segments from this polynomial's knots: [-xmax, ..., 0, ..., xmax]
98            let knots_pos = &poly.knots;
99            let mut full_segments = Vec::new();
100            for i in (0..knots_pos.len()).rev() {
101                full_segments.push(-knots_pos[i]);
102            }
103            for i in 1..knots_pos.len() {
104                full_segments.push(knots_pos[i]);
105            }
106
107            // Normalize by 1/sqrt(2) and convert to f64
108            let pos_data = DTensor::<f64, 2>::from_fn(*poly.data.shape(), |idx| {
109                poly.data[idx] / 2.0_f64.sqrt()
110            });
111
112            // Create negative part by reversing columns and applying signs
113            let pos_shape = *pos_data.shape();
114            let mut neg_data = DTensor::<f64, 2>::from_fn([pos_shape.0, pos_shape.1], |idx| {
115                // Reverse column order: map column j to column (n_cols - 1 - j)
116                let reversed_col = pos_shape.1 - 1 - idx[1];
117                pos_data[[idx[0], reversed_col]]
118            });
119
120            // Apply poly_flip_x and sign to negative part
121            for (i, &flip_sign) in poly_flip_x.iter().enumerate() {
122                let coeff_sign = flip_sign * sign;
123                for j in 0..pos_shape.1 {
124                    neg_data[[i, j]] *= coeff_sign;
125                }
126            }
127
128            // Combine negative and positive parts (concatenate along axis 1)
129            let combined_data = DTensor::<f64, 2>::from_fn([pos_shape.0, pos_shape.1 * 2], |idx| {
130                if idx[1] < pos_shape.1 {
131                    neg_data[[idx[0], idx[1]]]
132                } else {
133                    pos_data[[idx[0], idx[1] - pos_shape.1]]
134                }
135            });
136
137            // Create complete polynomial with full segments
138            // Preserve symmetry from even/odd decomposition
139            PiecewiseLegendrePoly::new(
140                combined_data,
141                full_segments,
142                poly.l,
143                None, // delta_x will be computed automatically
144                symm, // Preserve symmetry: +1 for even, -1 for odd
145            )
146        })
147        .collect()
148}
149
150/// Convert SVD matrix to piecewise Legendre polynomials
151///
152/// This function converts SVD results (U or V matrices) to piecewise Legendre
153/// polynomial representation.
154///
155/// # Arguments
156///
157/// * `u_or_v` - SVD result matrix (rows = Gauss points, cols = singular values)
158/// * `segments` - Segment boundaries
159/// * `gauss_rule` - Gauss quadrature rule
160/// * `n_gauss` - Number of Gauss points per segment
161///
162/// # Returns
163///
164/// Vector of piecewise Legendre polynomials
165pub fn svd_to_polynomials<T: CustomNumeric>(
166    u_or_v: &DTensor<T, 2>,
167    segments: &[T],
168    gauss_rule: &Rule<f64>,
169    n_gauss: usize,
170) -> Vec<PiecewiseLegendrePoly> {
171    let n_segments = segments.len() - 1;
172    let n_svals = u_or_v.shape().1;
173    let n_rows = u_or_v.shape().0;
174
175    // Reshape to 3D: (n_gauss, n_segments, n_svals)
176    // Note: Due to QR early termination, u_or_v may have fewer rows than expected
177    // We need to handle the case where row_idx exceeds the actual number of rows
178    let mut tensor_3d = DTensor::<f64, 3>::zeros([n_gauss, n_segments, n_svals]);
179    for i in 0..n_gauss {
180        for j in 0..n_segments {
181            for k in 0..n_svals {
182                let row_idx = j * n_gauss + i;
183                if row_idx < n_rows {
184                    tensor_3d[[i, j, k]] = u_or_v[[row_idx, k]].to_f64();
185                } else {
186                    // If row_idx is out of bounds, set to zero (due to early termination)
187                    tensor_3d[[i, j, k]] = 0.0;
188                }
189            }
190        }
191    }
192
193    // Create Legendre collocation matrix
194    let cmat = legendre_collocation_matrix(gauss_rule);
195
196    // Transform to Legendre basis
197    let cmat_shape = *cmat.shape();
198    let mut u_data = DTensor::<f64, 3>::zeros([cmat_shape.0, n_segments, n_svals]);
199    for j in 0..n_segments {
200        for k in 0..n_svals {
201            for i in 0..cmat_shape.0 {
202                let mut sum = 0.0;
203                for l in 0..n_gauss {
204                    sum += cmat[[i, l]] * tensor_3d[[l, j, k]];
205                }
206                u_data[[i, j, k]] = sum;
207            }
208        }
209    }
210
211    // Apply segment length normalization: sqrt(0.5 * delta_segment)
212    let mut dsegs = Vec::new();
213    for i in 0..segments.len() - 1 {
214        dsegs.push(segments[i + 1].to_f64() - segments[i].to_f64());
215    }
216
217    let u_data_shape = *u_data.shape();
218    for j in 0..n_segments {
219        let norm = (0.5 * dsegs[j]).sqrt();
220        for i in 0..u_data_shape.0 {
221            for k in 0..n_svals {
222                u_data[[i, j, k]] *= norm;
223            }
224        }
225    }
226
227    // Create polynomials
228    let mut polys = Vec::new();
229    let knots: Vec<f64> = segments.iter().map(|&x| x.to_f64()).collect();
230    let delta_x: Vec<f64> = knots.windows(2).map(|w| w[1] - w[0]).collect();
231
232    for k in 0..n_svals {
233        // Extract data for this singular value: (n_coeffs, n_segments)
234        let u_data_shape = u_data.shape();
235        let mut data = DTensor::<f64, 2>::zeros([u_data_shape.0, n_segments]);
236        for i in 0..u_data_shape.0 {
237            for j in 0..n_segments {
238                data[[i, j]] = u_data[[i, j, k]];
239            }
240        }
241
242        polys.push(PiecewiseLegendrePoly::new(
243            data,
244            knots.clone(),
245            k as i32,
246            Some(delta_x.clone()),
247            0, // no symmetry
248        ));
249    }
250
251    polys
252}
253
254// Note: legendre_collocation_matrix is imported from interpolation1d module
255// Note: legendre_vandermonde is available in gauss module
256
257/// Canonicalize singular function signs
258///
259/// Fix the gauge freedom in SVD by demanding u[l](xmax) > 0.
260/// This ensures consistent signs across different implementations.
261///
262/// # Arguments
263///
264/// * `u_polys` - Left singular functions
265/// * `v_polys` - Right singular functions
266/// * `xmax` - Maximum value to evaluate at (typically 1.0)
267fn canonicalize_signs(
268    u_polys: PiecewiseLegendrePolyVector,
269    v_polys: PiecewiseLegendrePolyVector,
270    xmax: f64,
271) -> (PiecewiseLegendrePolyVector, PiecewiseLegendrePolyVector) {
272    let u_vec = u_polys.get_polys();
273    let v_vec = v_polys.get_polys();
274
275    let mut new_u_vec = Vec::new();
276    let mut new_v_vec = Vec::new();
277
278    for i in 0..u_vec.len().min(v_vec.len()) {
279        // Evaluate u[i] at xmax
280        let u_at_xmax = u_vec[i].evaluate(xmax);
281
282        if u_at_xmax < 0.0 {
283            // Flip sign of both u and v
284            let u_data_flipped =
285                DTensor::<f64, 2>::from_fn(*u_vec[i].data.shape(), |idx| -u_vec[i].data[idx]);
286            let v_data_flipped =
287                DTensor::<f64, 2>::from_fn(*v_vec[i].data.shape(), |idx| -v_vec[i].data[idx]);
288
289            new_u_vec.push(PiecewiseLegendrePoly::new(
290                u_data_flipped,
291                u_vec[i].knots.clone(),
292                u_vec[i].l,
293                Some(u_vec[i].delta_x.clone()),
294                u_vec[i].symm,
295            ));
296            new_v_vec.push(PiecewiseLegendrePoly::new(
297                v_data_flipped,
298                v_vec[i].knots.clone(),
299                v_vec[i].l,
300                Some(v_vec[i].delta_x.clone()),
301                v_vec[i].symm,
302            ));
303        } else {
304            // Keep as is
305            new_u_vec.push(u_vec[i].clone());
306            new_v_vec.push(v_vec[i].clone());
307        }
308    }
309
310    (
311        PiecewiseLegendrePolyVector::new(new_u_vec),
312        PiecewiseLegendrePolyVector::new(new_v_vec),
313    )
314}
315
316/// Merge even and odd SVE results
317///
318/// # Arguments
319///
320/// * `result_even` - (u, s, v) for even symmetry
321/// * `result_odd` - (u, s, v) for odd symmetry
322/// * `epsilon` - Accuracy parameter
323///
324/// # Returns
325///
326/// Merged SVEResult with singular values sorted in decreasing order
327pub fn merge_results(
328    result_even: (
329        PiecewiseLegendrePolyVector,
330        Vec<f64>,
331        PiecewiseLegendrePolyVector,
332    ),
333    result_odd: (
334        PiecewiseLegendrePolyVector,
335        Vec<f64>,
336        PiecewiseLegendrePolyVector,
337    ),
338    epsilon: f64,
339) -> crate::sve::SVEResult {
340    use crate::sve::SVEResult;
341
342    let (u_even, s_even, v_even) = result_even;
343    let (u_odd, s_odd, v_odd) = result_odd;
344
345    // Debug output
346    // Create indices with symmetry info
347    let mut indices: Vec<(usize, bool)> = Vec::new();
348    for i in 0..s_even.len() {
349        indices.push((i, true)); // true = even
350    }
351    for i in 0..s_odd.len() {
352        indices.push((i, false)); // false = odd
353    }
354
355    // Sort by singular values (descending)
356    indices.sort_by(|a, b| {
357        let s_a = if a.1 { s_even[a.0] } else { s_odd[a.0] };
358        let s_b = if b.1 { s_even[b.0] } else { s_odd[b.0] };
359        s_b.partial_cmp(&s_a).unwrap_or(std::cmp::Ordering::Equal)
360    });
361
362    // Build sorted arrays
363    let mut u_polys = Vec::new();
364    let mut v_polys = Vec::new();
365    let mut s_sorted = Vec::new();
366
367    for (idx, is_even) in indices {
368        if is_even {
369            u_polys.push(u_even.get_polys()[idx].clone());
370            v_polys.push(v_even.get_polys()[idx].clone());
371            s_sorted.push(s_even[idx]);
372        } else {
373            u_polys.push(u_odd.get_polys()[idx].clone());
374            v_polys.push(v_odd.get_polys()[idx].clone());
375            s_sorted.push(s_odd[idx]);
376        }
377    }
378
379    // Canonicalize signs: ensure u[l](1) > 0
380    let (canonical_u, canonical_v) = canonicalize_signs(
381        PiecewiseLegendrePolyVector::new(u_polys),
382        PiecewiseLegendrePolyVector::new(v_polys),
383        1.0,
384    );
385
386    SVEResult::new(canonical_u, s_sorted, canonical_v, epsilon)
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    #[test]
394    fn test_remove_weights() {
395        let matrix = DTensor::<f64, 2>::from_fn([2, 2], |idx| (idx[0] * 2 + idx[1] + 1) as f64);
396        let weights = vec![1.0, 4.0];
397
398        let result = remove_weights(&matrix, &weights, true);
399
400        // Both U and V: remove from rows (Gauss points)
401        // First row: [1.0, 2.0] / sqrt(1.0) = [1.0, 2.0]
402        // Second row: [3.0, 4.0] / sqrt(4.0) = [1.5, 2.0]
403        assert!((result[[0, 0]] - 1.0).abs() < 1e-10);
404        assert!((result[[0, 1]] - 2.0).abs() < 1e-10);
405        assert!((result[[1, 0]] - 1.5).abs() < 1e-10);
406        assert!((result[[1, 1]] - 2.0).abs() < 1e-10);
407    }
408}