sassafras 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. sassafras-0.5.0/LICENSE +28 -0
  2. sassafras-0.5.0/PKG-INFO +522 -0
  3. sassafras-0.5.0/README.md +472 -0
  4. sassafras-0.5.0/pyproject.toml +54 -0
  5. sassafras-0.5.0/sassafras/__init__.py +5 -0
  6. sassafras-0.5.0/sassafras/__main__.py +3 -0
  7. sassafras-0.5.0/sassafras/_guess_worker.py +39 -0
  8. sassafras-0.5.0/sassafras/backend.py +607 -0
  9. sassafras-0.5.0/sassafras/fit.py +303 -0
  10. sassafras-0.5.0/sassafras/guess.py +475 -0
  11. sassafras-0.5.0/sassafras/library.py +544 -0
  12. sassafras-0.5.0/sassafras/ml/SAS-55M-20k_LICENSE.txt +21 -0
  13. sassafras-0.5.0/sassafras/ml/model.pt +0 -0
  14. sassafras-0.5.0/sassafras/ml/sans_ml/__init__.py +0 -0
  15. sassafras-0.5.0/sassafras/ml/sans_ml/catalogue.py +107 -0
  16. sassafras-0.5.0/sassafras/ml/sans_ml/encode.py +51 -0
  17. sassafras-0.5.0/sassafras/ml/sans_ml/net.py +59 -0
  18. sassafras-0.5.0/sassafras/ml/sans_ml/predict.py +89 -0
  19. sassafras-0.5.0/sassafras/ml/sans_ml/priors.py +514 -0
  20. sassafras-0.5.0/sassafras/ml/sans_ml/simulate.py +270 -0
  21. sassafras-0.5.0/sassafras/multi.py +147 -0
  22. sassafras-0.5.0/sassafras/notebook.py +502 -0
  23. sassafras-0.5.0/sassafras/server.py +244 -0
  24. sassafras-0.5.0/sassafras/static/app.css +563 -0
  25. sassafras-0.5.0/sassafras/static/app.js +1095 -0
  26. sassafras-0.5.0/sassafras/static/copy.js +213 -0
  27. sassafras-0.5.0/sassafras/static/favicon-chalked.png +0 -0
  28. sassafras-0.5.0/sassafras/static/favicon-cooked.png +0 -0
  29. sassafras-0.5.0/sassafras/static/favicon-girlypop.png +0 -0
  30. sassafras-0.5.0/sassafras/static/favicon-washed.png +0 -0
  31. sassafras-0.5.0/sassafras/static/favicon.png +0 -0
  32. sassafras-0.5.0/sassafras/static/fitting.js +224 -0
  33. sassafras-0.5.0/sassafras/static/guess.js +100 -0
  34. sassafras-0.5.0/sassafras/static/index.html +152 -0
  35. sassafras-0.5.0/sassafras/static/logo-chalked-dark.png +0 -0
  36. sassafras-0.5.0/sassafras/static/logo-chalked-light.png +0 -0
  37. sassafras-0.5.0/sassafras/static/logo-cooked-dark.png +0 -0
  38. sassafras-0.5.0/sassafras/static/logo-cooked-light.png +0 -0
  39. sassafras-0.5.0/sassafras/static/logo-dark.png +0 -0
  40. sassafras-0.5.0/sassafras/static/logo-girlypop-dark.png +0 -0
  41. sassafras-0.5.0/sassafras/static/logo-girlypop-light.png +0 -0
  42. sassafras-0.5.0/sassafras/static/logo-light.png +0 -0
  43. sassafras-0.5.0/sassafras/static/logo-washed-dark.png +0 -0
  44. sassafras-0.5.0/sassafras/static/logo-washed-light.png +0 -0
  45. sassafras-0.5.0/sassafras/static/multi.js +571 -0
  46. sassafras-0.5.0/sassafras/static/notebook.js +53 -0
  47. sassafras-0.5.0/sassafras.egg-info/PKG-INFO +522 -0
  48. sassafras-0.5.0/sassafras.egg-info/SOURCES.txt +59 -0
  49. sassafras-0.5.0/sassafras.egg-info/dependency_links.txt +1 -0
  50. sassafras-0.5.0/sassafras.egg-info/entry_points.txt +2 -0
  51. sassafras-0.5.0/sassafras.egg-info/requires.txt +25 -0
  52. sassafras-0.5.0/sassafras.egg-info/top_level.txt +1 -0
  53. sassafras-0.5.0/setup.cfg +4 -0
  54. sassafras-0.5.0/tests/test_backend.py +116 -0
  55. sassafras-0.5.0/tests/test_fit.py +84 -0
  56. sassafras-0.5.0/tests/test_guess.py +59 -0
  57. sassafras-0.5.0/tests/test_library.py +123 -0
  58. sassafras-0.5.0/tests/test_mixture.py +88 -0
  59. sassafras-0.5.0/tests/test_multi.py +110 -0
  60. sassafras-0.5.0/tests/test_notebook.py +93 -0
  61. sassafras-0.5.0/tests/test_server.py +87 -0
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Oliver Hammond
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,522 @@
1
+ Metadata-Version: 2.4
2
+ Name: sassafras
3
+ Version: 0.5.0
4
+ Summary: Sassafras: Small Angle Scattering Structure- And Form-factor Rapid ASsessment. A SANS/SAXS data viewer with live sasmodels overlays, fitting and IQuana export
5
+ Author: Oliver Hammond
6
+ Maintainer: SasView developers
7
+ License-Expression: BSD-3-Clause
8
+ Project-URL: Homepage, https://github.com/SasView/sassafras
9
+ Project-URL: Source, https://github.com/SasView/sassafras
10
+ Project-URL: Issues, https://github.com/SasView/sassafras/issues
11
+ Project-URL: Changelog, https://github.com/SasView/sassafras/blob/main/CHANGELOG.md
12
+ Project-URL: DOI, https://doi.org/10.5281/zenodo.24041504
13
+ Keywords: small-angle scattering,SANS,SAXS,sasmodels,sasview,fitting,visualisation
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Framework :: Jupyter
24
+ Classifier: Topic :: Scientific/Engineering :: Physics
25
+ Classifier: Topic :: Scientific/Engineering :: Visualization
26
+ Requires-Python: >=3.10
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ License-File: sassafras/ml/SAS-55M-20k_LICENSE.txt
30
+ Requires-Dist: sasmodels>=1.0.7
31
+ Requires-Dist: sasdata>=0.9
32
+ Requires-Dist: numpy
33
+ Provides-Extra: fit
34
+ Requires-Dist: bumps>=0.9; extra == "fit"
35
+ Provides-Extra: notebook
36
+ Requires-Dist: iquana>=0.5; extra == "notebook"
37
+ Requires-Dist: jupyterlab; extra == "notebook"
38
+ Provides-Extra: guess
39
+ Requires-Dist: torch>=2.5; extra == "guess"
40
+ Requires-Dist: scipy; extra == "guess"
41
+ Requires-Dist: iquana>=0.5; extra == "guess"
42
+ Provides-Extra: library
43
+ Requires-Dist: pyarrow; extra == "library"
44
+ Provides-Extra: test
45
+ Requires-Dist: pytest; extra == "test"
46
+ Requires-Dist: bumps>=0.9; extra == "test"
47
+ Provides-Extra: all
48
+ Requires-Dist: sassafras[fit,guess,notebook]; extra == "all"
49
+ Dynamic: license-file
50
+
51
+ # Sassafras
52
+
53
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.24041504.svg)](https://doi.org/10.5281/zenodo.24041504)
54
+
55
+ **Small Angle Scattering Structure- And Form-factor Rapid ASsessment**
56
+
57
+ A lightweight SANS/SAXS analysis tool. Load reduced 1D data
58
+ in any format sasdata reads, pick a sasmodels form factor (optionally × a
59
+ structure factor), and drag sliders to overlay the model on the data —
60
+ chi-by-eye, with live χ² and residuals. With the **Fit** toggle off there is
61
+ no optimiser; the only automatic step is a one-click linear solve for scale
62
+ and background. Switch **Fit** on to refine the chi-by-eye starting point
63
+ with BUMPS (needs the optional `bumps` package).
64
+
65
+ It is a generalised local browser GUI with no external JavaScript, working with
66
+ any model and any (including no) data file.
67
+
68
+ Sassafras is part of the [SasView](https://www.sasview.org) ecosystem,
69
+ alongside sasmodels, sasdata and [IQuana](https://github.com/SasView/IQuana).
70
+
71
+ ## Install
72
+
73
+ ```bash
74
+ pip install sassafras # viewer: sasmodels, sasdata, numpy
75
+ pip install "sassafras[all]" # + fitting (bumps), IQuana notebooks, Guess (torch)
76
+ python -m sassafras.library fetch # optional: the ~290 MB curve library for Guess
77
+ ```
78
+
79
+ Extras on their own: `fit` (bumps), `notebook` (IQuana, JupyterLab), `guess`
80
+ (torch, scipy, IQuana). From a clone, `pip install -e ".[all,test]"`.
81
+
82
+ ## Run
83
+
84
+ ```bash
85
+ sassafras # or: python -m sassafras
86
+ sassafras run_1234.xml run_1235.h5 -m core_shell_sphere -s hardsphere
87
+ sassafras /data/reduced/ # load every readable file in a folder (≤ 50)
88
+ ```
89
+
90
+ A browser tab opens automatically. If it doesn't, open the printed
91
+ `http://127.0.0.1:…` URL. Keep the terminal running, and stop with Ctrl+C or the
92
+ **Stop server** button. Everything runs on your machine, and the server only
93
+ listens on 127.0.0.1.
94
+
95
+ Options: `--port 8765` · `--no-browser` · `-m MODEL` · `-s STRUCTURE_FACTOR` ·
96
+ `--check` (load, compile, evaluate once, print a JSON summary, exit).
97
+
98
+ sasmodels needs a C compiler to build model kernels (Xcode command-line tools
99
+ on macOS). The first use of each model compiles it, which takes about 1–2 s,
100
+ and sasmodels caches the result after that. OpenCL is off by default
101
+ (`SAS_OPENCL=none`); set that variable yourself to change it.
102
+
103
+ ## Data
104
+
105
+ * **Drop files** on the page, click to browse, or type a **file or folder
106
+ path** (path loads can later be re-read with ↻, which is useful while
107
+ reduction is still rewriting a file).
108
+ * Formats: whatever sasdata's `Loader` supports — canSAS XML, NXcanSAS
109
+ (`.h5 .hdf5 .nxs`), ASCII/CSV (`.txt .dat .csv`), IGOR `.abs/.cor`, Anton
110
+ Paar `.pdh`, and others. Multi-entry files give one dataset per entry. 2D data
111
+ are skipped with a message.
112
+ * Q is shown in Å⁻¹. sasdata converts nm⁻¹ files on load.
113
+ * Points with Q ≤ 0, non-finite values or dI ≤ 0 are dropped, and the dataset's
114
+ tooltip says so.
115
+ * **Active** dataset (radio button): its Q points and resolution are used for
116
+ the model, and it is the one scored. Pinhole dQ or slit (dxl/dxw) smearing
117
+ is applied by sasmodels exactly as in SasView.
118
+ * **Shown** datasets (colour swatch): drawn for comparison, e.g. sample vs
119
+ buffer, or successive runs.
120
+ * **Model only**: evaluates on a log-spaced grid (Q min, Q max, points in the
121
+ toolbar). You can still show datasets underneath.
122
+
123
+ ## Model and parameters
124
+
125
+ * Form factor list = all sasmodels 1D models, grouped by category, with a
126
+ filter box (type, then Enter).
127
+ * S(Q) = hardsphere, hayter_msa, squarewell, stickyhardsphere, …, combined as
128
+ `model@sf` by sasmodels, so you get `radius_effective`, `volfraction`,
129
+ `radius_effective_mode` and, where supported, `structure_factor_mode` (β
130
+ approximation). `radius_effective` is greyed out unless its mode is
131
+ "unconstrained".
132
+ * Every float parameter has an exact-value box, a slider, editable slider
133
+ min/max boxes and a **log** toggle. Initial ranges come from the sasmodels
134
+ defaults and limits. If you type a value outside the slider range, the range
135
+ widens to include it. Values outside sasmodels' hard limits are refused.
136
+ * Choice parameters get a dropdown. Shell counts such as `n` in
137
+ `core_multi_shell` rebuild the parameter list.
138
+ * **PD** opens polydispersity (σ/mean, distribution type; 35 points, 3σ) for
139
+ any polydisperse parameter.
140
+ * Switching model keeps values for parameters with the same name (scale,
141
+ background, sld_solvent, radius, …) but not polydispersity. Each model also
142
+ remembers its own last settings for the session, and the last model and
143
+ values are restored on the next launch in the same browser.
144
+ * **Background guess on load:** when a newly loaded dataset becomes active,
145
+ the background box is set to the mean I of its 3 highest-Q points
146
+ (once per file, so reloading the page keeps any value you've typed since).
147
+ **bg** (Match) re-applies it.
148
+ * **Scale / Scale+bg** (Match): a weighted linear least-squares solve
149
+ over the χ² window, with the model shape fixed. It's for getting the curve
150
+ onto the data before you start adjusting by eye.
151
+ * **Pin** (Curve) keeps the current curve, residuals and χ² as a dashed
152
+ reference to compare against.
153
+ * **Save / Load** (Parameters; JSON: model, values, PD, slider ranges, Q window)
154
+ and **CSV ↓** (Curve; Q, I, dI, model, residual, in-window flag).
155
+ * These buttons sit in a compact bar at the top right of the plot panel, next
156
+ to the χ²/N readout; hover any of them for the full description.
157
+
158
+ ## Fitting (optional)
159
+
160
+ Install `bumps` (`pip install "sassafras[fit]"`) and restart
161
+ sassafras. A **Fit** toggle next to **Defaults** in the Parameters panel then shows the fitting controls. With
162
+ it off, or without bumps installed, the page is the plain chi-by-eye viewer.
163
+
164
+ 1. Get close by eye: sliders, **Bkg from high Q**, **Match scale + bkg**.
165
+ 2. Tick the parameters to vary. A **fit** tick also appears in each open PD
166
+ panel. Ticks are remembered per model.
167
+ 3. **The slider's min/max boxes are the fit bounds** (0–1 for PD widths), so
168
+ narrow them to keep the fit physical.
169
+ 4. Pick a method and press **Fit**:
170
+ * **Levenberg–Marquardt**: fast, local, with ± from the covariance. The
171
+ default.
172
+ * **Nelder–Mead simplex**: local and robust to rough χ² surfaces.
173
+ * **Differential evolution**: global search within the bounds. Slower.
174
+ * **DREAM**: MCMC, so the ± values come from the posterior. The slowest
175
+ (seconds to minutes).
176
+ 5. The fit runs in the background with live step and χ²ᵣ readouts.
177
+ **Cancel** keeps the best point found so far.
178
+
179
+ When it finishes:
180
+
181
+ * the sliders move to the fitted values;
182
+ * each fitted parameter shows its ± 1σ uncertainty (flagged ⚑ if it ended on
183
+ a bound: widen that range and refit);
184
+ * the starting curve is pinned as "before fit";
185
+ * **Undo fit** restores the previous values.
186
+
187
+ The fit uses the same model, resolution (dQ or slit) and χ² window as the
188
+ overlay, through `sasmodels.bumps_model`.
189
+
190
+ Two χ² values are shown side by side. **Fit χ²ᵣ (BUMPS)** is
191
+ Σ((I − model)/dI)² / (N − p), the usual reduced χ². **χ²/N** is the
192
+ fit-agnostic number used everywhere else. Change any value by hand and the
193
+ ± values disappear and the fit χ²ᵣ is struck through, because they no longer
194
+ describe the parameters on screen.
195
+
196
+ **Copy** and **Save parameters** include the method, χ²ᵣ, degrees of freedom,
197
+ uncertainties and any parameters at a bound while the fit is current.
198
+
199
+ ## Mix (several form factors)
200
+
201
+ Tick **Mix** (Model panel) to sum up to four form factors, e.g. particles plus
202
+ a low-Q power law, or two populations. The form factor and S(Q) selected above
203
+ stay as form factor 1 (the only one that can carry a structure factor); **+
204
+ form factor** adds the next, × removes one, and unticking Mix goes back to a
205
+ single model, keeping form factor 1's values and polydispersity.
206
+
207
+ * This is sasmodels' mixture (`sphere@hardsphere+power_law+cylinder`): one
208
+ global **scale** and **background**, and per-form parameters prefixed `A_`,
209
+ `B_`, `C_`, `D_`. The panel shows form factor 1's parameters by their plain
210
+ names (its own scale as **scale ₁**) and the others with their index
211
+ (**radius ₂**, **power ₂**, …), listed after form factor 1; hover a label
212
+ for the sasmodels name, which is also what fits, reports and JSON use.
213
+ * A newly added form factor's scale is set to about 10% of the rest of the
214
+ model at every Q, so it doesn't swamp the curve; adjust from there.
215
+ * Fitting, Match, Multi (shared/per-dataset/expressions on any of these
216
+ parameters) and the IQuana notebook all work with mixtures. The notebook
217
+ uses IQuana's raw `set_model('sphere@hardsphere+power_law')`, which keeps
218
+ the same `A_`/`B_` names, and `MultiFitter.add(..., model=...)` with the same
219
+ string; IQuana's Bayesian sampler doesn't take mixtures, so a DREAM setting
220
+ exports as simplex.
221
+ * **Guess** suggests single models: applying a suggestion turns Mix off.
222
+
223
+ ## Multi (several datasets at once)
224
+
225
+ Tick **Multi** (next to **Fit** in the Parameters panel) to work on every
226
+ ticked dataset together — a contrast series, a concentration series, SANS +
227
+ SAXS — on the same page:
228
+
229
+ * **Members** are the ticked (drawn) datasets. The active (radio) one is the
230
+ one the parameter panel edits; click another radio to edit that dataset.
231
+ Every member's model curve is drawn in its dataset colour, the residual plot
232
+ shows them all, and the χ²/N readout is the combined value (hover it for
233
+ each dataset's).
234
+ * **all / each** next to each parameter: *all* means one shared value, slider
235
+ range and polydispersity for every member; *each* means every dataset has its
236
+ own (scale, background and solvent SLDs start as *each*, the rest as *all*).
237
+ * **ƒ** (on an *each* parameter) sets this dataset's value by an expression of
238
+ other parameters, e.g. `0.8 * h2o.scale` or
239
+ `(h2o.background + d2o.background) / 2`. Datasets are referred to by the
240
+ short names shown in the Multi line (from the file names); a bare name such
241
+ as `scale` means the same dataset. Numbers, `+ - * /`, parentheses and
242
+ integer powers are allowed, as in IQuana; expressions are parsed, never
243
+ executed, and cycles are refused.
244
+ * **Fit** (with the Fit toggle on) runs one joint BUMPS fit: shared parameters
245
+ are one fitted quantity, expression parameters are derived, and the fit
246
+ ticks apply to every dataset (bounds: the slider range — per dataset for
247
+ *each* parameters). **Undo fit** restores every dataset.
248
+ * **IQuana notebook ↗** writes a `MultiFitter` notebook: one `fit.add(...)`
249
+ per dataset with its own values, bounds, PD, Q range and resolution, then
250
+ `fit.share(...)` and `fit.constrain(...)`, a check that prints each
251
+ dataset's χ²/N next to sassafras's, the joint fit, `fit.get_fit_report()`
252
+ and `fit.save_results(...)` (parameters, datasets, covariance and curves as
253
+ CSV). IQuana has no joint DREAM yet, so a DREAM setting exports as simplex.
254
+ * **Copy to clipboard → Params**, **Save** and **Load** cover all members (a table with one
255
+ column per dataset; the JSON restores the datasets it can find by path).
256
+ * All members share the model and the χ² window. **Guess** still looks at the
257
+ active dataset only and says so when Multi is on.
258
+
259
+ ## Guess (ML model suggestion)
260
+
261
+ **Guess** (Model panel) passes the active dataset, inside the χ² window, to the
262
+ trained network from the `ml-sans` project. It shows the ranked sasmodels
263
+ suggestions with their probabilities, and loads the top one sassafras can
264
+ handle. Click any other row to load that model instead.
265
+
266
+ The network ranks models only; it doesn't predict parameters. Starting values
267
+ come from a quick prior search:
268
+
269
+ 1. Up to 300 parameter sets for the suggested model (about 4 s) are drawn from
270
+ the same physical priors the network was trained on (`sans_ml.priors`,
271
+ including PD and the S(Q) effective-radius handling).
272
+ 2. Each set is scored against the data with sassafras's resolution-smeared
273
+ evaluation and the closed-form scale/background solve.
274
+ 3. If `bumps` is installed, the best three draws get a short, time-limited
275
+ Levenberg–Marquardt polish of the parameters the prior varies.
276
+
277
+ The result is a starting point, not a final fit; refine it with the sliders or
278
+ the Fit toggle.
279
+
280
+ * Two-component suggestions (`A+B`) are listed but can't be loaded yet.
281
+ * As the ml-sans README notes, the network does well on simulated-like curves
282
+ and less well on measured data, so treat the top three as the answer.
283
+ * Setup: the trained model is bundled, so only the Python packages are
284
+ needed: `pip install "sassafras[guess]"` (`torch`, `scipy`, `iquana`) in the same
285
+ environment as sassafras. Without them the Guess button is dimmed; clicking it shows what is missing,
286
+ and the terminal prints a `Guess: ready` or `Guess: unavailable` line at
287
+ start-up.
288
+ * The network runs in a short-lived child process, so PyTorch is never loaded
289
+ into the sassafras server. (On macOS, PyTorch and numpy/sasmodels can bring
290
+ conflicting OpenMP runtimes, which aborts the process that loads both.)
291
+ * `sassafras/ml` holds `model.pt` and the inference subset of ml-sans's
292
+ `sans_ml` package (`__init__`, `catalogue`, `encode`, `net`, `predict`,
293
+ `priors`, `simulate`); training and data-generation code stays in ml-sans.
294
+ After retraining, refresh the copy from the ml-sans folder:
295
+
296
+ ```bash
297
+ cp ../ml-sans/model.pt sassafras/ml/
298
+ for f in __init__ catalogue encode net predict priors simulate; do
299
+ cp ../ml-sans/sans_ml/$f.py sassafras/ml/sans_ml/; done
300
+ ```
301
+
302
+ To try a new model without copying, start with `--ml-sans /path/to/ml-sans`
303
+ (or set `SASSAFRAS_ML_SANS`).
304
+ * S(Q) can now be added to any form factor except those that already include
305
+ inter-particle structure or describe no finite particle. This is the same
306
+ rule ml-sans uses, so its P@S suggestions load.
307
+
308
+ ### Curve library (SAS-55M-20k)
309
+
310
+ When a curve-library index is installed, **Guess** uses it instead of the
311
+ network. SAS-55M-20k (the Sasformer training set) holds 1.1 million noise-free
312
+ sasmodels curves: 55 form factors × 20,000 random parameter sets on 512 Q
313
+ points from 0.001 to 1 Å⁻¹, each stored with its structural parameters.
314
+
315
+ * **Ranking:** every library curve is interpolated onto the active dataset's Q
316
+ points inside the χ² window and given its own weighted least-squares scale
317
+ and background, so only the shape counts. The list shows each model's best
318
+ χ²/N (unsmeared, so it is a ranking rather than a fit quality). The match
319
+ takes 1–3 s.
320
+ * **Starting values:** the three closest curves of the chosen model supply
321
+ their stored parameters. SLDs and polydispersity are not stored, so
322
+ sassafras keeps the default SLDs (scale absorbs the contrast), scans Schulz
323
+ polydispersity widths 0–0.3 on the parameters the generator made
324
+ polydisperse (fewer of them for slow kernels), then runs the short LM
325
+ polish. For models whose shape depends on contrast ratios (two or more
326
+ particle SLDs, e.g. core–shell), the polish also moves those SLDs.
327
+ * **S(Q) sub-library:** SAS-55M-20k itself has no structure factors (apart
328
+ from `binary_hard_sphere`), so a second index adds P×S curves. It takes 3,000
329
+ random curves of each of the 20 particle form factors that have a defined
330
+ effective radius (spheres, core–shells, vesicles, cylinders, ellipsoids,
331
+ prisms, …; not the shape-independent, lamellar or flexible-chain models),
332
+ and multiplies each by four structure factors:
333
+
334
+ | S(Q) | weak | strong |
335
+ |---|---|---|
336
+ | `hardsphere` | φ 0.02–0.10 | φ 0.20–0.40 |
337
+ | `hayter_msa` (water, 298 K) | φ 0.005–0.05, charge 5–20 e, salt 0.01–0.1 M | φ 0.02–0.15, charge 20–60 e, salt 0–0.005 M |
338
+
339
+ S(Q) is evaluated at the form's effective radius (equivalent-volume sphere
340
+ where defined) × 1.0–1.1, on the same Q points, as
341
+ I = (I_stored − 0.001)·S(Q) + 0.001, so each curve keeps the generator's
342
+ polydispersity and SLDs. Settings where Hayter–MSA has no physical solution
343
+ (S ≤ 0, mostly large particles at weak charge) are dropped, leaving about
344
+ 205,000 curves (~90 MB). The lookup searches both indexes at once, and a
345
+ `form@sf` match loads with `radius_effective_mode` "unconstrained" at the
346
+ stored radius_effective; temperature and dielectric constant stay fixed in
347
+ the polish.
348
+ * **Limits:** P×S is only covered for the S(Q) settings above, and
349
+ two-population mixtures aren't covered at all.
350
+ Where the generator coupled parameters or depended on the unknown SLDs, the
351
+ stored values are only approximate starting points.
352
+ * **Setup (quick):** download the prebuilt indexes (~290 MB, ~480 MB
353
+ unpacked; [doi:10.5281/zenodo.24080759](https://doi.org/10.5281/zenodo.24080759)),
354
+ checksum-verified, into your per-user data folder:
355
+
356
+ ```bash
357
+ python -m sassafras.library fetch
358
+ ```
359
+
360
+ The folder is `~/Library/Application Support/sassafras` on macOS,
361
+ `~/.local/share/sassafras` on Linux and `%LOCALAPPDATA%\sassafras` on
362
+ Windows; set `SASSAFRAS_DATA` to use another. It survives upgrades and is
363
+ shared by every Python environment. Indexes built or fetched into
364
+ `sassafras/ml/` by versions before 0.5 are still found.
365
+
366
+ * **Setup (from the dataset):** or download SAS-55M-20k
367
+ ([Figshare](https://doi.org/10.6084/m9.figshare.21716618), ~4.5 GB) and build
368
+ the index once (about 20 s; it needs `pyarrow`, `pip install -e ".[library]"`):
369
+
370
+ ```bash
371
+ python -m sassafras.library build ~/Downloads/sas-55m-20k
372
+ ```
373
+
374
+ This writes about 400 MB to `library` in the data folder. Then build the
375
+ S(Q) sub-library (about 30 s, ~90 MB in `library_sq`; `--per-model` sets
376
+ the size):
377
+
378
+ ```bash
379
+ python -m sassafras.library build-sq ~/Downloads/sas-55m-20k
380
+ ```
381
+
382
+ Use `--out` to put either elsewhere and start sassafras with `--library DIR`
383
+ / `--library-sq DIR` (or set `SASSAFRAS_LIBRARY` / `SASSAFRAS_LIBRARY_SQ`). Without an index, Guess uses the ml-sans network as
384
+ before.
385
+
386
+ * **Data credit and licence:** SAS-55M-20k is by B. Yildirim, J. Doutch and
387
+ J. M. Cole (*Digital Discovery* **3**, 694–704, 2024) and is MIT-licensed;
388
+ the licence is in `sassafras/ml/SAS-55M-20k_LICENSE.txt` and is shipped as
389
+ `DATA_LICENSE.txt` inside the release archive. To publish a rebuilt index:
390
+ `python -m sassafras.library pack sassafras-library-vN.tar.gz`, upload it as
391
+ a new version of the library's Zenodo record (and attach it to a
392
+ `library-vN` GitHub release as a mirror), then update `RELEASE_TAG`,
393
+ `RELEASE_FILE`, `RELEASE_SHA256` and `ZENODO_URL` in `sassafras/library.py`.
394
+
395
+ ## IQuana notebook
396
+
397
+ **IQuana notebook ↗** (top right of the plot panel) writes a Jupyter notebook with
398
+ [IQuana](https://sasview.github.io/IQuana/) code that reproduces what is on
399
+ screen, then opens it in JupyterLab. It covers the data, model and structure
400
+ factor, every parameter value, polydispersity, the Q window and the
401
+ resolution. When the Fit toggle is on it also sets up the same fit: the ticked
402
+ parameters with the slider ranges as bounds, and the same method (LM, simplex
403
+ and DE → `fit(engine='bumps', method=…)`, DREAM → `fit_bayesian()`).
404
+
405
+ * Notebooks are saved in `~/sassafras_notebooks` (change it with
406
+ `--notebook-dir`). One JupyterLab server rooted there is started on first
407
+ use and reused afterwards; it keeps running after sassafras stops, so open
408
+ notebooks aren't lost.
409
+ * A check cell prints IQuana's χ²/N next to the sassafras value. IQuana builds
410
+ kernels in single precision, so they agree to about 1 part in 10⁴.
411
+ * Path-loaded files are read from their original location. Uploaded files,
412
+ and files that sassafras rescaled or trimmed on load, are saved next to the
413
+ notebook as a CSV snapshot (Q, I, dI, dQ).
414
+ * Effective radius: sasmodels' `radius_effective_mode` "unconstrained" maps
415
+ to IQuana's `'unconstrained'`, and "radius" to `'link_radius'`. Other
416
+ computed modes (equivalent volume sphere, excluded volume, …) have no IQuana
417
+ equivalent, so `radius_effective` is fixed at the value sasmodels computes
418
+ for the current geometry, and the notebook says so.
419
+ * Models with per-shell vector parameters (e.g. `core_multi_shell`) can't be
420
+ addressed shell by shell in IQuana 0.5, so they are refused with a message.
421
+ * Needs `iquana` and `jupyterlab` in the same Python environment:
422
+ `pip install "sassafras[notebook]"`.
423
+
424
+ ## Copy to clipboard
425
+
426
+ Buttons above the plot put content on the clipboard, ready to paste into
427
+ OneNote, an ELN, Word, Excel or a plain-text cell:
428
+
429
+ * **All**: title line, figure and parameter table as one rich paste. Plain-text
430
+ editors get the text summary instead.
431
+ * **Figure**: PNG of the data, model, pinned curve, residuals and legend, with
432
+ the dataset, model and χ²/N as a title. It is always drawn on a white
433
+ background, whatever the screen theme.
434
+ * **Parameters**: model, χ²/N with N and the Q window, and every parameter with
435
+ units and polydispersity. It pastes as a table, or as aligned text where
436
+ tables aren't supported.
437
+ * **Data**: Q, I, dI, model, residual and in-window flag at full precision. It
438
+ pastes as columns into Excel and as a table into notebooks.
439
+ * **PNG ↓**: downloads the figure instead.
440
+
441
+ ## Plot
442
+
443
+ **Plot type** (next to log Q / log I): **I(Q)**; **Kratky**, Q²·I(Q) vs Q
444
+ (linear axes by default), a quick check of folding, compactness or chain-like
445
+ scattering; or **Guinier**, ln I vs Q² (log axes off), where a straight
446
+ low-Q line gives Rg from its slope (−Rg²/3). Guinier opens on the low-Q region
447
+ where ln I has fallen by about 0.8, a little past the usual Q·Rg ≈ 1.3 limit;
448
+ drag to zoom as usual. Data, error bars, the model and any pinned curve are
449
+ all transformed; the residual panel keeps (I − model)/dI on the same Q axis.
450
+
451
+ Light and dark themes follow the system setting. The **Theme** button in the header switches between auto, light and dark, and the browser remembers your choice. The **Colour** button next to it cycles the colour scheme (accents, first data series and logo): chopped (green, default), cooked (orange), washed (blue), chalked (grey) and girlypop (pink); it is remembered too. Log/linear axes, drag a box to zoom (a horizontal drag zooms Q only),
452
+ double-click or **Reset zoom** to undo, and hover for point values. The χ² window
453
+ greys out points outside it and restricts both χ² and Match.
454
+
455
+ χ²/N = Σ((I − model)/dI)² ÷ (points in window). No parameters are fitted, so
456
+ this is a number for comparing settings, not a goodness-of-fit statistic. To
457
+ compare with a BUMPS/SasView fit's reduced χ², multiply by N/(N − p).
458
+
459
+ ## Checks
460
+
461
+ `python -m pytest tests` covers loading every example format, nm⁻¹
462
+ conversion, product and polydisperse models, vector parameters, the
463
+ scale/background solve, error reporting and every HTTP endpoint. One
464
+ regression test reproduces the `elliptical_cylinder_gui` starting curve to
465
+ within 2 × 10⁻⁶ relative, and its BUMPS χ² (561.675) after the N/(N − p)
466
+ rescale. With bumps installed, `tests/test_fit.py` checks that LM and
467
+ simplex fits recover known parameters within their uncertainties, that the Q
468
+ window and PD widths are respected, that cancellation works and that bad
469
+ requests are rejected. Those tests are skipped when bumps is absent.
470
+
471
+ `examples/` contains measured flexible elliptical cylinder SANS data
472
+ (`flexible_cylinder_elliptical.dat`; four columns Q (Å⁻¹), I, dI (cm⁻¹), dQ (Å⁻¹), with
473
+ metadata removed), synthetic sphere × hard-sphere data (canSAS XML and
474
+ NXcanSAS), and a synthetic SAXS core–shell dataset written in nm⁻¹.
475
+
476
+ ## Layout
477
+
478
+ ```
479
+ sassafras/backend.py data loading (sasdata), model catalogue, evaluation (sasmodels)
480
+ sassafras/server.py stdlib HTTP server + CLI
481
+ sassafras/fit.py optional BUMPS fitting (background jobs, progress, cancel; joint multi-dataset fits)
482
+ sassafras/multi.py Multi: request checks and safe expressions shared by the joint fit and the export
483
+ sassafras/notebook.py IQuana notebook export and JupyterLab launcher
484
+ sassafras/guess.py Guess: ml-sans model suggestion + prior-search starting values
485
+ sassafras/_guess_worker.py child process that runs the ml-sans network (torch)
486
+ sassafras/library.py curve-library index (build) and shape matching for Guess
487
+ sassafras/ml/ bundled model.pt + sans_ml inference code (copied from ml-sans)
488
+ sassafras/static/ index.html, app.js (SVG plotting, controls), copy.js (clipboard),
489
+ fitting.js (Fit toggle and controls), multi.js (Multi), notebook.js, guess.js, app.css, logos + favicon
490
+ ```
491
+
492
+ The backend can be used without the GUI:
493
+
494
+ ```python
495
+ from sassafras.backend import Session
496
+ s = Session()
497
+ ds, _ = s.load_path("run.xml")
498
+ out = s.evaluate({"model": "sphere", "structure_factor": "hardsphere",
499
+ "params": {"radius": 60, "volfraction": 0.2}, "dataset": ds[0].id})
500
+ out["chisq"]
501
+ ```
502
+
503
+ Tested with Python 3.13, sasmodels 1.1.0, sasdata 0.12.2, bumps 1.0.3 and numpy 2.5.3, in
504
+ headless Chromium (light and dark themes, desktop and phone widths).
505
+
506
+ ## Contributing
507
+
508
+ Issues and pull requests are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md).
509
+
510
+ ## Citing
511
+
512
+ If sassafras helps your work, please cite it:
513
+ [doi:10.5281/zenodo.24041504](https://doi.org/10.5281/zenodo.24041504). That DOI
514
+ always resolves to the latest version; each release also has its own DOI on
515
+ that page. GitHub's "Cite this repository" button gives the same reference
516
+ from `CITATION.cff`.
517
+
518
+ ## Licence
519
+
520
+ BSD 3-Clause; see [LICENSE](LICENSE). Sassafras uses
521
+ [sasmodels](https://github.com/SasView/sasmodels) and
522
+ [sasdata](https://github.com/SasView/sasdata), which are also BSD-licensed.