Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ version = "3.0.0"
derivative = "2.2.0"
flate2 = "1.0.28"
pest = "2.7.7"
pest_derive = "2.7.7"
thiserror = "1.0.69"

xraytsubaki = { version = "0.1.0", path = "crates/xraytsubaki" }
Expand Down
5 changes: 5 additions & 0 deletions crates/xraytsubaki/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ easyfft = { workspace = true }
serde_json = { workspace = true }
flate2 = { workspace = true }
pest = { workspace = true }
pest_derive = { workspace = true }
thiserror = { workspace = true }

[features]
Expand All @@ -56,3 +57,7 @@ harness = false
[[bench]]
name = "autobk_stage_benchmark"
harness = false

[[bench]]
name = "feff_fitting_batch_benchmark"
harness = false
12 changes: 12 additions & 0 deletions crates/xraytsubaki/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ Additionally, this project seeks to leverage Rust's ecosystem to create a genera
- [x] Standard EXAFS analysis (find_e0, preedge postedge normalization, AUTOBK, FFT, IFFT)
- [x] Parallel processing using Rayon. (For example, M1 Macbook Pro with 10 cores can process 10000 spectra in 20 seconds, which is ~x10 enhancement without parallelization. Numpy + xraylarch takes 145 seconds.)
- [x] Optimization on AUTOBK. The AUTOBK process were optimized with providing an analytical Jacobian to speed up the minimization process by Leverberg-Marquardt algorithm.
- [x] FEFF85L path-based EXAFS fitting in Rust core (`xfeffdat` parsing, `path2chi`, `ff2chi`, single-dataset R-space fit with shared expression variables).
- [x] FEFF85L pure-Rust module execution workflow (`resolve_feff_commands`, `run_feff`, `run_feff_and_load_paths`) starting from a provided FEFF executable path.

## FEFF Fitting MVP Boundaries

- Rust core only in this release (`crates/xraytsubaki`).
- FEFF85L execution path supports deterministic module resolution (`feff8l_rdinp`, `feff8l_pot`, `feff8l_xsph`, `feff8l_pathfinder`, `feff8l_genfmt`, `feff8l_ff2x`) and output discovery.
- Existing parse-only workflows with pre-generated `feffNNNN.dat` files remain supported.
- Single-dataset R-space fitting only.
- FEFF10 parsing/execution are reserved and return typed unsupported errors in this MVP.

See `crates/xraytsubaki/doc/feff-fitting-mvp.md` for details and FEFF10 follow-up compatibility notes.

## Future Developments

Expand Down
96 changes: 96 additions & 0 deletions crates/xraytsubaki/benches/feff_fitting_batch_benchmark.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
mod perf;

use criterion::{black_box, criterion_group, criterion_main, Criterion, Throughput};
use nalgebra::DVector;
use perf::FlamegraphProfiler;
use std::num::NonZeroUsize;
use xraytsubaki::xafs::fitting::{
feffit_independent, feffpath, ff2chi, FeffBatchOptions, FeffFitDataset, FeffFlavor,
FitVariable, FitVariables, PathParamSpec,
};

pub const TOP_DIR: &str = env!("CARGO_MANIFEST_DIR");

fn fixture_dataset() -> FeffFitDataset {
let pathfile = format!("{TOP_DIR}/tests/testfiles/feffcu01.dat");
let mut path = feffpath(&pathfile, FeffFlavor::Feff85L).unwrap();
path.s02 = PathParamSpec::Expression("amp".to_string());
path.e0 = PathParamSpec::Expression("de0".to_string());
path.sigma2 = PathParamSpec::Expression("sig2".to_string());
path.deltar = PathParamSpec::Expression("dr".to_string());

let k = DVector::from_iterator(140, (0..140).map(|i| 0.05 * (i as f64 + 1.0)));
let mut truth = FitVariables::new();
truth.insert("amp", FitVariable::new(0.9, false));
truth.insert("de0", FitVariable::new(1.1, false));
truth.insert("sig2", FitVariable::new(0.0031, false));
truth.insert("dr", FitVariable::new(0.01, false));
let synthetic = ff2chi(&[path.clone()], &truth, &k).unwrap();

FeffFitDataset::new()
.data(&k, &synthetic.chi)
.epsilon_k(1.0)
.add_path(path)
}

fn initial_variables() -> FitVariables {
let mut initial = FitVariables::new();
initial.insert("amp", FitVariable::new(0.95, true));
initial.insert("de0", FitVariable::new(0.0, true));
initial.insert(
"sig2",
FitVariable::new(0.002, true).with_bounds(Some(0.0), Some(0.02)),
);
initial.insert("dr", FitVariable::new(0.0, true));
initial
}

fn criterion_benchmark(c: &mut Criterion) {
let template = fixture_dataset();
let vars = initial_variables();
let batch = vec![template; 10_000];

let mut group = c.benchmark_group("feff_fitting_batch");
group.throughput(Throughput::Elements(batch.len() as u64));

let serial = FeffBatchOptions::sequential()
.with_chunk_size(NonZeroUsize::new(256).expect("nonzero constant"));
group.bench_function("feff_batch_independent_serial_10k", |b| {
b.iter(|| {
let out = feffit_independent(&batch, &vars, &serial);
let ok_count = out.iter().filter(|item| item.is_ok()).count();
black_box(ok_count)
})
});

let rayon = FeffBatchOptions::parallel()
.with_chunk_size(NonZeroUsize::new(256).expect("nonzero constant"));
group.bench_function("feff_batch_independent_rayon_10k", |b| {
b.iter(|| {
let out = feffit_independent(&batch, &vars, &rayon);
let ok_count = out.iter().filter(|item| item.is_ok()).count();
black_box(ok_count)
})
});

group.finish();
}

fn custom() -> Criterion {
let base = Criterion::default().sample_size(10);
let enable_profiler =
std::env::args().any(|arg| arg == "--profile-time" || arg.starts_with("--profile-time="));
if enable_profiler {
base.with_profiler(FlamegraphProfiler::new(1000))
} else {
base
}
}

criterion_group! {
name = benches;
config = custom();
targets = criterion_benchmark
}

criterion_main!(benches);
67 changes: 67 additions & 0 deletions crates/xraytsubaki/doc/feff-fitting-mvp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# FEFF Fitting MVP (Rust Core)

This document describes the scope and compatibility guarantees of the first FEFF fitting implementation added in `add-feff85l-xafs-fitting`.

## MVP Scope

Implemented now:
- Rust-core APIs in `xraytsubaki::xafs::fitting`.
- FEFF85L command discovery and module execution from a caller-provided executable path.
- FEFF85L path file parsing for `feffNNNN.dat` style files.
- FEFF path model synthesis with `path2chi` and summed `ff2chi`.
- Single-dataset nonlinear fitting in R-space with configurable transform window/range.
- Shared fit variables plus expression-bound path parameters.
- Reference fixtures and parity-oriented regression checks against xraylarch-generated data.

Out of scope in MVP:
- Python binding surface for fitting APIs.
- Multi-dataset/global fits.
- Non-R fitspaces (`k`, `q`, wavelet).
- FEFF10 execution support.

## Builder API and Canonical Examples

The redesigned fitting surface adds additive builder APIs (`FeffFit`, `Param`) and
multi-dataset fitting while keeping the existing free-function flow available.

Canonical usage examples are maintained in:
- `plans/fitting-api-redesign.md`

Those examples define the intended ergonomic workflows:
- path-model chaining and clone-and-reuse,
- tuple shorthand via `set_inits`,
- `Param`-based parameter declarations,
- single-dataset fitting with builder chaining,
- multi-dataset global fitting with shared parameters.

## Migration Guidance (Legacy -> Builder)

Legacy flow remains valid:
- Build `FeffFitDataset` and `FitVariables` manually.
- Call `feffit(&dataset, &vars)`.

Builder flow is additive:
- Build with `FeffFit::new().data(...).add_path(...).set_inits(...).fit()`.
- Use `add_dataset(...)` for global multi-dataset fitting.
- Use `params([Param::new(...), Param::fixed(...), Param::expr(...)])` for concise variable setup.

Compatibility policy:
- Existing entrypoints and single-dataset access patterns remain supported.
- Builder/multi-dataset APIs are additive.
- Deprecation is documentation-first; no mandatory migration is introduced in this change.

## FEFF Flavor Compatibility

- `FeffFlavor::Feff85L`: supported in this MVP.
- `FeffFlavor::Feff10`: intentionally returns a typed unsupported error.

This explicit dispatch behavior is intentional to keep caller contracts stable while FEFF10 parsing is implemented in a follow-up.

## FEFF10 Follow-up Path

The extension path is:
1. Implement FEFF10 parser normalization into the existing `FeffDat` model.
2. Keep `path2chi`, `ff2chi`, and `feffit` API signatures unchanged.
3. Add FEFF10 fixtures and parity tests beside FEFF85L fixtures.

No breaking API changes are expected for Rust callers when FEFF10 support is added.
Loading
Loading