SANS Model Fitter
A flexible, model-agnostic Python template for fitting Small-Angle Neutron Scattering (SANS) data using the SasModels library.
Features
- Model-Agnostic Design: Works with any model from the SasModels library (cylinder, sphere, core_shell, etc.)
- Multiple Fitting Engines: Supports both BUMPS (default) and LMFit optimization engines
- Flexible Data Loading: Reads CSV, XML, and HDF5 formats via sasdata
- Example Data Built In: A curated set of ready-to-fit datasets plus a simulator that generates data —
examples.load_fitter('silica_spheres')orexamples.simulate('sphere', radius=50) - Q-Range Restriction: Fit only a chosen [qmin, qmax] window (e.g. trim beam-stop or background-dominated points)
- Dataset Arithmetic: Add, subtract, multiply, and divide datasets (or scale by constants) with propagated uncertainties via
data_ops— e.g. background subtraction and transmission correction before fitting - User-Friendly Parameter Management: Easy-to-use interface for setting parameter values, bounds, and fitting flags
- Theory Preview: See the model at the current parameters before fitting —
plot_model()for data, curve and residuals,calculate()for the intensities on any Q grid, andcompare()to overlay several parameter sets - Interactive Visualization: Automatic plotting of data, fitted model, and residuals with Plotly
- Bayesian Analysis: Posterior sampling with BUMPS DREAM (MCMC) plus corner, marginal, predictive-band, correlation, and trace plots
- P(r) Inversion: Model-free pair distance distribution analysis (indirect Fourier transform) via
pr_inversion— D_max exploration, automatic regularization/term selection, and Rg/I(0)/positivity diagnostics - Fit-Quality Reporting: Reduced χ² alongside raw χ², point and parameter counts, covariance/correlation matrices, a convergence verdict and a warning when a fitted parameter rests on a bound — all via
fitter.get_fit_report(), which renders as a table in a notebook, as text, or as Markdown - Result Export: Save fitted parameters and curves to CSV files
Installation
Option 1: Using pip (recommended for users)
# Clone the repository
git clone https://github.com/ai4se1dk/SANS-fitter.git
cd SANS-fitter
# Install the package
pip install -e .
# Or install with development dependencies
pip install -e ".[dev]"
# The Jupyter stack is optional - only needed to run notebooks/
pip install -e ".[notebooks]"
Option 2: Using Pixi (recommended for development)
# Clone the repository
git clone https://github.com/ai4se1dk/SANS-fitter.git
cd SANS-fitter
# Install dependencies with Pixi
pixi install
# Run tests
pixi run test
# Run demo notebook
pixi run run-demo
Quick Start
No data of your own yet? Start from a bundled example:
from sans_fitter import examples
examples.describe() # see what's available
fitter = examples.load_fitter('silica_spheres') # data + model + parameters
result = fitter.fit()
fitter.plot_results()
Or simulate data, so you can check the fit recovers it:
data = examples.simulate('sphere', radius=50, noise=0.02, seed=0)
data.truth['radius'] # 50.0
See the Example Data guide for the full collection.
With your own data:
from sans_fitter import SANSFitter
# Create fitter instance
fitter = SANSFitter()
# Load your data
fitter.load_data('my_sans_data.csv')
# Set the model (any model from SasModels!)
fitter.set_model('cylinder')
# Optionally restrict the Q range used for fitting
fitter.set_q_range(qmin=0.01, qmax=0.3)
# View initial parameter values
fitter.get_params()
# Configure parameters for fitting
fitter.set_param('radius', value=20, min=1, max=100, vary=True)
fitter.set_param('length', value=400, min=10, max=1000, vary=True)
fitter.set_param('scale', value=0.1, min=0, max=1, vary=True)
fitter.set_param('background', value=0.01, min=0, max=1, vary=True)
# View current parameters
fitter.get_params()
# Check the starting values against the data before fitting
fitter.plot_model()
# Perform the fit (using BUMPS by default)
result = fitter.fit(engine='bumps', method='amoeba')
# Visualize results
fitter.plot_results(show_residuals=True)
# Judge the fit: reduced chi-squared, correlations, bound checks
report = fitter.get_fit_report()
print(report)
# Save results
fitter.save_results('fit_results.csv')
# Save the whole analysis, and reopen it later or elsewhere
fitter.save_analysis('my_analysis.json')
fitter = SANSFitter.load_analysis('my_analysis.json')
# One shareable document: settings, tables and the plot
fitter.report('my_analysis.html')
Switching Models
The fitter is completely model-agnostic. Simply load a different model:
# Try with a sphere model instead
fitter.set_model('sphere')
fitter.get_params() # See different parameters!
fitter.set_param('radius', value=25, min=5, max=100, vary=True)
result = fitter.fit()
Switching Fitting Engines
# Use BUMPS (default)
result = fitter.fit(engine='bumps', method='amoeba')
# Or use LMFit
result = fitter.fit(engine='lmfit', method='leastsq')
Working with Structure Factors
Combine any SasModels form factor with an interaction model to capture correlated systems.
fitter.set_model('sphere')
# Apply a structure factor (creates sphere@hardsphere product model)
fitter.set_structure_factor('hardsphere', radius_effective_mode='link_radius')
# Inspect linked parameters and run the fit as usual
fitter.get_params()
result = fitter.fit()
# Remove the structure factor to go back to the pure form factor
fitter.remove_structure_factor()
- Structure factors: queried from sasmodels at runtime — call
get_structure_factors()for the full list (currently includeshardsphere,hayter_msa,squarewell,stickyhardsphere,two_yukawa). - Radius handling: use
radius_effective_mode='link_radius'to keepradius_effectiveequal to the form-factorradius, or leave the defaultunconstrainedto fit it independently. - State helpers:
get_structure_factor()returns the active structure factor so notebooks/scripts can branch as needed.
Combining Models
Describe a dataset with several models at once — for example a low-Q diffuse feature plus a high-Q peak — fitted simultaneously against one dataset:
fitter = SANSFitter()
fitter.load_data('data.csv')
# Combine models; parameters get friendly per-model prefixes
fitter.set_models('dab', 'peak_lorentz')
fitter.set_param('dab_cor_length', value=50, min=1, max=500, vary=True)
fitter.set_param('peak_lorentz_peak_pos', value=0.1, min=0.01, max=0.5, vary=True)
fitter.set_param('background', value=0.001, min=0, max=0.1, vary=True)
result = fitter.fit(engine='bumps')
# See which feature each model accounts for
fitter.plot_results(show_components=True)
The combined intensity is
$I(q) = \text{scale} \cdot \sum_i \text{scale}_i \cdot I_i(q) + \text{background}$
so the global scale and background are shared by all components natively,
while each component has its own <model>_scale.
Custom names (monikers) — for long model names, duplicates, or physics labels:
fitter.set_models(small='sphere', large='sphere', shared=['sld', 'sld_solvent'])
fitter.set_param('small_radius', value=20, vary=True)
fitter.set_param('large_radius', value=200, vary=True)
Sharing parameters — any name in shared=[...] that exists in ≥ 2
components becomes a single unprefixed parameter driving all of them
(sld above). Polydispersity configuration stays per-component under the
prefixed names.
Component curves — after fitting a '+' mixture, plot_results(show_components=True)
overlays one dashed curve per component, each drawn as
scale · part_scale · I_part(q) (background shown implicitly in the total).
Advanced: the raw sasmodels expression syntax is also available and keeps sasmodels' native
A_/B_names:fitter.set_model('dab+peak_lorentz'). For after-the-fact or asymmetric sharing, use equality links:fitter.link_params('large_sld', to='small_sld')andfitter.unlink_params('large_sld'). Composite models and parameter links currently require thebumpsengine.
Bayesian Analysis
Sample the full posterior distribution of the varying parameters with the DREAM MCMC sampler (built into BUMPS — no extra dependencies):
fitter.set_model('sphere')
fitter.set_param('radius', value=50, min=10, max=200, vary=True)
fitter.set_param('scale', value=0.1, min=0.01, max=1.0, vary=True)
# Run the Bayesian fit (prints point estimates + credible intervals + diagnostics)
result = fitter.fit_bayesian(samples=10000, burn=200)
# Corner plot of the posterior
fitter.plot_posterior_pairs()
# More displays
fitter.plot_param_distribution('radius') # marginal posterior
fitter.plot_posterior_predictive() # 95% credible band over the data
fitter.plot_param_correlations() # correlation heatmap
fitter.plot_trace() # MCMC chain traces
# Raw chain access / export
posterior = fitter.get_posterior()
posterior.save_posterior_csv('posterior_chain.csv')
See the User Guide for details.
Available Methods
BUMPS methods:
'amoeba'- Nelder-Mead simplex (default, robust)'lm'- Levenberg-Marquardt'newton'- Newton's method'de'- Differential evolution
LMFit methods:
'leastsq'- Levenberg-Marquardt (default)'least_squares'- Trust Region Reflective'differential_evolution'- Global optimizer'powell','nelder', etc.
Demo Notebooks
- notebooks/sans_fitter_demo.ipynb — comprehensive demonstration of the fitting workflow with examples.
- notebooks/bayesian_sampling.ipynb — Bayesian posterior sampling API (
fit_bayesian()) and the associated posterior plots. - notebooks/pr_inversion_demo.ipynb — model-free P(r) inversion: D_max exploration, automatic inversion, and diagnostics.
- notebooks/resolution_control.ipynb — explicit resolution (smearing) control: the four modes, what each hands sasmodels, and what ignoring resolution costs.
Design Philosophy
This implementation follows a template pattern where:
- The core fitting logic is abstracted into a reusable class
- Models are loaded dynamically from SasModels - no hardcoded model assumptions
- Parameters are discovered automatically from the model definition
- Multiple optimization engines are supported through a unified interface
- The user maintains full control over parameter initialization and bounds
Implementation Details
Engine Adapters
The fitter implements adapter patterns for both BUMPS and LMFit:
- BUMPS: Uses native
sasmodels.bumps_modelintegration - LMFit: Uses
sasmodels.direct_model.DirectModelwith a custom residual function
Parameter Management
Parameters are stored internally with:
value: Current/initial valuemin,max: Boundsvary: Fitting flagdescription: From model metadata
This allows the fitter to work with any model without prior knowledge of its parameters.
License
BSD 3-Clause License. See LICENSE for the full text.
References
- SasModels: https://github.com/SasView/sasmodels
- BUMPS: https://github.com/bumps/bumps
- LMFit: https://lmfit.github.io/lmfit-py/
- Plotly: https://plotly.com/python/
Metadata
Release files for sans-fitter 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sans_fitter-0.4.0.tar.gz | 209.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sans_fitter-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 359.7 kB
Release files / sans_fitter-0.4.0.tar.gz
| Download URL | sans_fitter-0.4.0.tar.gz |
|---|---|
| Size | 209.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b2f6c0400a0761faf52b2435f7dc73581ce6e54b47af27257f4d0fbb901c6fa9
|
|
BLAKE2b-256 checksum How to use checksums |
c2d12d595e82ae2e9f487e14e50dfe8e4855839d9bfc1f4ebafeff12dce8c54b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency logRelease files / sans_fitter-0.4.0-py3-none-any.whl
| Download URL | sans_fitter-0.4.0-py3-none-any.whl |
|---|---|
| Size | 150.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
86f183d62195818c7d0715ef408fcb17ebbcecdb723163a7f209e17ec9eeba02
|
|
BLAKE2b-256 checksum How to use checksums |
88c232bae2895be66cea38fd448fe80b8831fa9f28083753fc76be38b22855d6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency log