Skip to content

Commit 80a8024

Browse files
committed
fix(solver): use dataset-local dof for reduced chi-square
1 parent 8ac9f73 commit 80a8024

1 file changed

Lines changed: 107 additions & 2 deletions

File tree

crates/xraytsubaki/src/xafs/fitting/solver.rs

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::collections::BTreeSet;
2+
13
use levenberg_marquardt::{LeastSquaresProblem, LevenbergMarquardt};
24
use nalgebra::{DMatrix, DVector, Dyn, Owned};
35
use rayon::prelude::*;
@@ -9,8 +11,9 @@ use super::transform::{
911
};
1012
use super::types::{
1113
DatasetResult, FeffBatchExecutionStrategy, FeffBatchOptions, FeffFitDataset, FeffFitResult,
12-
FitVariables, PathContribution,
14+
FitVariables, PathContribution, PathParamSpec,
1315
};
16+
use super::variables::try_extract_symbols;
1417

1518
fn feffit(
1619
dataset: &FeffFitDataset,
@@ -77,7 +80,8 @@ pub fn feffit_joint(
7780
)?;
7881
let ds_chi_square = ds_residual.dot(&ds_residual);
7982
let ds_n_data = ds_residual.len();
80-
let ds_dof = ds_n_data.saturating_sub(n_vary).max(1);
83+
let ds_n_vary = dataset_vary_count(dataset, &solved.variables, &solved.variable_names)?;
84+
let ds_dof = ds_n_data.saturating_sub(ds_n_vary).max(1);
8185
let ds_reduced_chi_square = ds_chi_square / ds_dof as f64;
8286

8387
let data_norm = dataset.chi.iter().map(|value| value.abs()).sum::<f64>();
@@ -153,6 +157,75 @@ pub fn feffit_joint(
153157
Ok(out)
154158
}
155159

160+
fn dataset_vary_count(
161+
dataset: &FeffFitDataset,
162+
variables: &FitVariables,
163+
varying_names: &[String],
164+
) -> Result<usize, FittingError> {
165+
let varying = varying_names
166+
.iter()
167+
.map(String::as_str)
168+
.collect::<BTreeSet<_>>();
169+
let mut referenced = BTreeSet::<String>::new();
170+
171+
for path in dataset.paths.iter().filter(|path| path.use_path) {
172+
for spec in [
173+
&path.degen,
174+
&path.s02,
175+
&path.e0,
176+
&path.ei,
177+
&path.deltar,
178+
&path.sigma2,
179+
&path.third,
180+
&path.fourth,
181+
] {
182+
if let PathParamSpec::Expression(expr) = spec {
183+
for symbol in try_extract_symbols(expr)? {
184+
referenced.insert(symbol);
185+
}
186+
}
187+
}
188+
}
189+
190+
let mut active = BTreeSet::<String>::new();
191+
let mut visiting = BTreeSet::<String>::new();
192+
193+
fn walk_symbol(
194+
symbol: &str,
195+
varying: &BTreeSet<&str>,
196+
variables: &FitVariables,
197+
active: &mut BTreeSet<String>,
198+
visiting: &mut BTreeSet<String>,
199+
) -> Result<(), FittingError> {
200+
if varying.contains(symbol) {
201+
active.insert(symbol.to_string());
202+
return Ok(());
203+
}
204+
if !visiting.insert(symbol.to_string()) {
205+
return Ok(());
206+
}
207+
208+
if let Some(expr) = variables
209+
.vars
210+
.get(symbol)
211+
.and_then(|variable| variable.expr.as_ref())
212+
{
213+
for dependency in try_extract_symbols(expr)? {
214+
walk_symbol(&dependency, varying, variables, active, visiting)?;
215+
}
216+
}
217+
218+
visiting.remove(symbol);
219+
Ok(())
220+
}
221+
222+
for symbol in referenced {
223+
walk_symbol(&symbol, &varying, variables, &mut active, &mut visiting)?;
224+
}
225+
226+
Ok(active.len())
227+
}
228+
156229
pub fn feffit_independent(
157230
datasets: &[FeffFitDataset],
158231
variables: &FitVariables,
@@ -511,6 +584,38 @@ mod tests {
511584
let result = feffit_joint(&[ds1, ds2], &initial).unwrap();
512585
assert!(result.chi_square.is_finite());
513586
assert_eq!(result.datasets.len(), 2);
587+
588+
// Per-dataset reduced chi-square should use dataset-local varying parameter counts.
589+
let expected0 = result.datasets[0].chi_square
590+
/ result.datasets[0].n_data.saturating_sub(4).max(1) as f64;
591+
let expected1 = result.datasets[1].chi_square
592+
/ result.datasets[1].n_data.saturating_sub(4).max(1) as f64;
593+
assert!((result.datasets[0].reduced_chi_square - expected0).abs() < 1.0e-10);
594+
assert!((result.datasets[1].reduced_chi_square - expected1).abs() < 1.0e-10);
595+
}
596+
597+
#[test]
598+
fn test_dataset_vary_count_tracks_expression_dependencies() {
599+
let path1 = crate::xafs::fitting::types::FeffPathModel::default()
600+
.set_s02("amp")
601+
.set_sigma2("sig2_eff");
602+
let path2 = crate::xafs::fitting::types::FeffPathModel::default().set_s02("amp2");
603+
604+
let ds1 = FeffFitDataset::new().add_path(path1);
605+
let ds2 = FeffFitDataset::new().add_path(path2);
606+
607+
let mut vars = FitVariables::new();
608+
vars.insert("amp", FitVariable::new(1.0, true));
609+
vars.insert("amp2", FitVariable::new(1.0, true));
610+
vars.insert("sig2", FitVariable::new(0.003, true));
611+
vars.insert(
612+
"sig2_eff",
613+
FitVariable::new(0.0, false).with_expr("sig2 * 2.0"),
614+
);
615+
let varying = vec!["amp".to_string(), "amp2".to_string(), "sig2".to_string()];
616+
617+
assert_eq!(dataset_vary_count(&ds1, &vars, &varying).unwrap(), 2);
618+
assert_eq!(dataset_vary_count(&ds2, &vars, &varying).unwrap(), 1);
514619
}
515620

516621
#[test]

0 commit comments

Comments
 (0)