1use crate::estimate::EstimationError;
2use gam_linalg::faer_ndarray::{FaerSymmetricFactor, array2_to_matmut};
3use gam_linalg::matrix::SymmetricMatrix;
4use gam_linalg::utils::{StableSolver, array_is_finite};
5use gam_problem::Coefficients;
6use ndarray::{Array1, Array2};
7
8use super::{PirlsPenalty, PirlsWorkspace};
9
10#[derive(Clone)]
12pub struct StablePLSResult {
13 pub beta: Coefficients,
15 pub penalized_hessian: SymmetricMatrix,
17 pub edf: f64,
19 pub standard_deviation: f64,
24 pub ridge_used: f64,
26}
27
28pub(super) fn calculate_edfwithworkspace_from_factor(
34 factor: &FaerSymmetricFactor,
35 penalty: &PirlsPenalty,
36 workspace: &mut PirlsWorkspace,
37) -> Result<f64, EstimationError> {
38 match penalty {
39 PirlsPenalty::Dense { e_transformed, .. } => {
40 let p = factor.n();
41 let r = e_transformed.nrows();
42 let mp = (p as f64 - r as f64).max(0.0);
43 if r == 0 {
44 return Ok(p as f64);
45 }
46 if workspace.final_aug_matrix.nrows() != p || workspace.final_aug_matrix.ncols() != r {
47 workspace.final_aug_matrix = Array2::zeros((p, r));
48 }
49 for j in 0..r {
50 for i in 0..p {
51 workspace.final_aug_matrix[[i, j]] = e_transformed[[j, i]];
52 }
53 }
54 {
55 let mut rhsview = array2_to_matmut(&mut workspace.final_aug_matrix);
56 factor.solve_in_place(rhsview.as_mut());
57 }
58 if workspace.final_aug_matrix.nrows() == p
59 && workspace.final_aug_matrix.ncols() == r
60 && array_is_finite(&workspace.final_aug_matrix)
61 {
62 return Ok(edf_from_solution(p, r, mp, e_transformed, |i, j| {
63 workspace.final_aug_matrix[(i, j)]
64 }));
65 }
66 Err(EstimationError::ModelIsIllConditioned {
67 condition_number: f64::INFINITY,
68 })
69 }
70 PirlsPenalty::Diagonal {
71 diag,
72 positive_indices,
73 ..
74 } => {
75 let p = factor.n();
76 let r = positive_indices.len();
77 let mp = (p as f64 - r as f64).max(0.0);
78 if r == 0 {
79 return Ok(p as f64);
80 }
81 if workspace.final_aug_matrix.nrows() != p || workspace.final_aug_matrix.ncols() != r {
82 workspace.final_aug_matrix = Array2::zeros((p, r));
83 } else {
84 workspace.final_aug_matrix.fill(0.0);
85 }
86 for (col, &idx) in positive_indices.iter().enumerate() {
87 workspace.final_aug_matrix[[idx, col]] = 1.0;
88 }
89 {
90 let mut rhsview = array2_to_matmut(&mut workspace.final_aug_matrix);
91 factor.solve_in_place(rhsview.as_mut());
92 }
93 let mut tr = 0.0;
94 for (col, &idx) in positive_indices.iter().enumerate() {
95 tr += diag[idx] * workspace.final_aug_matrix[[idx, col]];
96 }
97 Ok((p as f64 - tr).clamp(mp, p as f64))
98 }
99 }
100}
101
102pub(super) fn calculate_edf_from_sparse_factor(
111 factor: &gam_linalg::sparse_exact::SparseExactFactor,
112 penalty: &PirlsPenalty,
113) -> Result<f64, EstimationError> {
114 let PirlsPenalty::Dense { e_transformed, .. } = penalty else {
115 crate::bail_invalid_estim!("calculate_edf_from_sparse_factor requires PirlsPenalty::Dense");
116 };
117 let p = e_transformed.ncols();
119 let r = e_transformed.nrows();
120 let mp = (p as f64 - r as f64).max(0.0);
121 if r == 0 {
122 return Ok(p as f64);
123 }
124 let rhs_arr = e_transformed.t().to_owned();
125 let sol = gam_linalg::sparse_exact::solve_sparse_spdmulti(factor, &rhs_arr).map_err(|_| {
126 EstimationError::ModelIsIllConditioned {
127 condition_number: f64::INFINITY,
128 }
129 })?;
130 if sol.nrows() == p && sol.ncols() == r && sol.iter().all(|v| v.is_finite()) {
131 return Ok(edf_from_solution(p, r, mp, e_transformed, |i, j| {
132 sol[[i, j]]
133 }));
134 }
135 Err(EstimationError::ModelIsIllConditioned {
136 condition_number: f64::INFINITY,
137 })
138}
139
140pub(super) fn calculate_edf(
141 penalized_hessian: &SymmetricMatrix,
142 e_transformed: &Array2<f64>,
143) -> Result<f64, EstimationError> {
144 let p = penalized_hessian.ncols();
145 let r = e_transformed.nrows();
146 let mp = (p as f64 - r as f64).max(0.0);
147 if r == 0 {
148 return Ok(p as f64);
149 }
150 let rhs_arr = e_transformed.t().to_owned();
151 let factor =
154 penalized_hessian
155 .factorize()
156 .map_err(|_| EstimationError::ModelIsIllConditioned {
157 condition_number: f64::INFINITY,
158 })?;
159 let sol = factor
160 .solvemulti(&rhs_arr)
161 .map_err(|_| EstimationError::ModelIsIllConditioned {
162 condition_number: f64::INFINITY,
163 })?;
164 if sol.nrows() == p && sol.ncols() == r && sol.iter().all(|v| v.is_finite()) {
165 return Ok(edf_from_solution(p, r, mp, e_transformed, |i, j| {
166 sol[[i, j]]
167 }));
168 }
169
170 Err(EstimationError::ModelIsIllConditioned {
171 condition_number: f64::INFINITY,
172 })
173}
174
175pub(super) fn calculate_edf_with_penalty(
176 penalized_hessian: &SymmetricMatrix,
177 penalty: &PirlsPenalty,
178) -> Result<f64, EstimationError> {
179 match penalty {
180 PirlsPenalty::Dense { e_transformed, .. } => {
181 calculate_edf(penalized_hessian, e_transformed)
182 }
183 PirlsPenalty::Diagonal {
184 diag,
185 positive_indices,
186 ..
187 } => calculate_edf_from_diagonal_penalty(penalized_hessian, diag, positive_indices),
188 }
189}
190
191pub(super) fn calculate_edfwithworkspace(
192 penalized_hessian: &Array2<f64>,
193 e_transformed: &Array2<f64>,
194 workspace: &mut PirlsWorkspace,
195) -> Result<f64, EstimationError> {
196 let p = penalized_hessian.ncols();
197 let r = e_transformed.nrows();
198 let mp = (p as f64 - r as f64).max(0.0);
199 if r == 0 {
200 return Ok(p as f64);
201 }
202 if workspace.final_aug_matrix.nrows() != p || workspace.final_aug_matrix.ncols() != r {
203 workspace.final_aug_matrix = Array2::zeros((p, r));
204 }
205 for j in 0..r {
206 for i in 0..p {
207 workspace.final_aug_matrix[[i, j]] = e_transformed[[j, i]];
208 }
209 }
210
211 let factor = StableSolver::new()
212 .factorize(penalized_hessian)
213 .map_err(|_| EstimationError::ModelIsIllConditioned {
214 condition_number: f64::INFINITY,
215 })?;
216 {
217 let mut rhsview = array2_to_matmut(&mut workspace.final_aug_matrix);
218 factor.solve_in_place(rhsview.as_mut());
219 }
220 if workspace.final_aug_matrix.nrows() == p
221 && workspace.final_aug_matrix.ncols() == r
222 && array_is_finite(&workspace.final_aug_matrix)
223 {
224 return Ok(edf_from_solution(p, r, mp, e_transformed, |i, j| {
225 workspace.final_aug_matrix[(i, j)]
226 }));
227 }
228
229 Err(EstimationError::ModelIsIllConditioned {
230 condition_number: f64::INFINITY,
231 })
232}
233
234pub(super) fn calculate_edfwithworkspace_with_penalty(
235 penalized_hessian: &Array2<f64>,
236 penalty: &PirlsPenalty,
237 workspace: &mut PirlsWorkspace,
238) -> Result<f64, EstimationError> {
239 match penalty {
240 PirlsPenalty::Dense { e_transformed, .. } => {
241 calculate_edfwithworkspace(penalized_hessian, e_transformed, workspace)
242 }
243 PirlsPenalty::Diagonal {
244 diag,
245 positive_indices,
246 ..
247 } => calculate_edfwithworkspace_from_diagonal_penalty(
248 penalized_hessian,
249 diag,
250 positive_indices,
251 workspace,
252 ),
253 }
254}
255
256pub(super) fn calculate_edf_from_diagonal_penalty(
257 penalized_hessian: &SymmetricMatrix,
258 diag: &Array1<f64>,
259 positive_indices: &[usize],
260) -> Result<f64, EstimationError> {
261 let p = penalized_hessian.ncols();
262 let r = positive_indices.len();
263 let mp = (p as f64 - r as f64).max(0.0);
264 if r == 0 {
265 return Ok(p as f64);
266 }
267 let mut rhs_arr = Array2::<f64>::zeros((p, r));
268 for (col, &idx) in positive_indices.iter().enumerate() {
269 rhs_arr[[idx, col]] = 1.0;
270 }
271 let factor =
272 penalized_hessian
273 .factorize()
274 .map_err(|_| EstimationError::ModelIsIllConditioned {
275 condition_number: f64::INFINITY,
276 })?;
277 let sol = factor
278 .solvemulti(&rhs_arr)
279 .map_err(|_| EstimationError::ModelIsIllConditioned {
280 condition_number: f64::INFINITY,
281 })?;
282 let mut tr = 0.0;
283 for (col, &idx) in positive_indices.iter().enumerate() {
284 tr += diag[idx] * sol[[idx, col]];
285 }
286 Ok((p as f64 - tr).clamp(mp, p as f64))
287}
288
289pub(super) fn calculate_edfwithworkspace_from_diagonal_penalty(
290 penalized_hessian: &Array2<f64>,
291 diag: &Array1<f64>,
292 positive_indices: &[usize],
293 workspace: &mut PirlsWorkspace,
294) -> Result<f64, EstimationError> {
295 let p = penalized_hessian.ncols();
296 let r = positive_indices.len();
297 let mp = (p as f64 - r as f64).max(0.0);
298 if r == 0 {
299 return Ok(p as f64);
300 }
301 if workspace.final_aug_matrix.nrows() != p || workspace.final_aug_matrix.ncols() != r {
302 workspace.final_aug_matrix = Array2::zeros((p, r));
303 } else {
304 workspace.final_aug_matrix.fill(0.0);
305 }
306 for (col, &idx) in positive_indices.iter().enumerate() {
307 workspace.final_aug_matrix[[idx, col]] = 1.0;
308 }
309
310 let factor = StableSolver::new()
311 .factorize(penalized_hessian)
312 .map_err(|_| EstimationError::ModelIsIllConditioned {
313 condition_number: f64::INFINITY,
314 })?;
315 {
316 let mut rhsview = array2_to_matmut(&mut workspace.final_aug_matrix);
317 factor.solve_in_place(rhsview.as_mut());
318 }
319 let mut tr = 0.0;
320 for (col, &idx) in positive_indices.iter().enumerate() {
321 tr += diag[idx] * workspace.final_aug_matrix[[idx, col]];
322 }
323 Ok((p as f64 - tr).clamp(mp, p as f64))
324}
325
326#[inline]
327pub(super) fn edf_from_solution<F>(
328 p: usize,
329 r: usize,
330 mp: f64,
331 e_transformed: &Array2<f64>,
332 solved_at: F,
333) -> f64
334where
335 F: Fn(usize, usize) -> f64,
336{
337 let mut tr = 0.0;
338 for j in 0..r {
339 for i in 0..p {
340 tr += solved_at(i, j) * e_transformed[(j, i)];
341 }
342 }
343 (p as f64 - tr).clamp(mp, p as f64)
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349 use gam_linalg::matrix::SymmetricMatrix;
350 use ndarray::array;
351
352 #[test]
361 pub(crate) fn calculate_edf_floors_when_penalty_rank_exceeds_coefficient_dim() {
362 let p = 2usize;
364 let hessian = SymmetricMatrix::Dense(array![[4.0, 1.0], [1.0, 3.0]]);
366 let e_transformed = array![[1.0, 0.0], [0.0, 1.0], [0.5, 0.5]];
369 assert_eq!(e_transformed.nrows(), 3);
370 assert_eq!(e_transformed.ncols(), p);
371
372 let edf = calculate_edf(&hessian, &e_transformed)
373 .expect("EDF solve should succeed for an SPD Hessian with r > p");
374
375 assert!(
377 edf.is_finite(),
378 "EDF must be finite for r > p penalty, got {edf}"
379 );
380 assert!(
381 (0.0..=p as f64).contains(&edf),
382 "EDF must lie in [0, {p}] for r > p penalty, got {edf}"
383 );
384 }
385}