diff --git a/.env b/.env index 0b92e42..28eadec 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -PYTHONPATH="src" \ No newline at end of file +PYTHONPATH="src" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ae0758d..3e36643 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,10 +18,10 @@ jobs: with: ref: main - - name: Setting up Python 3.11 + - name: Setting up Python 3.12 uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.12' cache: pip cache-dependency-path: | ci/requirements.txt diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 5fd9309..12b0128 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -26,6 +26,10 @@ jobs: uses: ./.github/workflows/build.yml secrets: inherit + standalone: + needs: [tests] + uses: ./.github/workflows/standalone.yml + docs: needs: [release] # get (release) version number first uses: ./.github/workflows/docs.yml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index f7eaace..ec891ad 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -21,10 +21,10 @@ jobs: with: ref: main - - name: Setting up Python 3.11 + - name: Setting up Python 3.12 uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.12' cache: pip cache-dependency-path: | ci/requirements.txt diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7397c2c..bf48d22 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,10 +16,10 @@ jobs: fetch-depth: 0 ref: main - - name: Setting up Python 3.11 + - name: Setting up Python 3.12 uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.12' cache: pip cache-dependency-path: | ci/requirements.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5907032..807d6b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,10 +25,10 @@ jobs: git config user.name "${GITHUB_ACTOR}" git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" - - name: Setting up Python 3.11 + - name: Setting up Python 3.12 uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.12' cache: pip cache-dependency-path: | ci/requirements.txt diff --git a/.github/workflows/standalone.yml b/.github/workflows/standalone.yml new file mode 100644 index 0000000..4c01912 --- /dev/null +++ b/.github/workflows/standalone.yml @@ -0,0 +1,60 @@ +name: Standalone + +on: + workflow_call: + workflow_dispatch: + +jobs: + standalone: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - name: Checking out the repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setting up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: | + ci/requirements.txt + + - name: Install required system packages + shell: sh + run: | + PLATFORM="$(echo "$RUNNER_OS" | tr '[:upper:]' '[:lower:]')" + REQFN="ci/requirements_$PLATFORM.txt" + if [ "$PLATFORM" = "linux" ] && [ -f "$REQFN" ]; then + sudo apt-get -y install $(sed -e '/^\s*#/d' "$REQFN") + elif [ "$PLATFORM" = "windows" ] && [ -f "$REQFN" ]; then + choco install $(sed -e '/^\s*#/d' "$REQFN") + else + echo "No extra standalone system packages declared for $PLATFORM." + fi + + - name: Install dependencies + shell: sh + run: | + [ -d "/c/miniconda" ] && /c/miniconda/condabin/activate.bat + python -m pip install --upgrade pip + python -m pip install --progress-bar=off -r ci/requirements.txt + + - name: Build standalone bundles + shell: sh + run: tox -e standalone -v + + - name: Upload standalone artifacts + uses: actions/upload-artifact@v4 + with: + name: standalone-${{ matrix.os }} + path: | + dist/standalone/*.zip + dist/standalone/*/README_STANDALONE.txt + dist/standalone/*/build_info.json diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ec5bc06..f177a5f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,14 +16,14 @@ jobs: matrix: python_arch: ['x64'] python: - - [3, 11] - [3, 12] - [3, 13] + - [3, 14] # os: ['ubuntu', 'windows', 'macos'] os: ['ubuntu'] env: - PYTHON: ${{ join(matrix.python, '.') }} # e.g. '3.11' - TOX_ENV: ${{ format('{0}{1}', 'py', join(matrix.python, '')) }} # e.g. 'py311' + PYTHON: ${{ join(matrix.python, '.') }} # e.g. '3.12' + TOX_ENV: ${{ format('{0}{1}', 'py', join(matrix.python, '')) }} # e.g. 'py312' runs-on: ${{ format('{0}-latest', matrix.os) }} name: "Test py-${{ join(matrix.python, '.') }} (${{ matrix.os }})" steps: @@ -32,7 +32,6 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - ref: main - name: Setting up Python ${{ env.PYTHON }} uses: actions/setup-python@v5 diff --git a/.gitignore b/.gitignore index ceae0c2..2f4576e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,13 @@ *.py[cod] __pycache__ +# random files +S2870 BSA THF 1 1 d.h5 +test.nxs +testdata/test_nexus_io.nxs +testdata/test.yaml +.vscode/settings.json + # C extensions *.so diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 59ad68d..a07d130 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,15 +10,9 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - id: debug-statements - - repo: https://github.com/pycqa/isort - rev: 5.12.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.8 hooks: - - id: isort - - repo: https://github.com/psf/black - rev: 23.1.0 - hooks: - - id: black - - repo: https://github.com/pycqa/flake8 - rev: 6.0.0 - hooks: - - id: flake8 + - id: ruff + args: [--fix] + - id: ruff-format diff --git a/CHANGELOG.md b/CHANGELOG.md index 4baed0c..f5e89c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,11 @@ ## v1.0.6 (2025-07-25) -### Bug fixes +### Bug fixes * Project Config: declare Python 3.11 compat. updates GitHub Action tests as well ([`eaeaae8`](https://github.com/BAMresearch/McSAS3/commit/eaeaae83b111a2b0bb1a524ec19e7142fc69bcec)) -### Code style +### Code style * __main__: reformat long lines ([`606fc2f`](https://github.com/BAMresearch/McSAS3/commit/606fc2fa62d2b758eedac951a61fb8df6d02c948)) diff --git a/MANIFEST.in b/MANIFEST.in index 8acfbaf..5e15978 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,11 +1,14 @@ graft docs exclude docs/reference/autosummary/* +graft design_documentation graft src graft ci graft tests graft testdata +exclude testdata/test.yaml exclude testdata/quickstartdemo1_fitResult.* graft example_configurations +graft tools include .editorconfig include .vscode/*.json diff --git a/README.md b/README.md index 9c83eb9..7170381 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ Due to an issue with sasmodels when using OpenCL: if you see problems with the f 2. There are launchers that can work from the command line, for optimization and (separately) histogramming. These use minimal configuration files for setting up the different parts of the code. Adjust these for your output files and optimization requirements, and then you can use these to automatically provide a McSAS3 analysis for every measurement. 3. Currently, it reads three-column ascii / CSV files, or NeXus/HDF5 files. example read configurations are provided. 4. Observability limits are not included yet -5. A GUI is not available (yet). +5. A separate GUI client exists in the sibling `McSAS3GUI` repository. The core documentation here + focuses on the maintained CLI and canonical Python workflow APIs. 6. Some bugs remain. Feel free to add bugs to the issues. They will be fixed as time permits. ## Installation @@ -58,6 +59,51 @@ To run the optimizer from the command line using the test settings and test data This is, of course, a mere test case. The result should look like the Figure shown earlier. +### Python API + +The supported Python entry point is the canonical `ProcessingData` workflow API. For scripts or +notebooks, prefer the top-level `mcsas3` workflow functions: + +```python +from pathlib import Path + +from mcsas3 import ( + STAGE_CLIPPED, + load_result_processing_data, + optimize_processing_data, + prepare_1d_processing_data_from_file, + selected_bundle_from_processing, +) + +processing = prepare_1d_processing_data_from_file( + Path("testdata", "quickstartdemo1.csv"), + csvargs={"sep": ";", "header": None, "names": ["Q", "I", "ISigma"]}, + nbins=100, + analysis_stage=STAGE_CLIPPED, +) + +optimize_processing_data( + processing, + Path("result.h5"), + modelName="mcsas_sphere", + fitParameterLimits={"radius": "auto"}, + staticParameters={"background": 0.0, "scale": 1.0, "sld": 33.4, "sld_solvent": 0.0}, + maxIter=1000, + convCrit=1.0, + nRep=2, + nCores=1, +) + +restored = load_result_processing_data(Path("result.h5")) +selected_bundle = selected_bundle_from_processing(restored) +q = selected_bundle["Q"].signal +intensity = selected_bundle["signal"].signal +``` + +This keeps the public path on canonical `ProcessingData` / `DataBundle` objects. For reusable +clipping, omission, rebinning, and 2D reconstruction helpers, use `mcsas3.preprocessing`. If you +are updating older notebooks or scripts, see the migration notes in the user documentation. + To do the same for real measurements, you need to configure McSAS3 by supplying it with three configuration files (two for the optimization, one for the histogramming): ### Data read configuration file @@ -66,7 +112,10 @@ This file contains the parameters necessary to read a data file. The example fil ```yaml --- # configuration used to read files into McSAS3. this is assumed to be a 1D file in csv format - # Note that the units are assumed to be 1/(m sr) for I and 1/nm for Q + # Internal canonical units are 1/(m sr) for I and 1/nm for Q. + # Override them here when the source file uses different units. + QUnits: "1 / angstrom" + IUnits: "1 / centimeter / steradian" nbins: 100 dataRange: - 0.0 # minimum @@ -82,14 +131,17 @@ This file contains the parameters necessary to read a data file. The example fil Here, *nbins* is the number of binned datapoints to apply to the data clipped to within the dataRange Q limits. We normally rebin the data to reduce the number of datapoints used for the optimization procedure. Typically 100 datapoints per decade is more than sufficient. The uncertainties are propagated and means calculated from the datapoints within a bin. -The *csvargs* is the dictionary of options passed on to the Pandas.from_csv function. The thus loaded columns should at least contain columns named 'Q', 'I', and 'ISigma' (the uncertainty on I). +The *csvargs* is the dictionary of options passed on to `pandas.read_csv()`. The loaded columns +should at least contain columns named `Q`, `I`, and `ISigma` (the uncertainty on `I`). You can also directly load NeXus or HDF5 files, for example you can directly load the processed files that come out of the DAWN software package. The file read configuration for a NeXus or HDF5 file is slightly different. The reader can follow either the 'default' attributes to the data to use, or you can supply a dictionary of HDF5 paths to the datasets to fit (this is the more robust option). For example: ```yaml --- # configuration used to read nexus files into McSAS3. this is assumed to be a 1D file in nexus - # Note that the units are assumed to be 1/(m sr) for I and 1/nm for Q - # if necessary, the paths to the datasets can be indicated. + # Internal canonical units are 1/(m sr) for I and 1/nm for Q. + # if necessary, the paths to the datasets can be indicated, and units can be overridden. + QUnits: "1 / angstrom" + IUnits: "1 / centimeter / steradian" nbins: 100 dataRange: - 0.0 # minimum @@ -162,6 +214,28 @@ For each histogramming range, histogram-independent population statistics are al https://BAMresearch.github.io/McSAS3 +The docs now also cover: + +- quickstart workflows for the maintained CLI and canonical Python API +- upgrade notes for older notebooks and scripts +- generated module-structure diagrams +- release delivery for Python packages and standalone CLI bundles + +## Project structure + +The maintained code structure is documented in the design docs, including a generated Mermaid +module dependency diagram: + +- [canonical data contract](design_documentation/canonical_data_contract.md) +- [upgrade plan](design_documentation/upgrade_plan.md) +- [generated module dependency diagram](design_documentation/generated_module_dependencies.md) + +To regenerate the dependency diagram after structural changes, run: + +```bash +./.venv/bin/python tools/generate_dependency_diagram.py +``` + ## Development ### Testing @@ -183,4 +257,3 @@ Run all tests with: Update the project configuration from the *copier* template: copier update --trust --skip-answered - diff --git a/design_documentation/README.md b/design_documentation/README.md new file mode 100644 index 0000000..e2143a1 --- /dev/null +++ b/design_documentation/README.md @@ -0,0 +1,19 @@ +# McSAS3 Design Documentation + +This directory captures the current internal shape of McSAS3 and the most important +refactor target currently in scope: replacing the internal measurement containers with the +MoDaCor `ProcessingData` / `DataBundle` / `BaseData` model. + +Documents in this directory: + +- `upgrade_plan.md`: living stepwise upgrade plan for McSAS3 and coordinated McSAS3GUI work. +- `generated_module_dependencies.md`: generated Mermaid diagram of current top-level module + dependencies in `src/mcsas3`; regenerate it with `python tools/generate_dependency_diagram.py`. +- `current_architecture.md`: historical package layout and runtime flow captured before the final + `McData*` retirement; preserved for migration context. +- `canonical_data_contract.md`: agreed canonical `ProcessingData` stage names, bundle keys, + default units, and current canonical workflow rules. +- `modacor_data_model_migration.md`: historical migration rationale and staging notes from the + period when `McData*` still existed in the core repo. + +This is an internal engineering baseline, not user-facing documentation. diff --git a/design_documentation/canonical_data_contract.md b/design_documentation/canonical_data_contract.md new file mode 100644 index 0000000..3a3127d --- /dev/null +++ b/design_documentation/canonical_data_contract.md @@ -0,0 +1,184 @@ +# McSAS3 Canonical Data Contract + +Last updated: 2026-04-01 + +This document defines the maintained McSAS3 data contract. +It is the source of truth for the canonical bundle keys, stage names, and supported adapter/helper +rules. + +## Dependency boundary + +McSAS3 now imports MoDaCor data classes through `mcsas3.data_model`. + +Import strategy: + +- require a normal installed `modacor` package + +McSAS3 no longer uses sibling-checkout fallback import plumbing for MoDaCor. + +Supported public Python API: + +- top-level `mcsas3` exports now expose the maintained canonical workflow entry points: + - `prepare_1d_processing_data` + - `prepare_1d_processing_data_from_file` + - `prepare_2d_processing_data` + - `prepare_2d_processing_data_from_file` + - `optimize_processing_data` + - `load_result_processing_data` + - `store_result_processing_data` +- top-level `mcsas3` also re-exports the canonical carrier types and stage constants: + - `BaseData` + - `DataBundle` + - `ProcessingData` + - `STAGE_RAW` + - `STAGE_CLIPPED` + - `STAGE_BINNED` +- reusable canonical preprocessing helpers live in `mcsas3.preprocessing` +- new notebook and script usage should import the top-level workflow functions and canonical + helpers directly + +## Canonical stage names + +`ProcessingData` stage names are: + +- `sample_raw` +- `sample_clipped` +- `sample_binned` + +These names are represented in code by: + +- `mcsas3.data_adapters.STAGE_RAW` +- `mcsas3.data_adapters.STAGE_CLIPPED` +- `mcsas3.data_adapters.STAGE_BINNED` + +## Canonical selected analysis stage + +`ProcessingData` now carries the selected stage for fitting or analysis via the instance attribute: + +- `processing.analysis_stage` + +Current rules: + +- the value must be one of the canonical stage names +- the default is `sample_binned` +- this selects which stage feeds fitting and histogramming + +## Canonical 1D bundle contract + +Each 1D stage is a `DataBundle` with these keys: + +- `signal`: intensity data +- `Q`: scattering vector +- `mask`: optional boolean mask + +Current default units: + +- `signal.units = 1 / (m sr)` +- `Q.units = 1 / nm` +- `mask.units = dimensionless` + +Current uncertainty rules: + +- intensity uncertainties live on `signal.uncertainties["propagate_to_all"]` +- optional `QSigma` lives on `Q.uncertainties["propagate_to_all"]` + +Notes: + +- McSAS3 uses absolute scattering cross-section units for the canonical signal representation. +- `1 / nm` matches the existing McSAS3 optimizer and reporting convention. +- source data is normalized to these canonical units at ingestion time + +## Canonical 2D bundle contract + +Each 2D stage is a `DataBundle` with these keys: + +- `signal`: 2D intensity image +- `Qx`: horizontal reciprocal-space coordinate +- `Qy`: vertical reciprocal-space coordinate +- `mask`: optional boolean mask image + +Current default units: + +- `signal.units = 1 / (m sr)` +- `Qx.units = 1 / nm` +- `Qy.units = 1 / nm` +- `mask.units = dimensionless` + +Current uncertainty rules: + +- intensity uncertainties live on `signal.uncertainties["propagate_to_all"]` +- `Qx` and `Qy` currently carry no uncertainty arrays in the adapter layer + +Notes: + +- the canonical 2D representation remains image-shaped +- flattened fit vectors are derived adapter output, not the stored canonical representation +- source data is normalized to these canonical units at ingestion time + +## Adapter rules + +The adapter layer lives in `mcsas3.data_adapters`. + +Supported conversions: + +- 1D `DataFrame` -> canonical `DataBundle` +- 2D stage dict-of-arrays -> canonical `DataBundle` +- canonical `ProcessingData` + `analysis_stage` -> selected analysis `DataBundle` +- canonical selected analysis `DataBundle` -> optimizer fit arrays +- canonical `DataBundle` -> derived stage `DataFrame` via `frame_from_bundle()` + +Current normalization rules: + +- adapter entry points accept optional source-unit declarations for `Q` and `signal` +- read-configuration YAML and canonical file-ingest helpers may provide these as `QUnits` / + `IUnits` (preferred, matching the existing config style) or `Q_units` / `I_units` (accepted + alias) +- canonical bundles are always stored in `1 / nm` and `1 / (m sr)` regardless of input units +- uncertainty arrays are converted alongside their parent `BaseData` + +SasModels bridge rules: + +- canonical McSAS3 data stays in `1 / nm` and `1 / (m sr)` +- the SasModels execution boundary converts reciprocal-space and size-like parameters to the + angstrom-based conventions expected by SasModels, then converts intensity back to canonical + McSAS3 units +- fast regression coverage checks that this bridge preserves the expected recovered volume + fraction for a sphere model at fixed SLD contrast + +## Canonical workflow and preprocessing surface + +McSAS3 now uses canonical workflows directly: + +- `mcsas3.workflows` owns supported file ingestion, preprocessing orchestration, HDF5 + load/store, and optimization entry points +- `mcsas3.preprocessing` owns the reusable clip / omit / rebin / reconstruct helpers over + canonical `DataBundle` objects +- `mcsas3.ingestion` owns shared 1D and 2D file loading plus source-unit detection +- `mcsas3.data_adapters.fit_arrays_from_bundle()` is the maintained bridge from canonical bundles + to flattened optimizer arrays; the maintained execution path no longer accepts flat fit-data + dicts as input + +## Canonical HDF5 persistence + +McSAS3 now stores first-class canonical processing data at: + +- `/analyses/MCResult*/mcdata/processingData` + +Current storage rules: + +- `ProcessingData.analysis_stage` is stored with the processing-data group +- each canonical stage is stored as its own `DataBundle` subgroup +- each `BaseData` entry stores: + - `signal` + - `weights` + - `uncertainties/*` + - `units` + - `rank_of_data` +- bundle metadata such as `default_plot` and `description` is preserved + +Current load rules: + +- `load_result_processing_data()` requires the canonical `processingData` schema +- result files store canonical stage bundles under `processingData` without duplicate stage-table + groups +- the maintained load path expects that canonical `processingData` schema diff --git a/design_documentation/current_architecture.md b/design_documentation/current_architecture.md new file mode 100644 index 0000000..3ec428c --- /dev/null +++ b/design_documentation/current_architecture.md @@ -0,0 +1,285 @@ +# McSAS3 Current Architecture + +> Historical note: +> This document captures the architecture before `McData`, `McData1D`, and `McData2D` were +> removed from the maintained core path. It is preserved for migration context. For the current +> supported architecture, use `canonical_data_contract.md`, `upgrade_plan.md`, and the public +> workflow API documented in the top-level README. + +## Scope + +The current `McSAS3` repository is the optimizer, result persistence layer, and +histogramming/plotting pipeline. It does not contain the GUI itself. The GUI currently lives in +the sibling `McSAS3GUI` repository and depends on several McSAS3 internals. + +At a high level, McSAS3 is a small library plus CLI wrappers around a Monte Carlo acceptance / +rejection optimizer for scattering data. + +## Package Map + +### Entry points + +- `src/mcsas3/__main__.py` + Runs optimization and histogramming in sequence. +- `src/mcsas3/mcsas3_cli_runner.py` + Optimization-only CLI. +- `src/mcsas3/mcsas3_cli_histogrammer.py` + Histogramming-only CLI. +- `src/mcsas3/cli_tools.py` + Thin orchestration layer used by the CLIs. + +Important current limitation: + +- the CLI path is effectively 1D-only today because `cli_tools.py` instantiates `McData1D` + directly for both optimization input and histogramming reload. + +### Data loading and preprocessing + +- `src/mcsas3/mc_data.py` + Base class for file loading, clipping, binning, `measData` creation, and HDF5 + serialization. +- `src/mcsas3/mc_data_1d.py` + Main production path today. Uses `pandas.DataFrame` for raw, clipped, and binned data. +- `src/mcsas3/mc_data_2d.py` + Partial 2D support. Uses a mix of flattened `DataFrame` data and dict-based 2D arrays. + +### Optimization + +- `src/mcsas3/mc_hat.py` + Repetition-level orchestration and multiprocessing. +- `src/mcsas3/mc_core.py` + Inner Monte Carlo loop for one repetition. +- `src/mcsas3/mc_model.py` + Model abstraction, random parameter generation, and SasModels integration. +- `src/mcsas3/mc_opt.py` + Optimization state container. +- `src/mcsas3/osb.py` + Scaling/background fit for measured vs. modeled intensity. + +### Analysis and output + +- `src/mcsas3/mc_model_histogrammer.py` + Histograms a single repetition. +- `src/mcsas3/mc_analysis.py` + Re-loads all repetitions, aggregates histograms and optimization statistics. +- `src/mcsas3/mc_plot.py` + Produces the PDF result card. +- `src/mcsas3/mc_hdf.py` + Generic HDF5 persistence helpers and the `ResultIndex` path helper. + +### Tests + +- `tests/test_McData1D_unittest.py` + Main coverage of data loading and result-file restore. +- `tests/test_McData2D_unittest.py` + Minimal 2D smoke coverage. +- `tests/test_optimizer_integraltest.py` + End-to-end optimization and histogramming tests. + +## Runtime Flow + +## 1. Optimization flow + +1. CLI reads the YAML read configuration and run configuration. +2. `McSAS3_cli_optimize` constructs `McData1D`. +3. `McData1D` loads the source file, clips, omits ranges, rebins, and builds `measData`. +4. `McData.store()` writes the data state into the output HDF5 file. +5. `McHat` builds shared `McModel` and `McOpt` configuration. +6. For each repetition, `McCore`: + - builds the SasModels kernel, + - initializes the summed model intensity from all contributions, + - repeatedly proposes a new contribution, + - re-fits scaling/background, + - accepts the move if GOF improves. +7. Each repetition stores model parameters and optimization state to HDF5. + +The important execution boundary is: + +`McData*` -> `measData` plain dict -> `McHat` / `McCore` + +That boundary is the easiest place to introduce a new shared data model without rewriting the +optimizer in one pass. + +## 2. Histogramming flow + +1. CLI reads the result file and histogram YAML. +2. `McData1D(loadFromFile=...)` reconstructs measurement data from HDF5. +3. `McAnalysis` discovers all stored repetitions. +4. For each repetition, `McAnalysis` rebuilds a `McCore` object from stored model and + optimization state. +5. `McModelHistogrammer` produces per-range histograms and mode statistics. +6. `McAnalysis` averages histograms, fit statistics, and model intensities over repetitions. +7. `McPlot` writes the PDF summary card. + +## Current Data Representations + +McSAS3 currently uses several incompatible representations for essentially the same +measurement. + +### 1D path + +- `rawData`: `pandas.DataFrame` with `Q`, `I`, `ISigma` +- `clippedData`: `pandas.DataFrame` +- `binnedData`: `pandas.DataFrame`, usually with extra statistics columns such as `IStd`, + `ISEM`, `QStd`, `QSigma`, ... +- `measData`: plain `dict` + - `Q`: a one-element list containing the Q array + - `I`: intensity array + - `ISigma`: uncertainty array + +### 2D path + +- `rawData2D`: dict of 2D arrays such as `I`, `ISigma`, `Qx`, `Qy`, `mask` +- `rawData`: flattened `DataFrame` +- `clippedData`: dict containing both cropped 2D arrays and flattened fit arrays +- `binnedData`: currently just `clippedData` +- `measData`: plain `dict` + - `Q`: two-element list `[Qy, Qx]` + - `I`: flattened intensity array + - `ISigma`: flattened uncertainty array + +### Observations + +- The same logical dataset is represented as `DataFrame`, dict-of-arrays, and plain dict. +- The 1D and 2D representations diverge significantly. +- Units exist only by convention and comments, not as a first-class runtime contract. +- Uncertainties are reduced to a single array at the optimizer boundary. +- `McData` combines file IO, preprocessing, transient cache state, and persistence. + +## Current HDF5 Layout + +The result file is rooted under: + +`/analyses/MCResult{resultIndex}` + +Main groups: + +- `mcdata` + Stored read/preprocessed data state and configuration. +- `model` + Shared model configuration plus per-repetition parameter sets and volumes. +- `optimization` + Global optimization settings plus per-repetition optimization state. +- `histograms` + Per-range / per-repetition histograms and averaged histogram outputs. + +Approximate structure: + +```text +/analyses/MCResult1 + /mcdata + /rawData + /clippedData + /binnedData + /measData + filename + loader + nbins + ... + /model + /fitParameterLimits + /staticParameters + modelName + /repetition0 + /parameterSet + seed + volumes + modelDType + /repetition1 + ... + /optimization + nCores + nRep + /repetition0 + modelI + gof + x0 + accepted + acceptedSteps + acceptedGofs + ... + /average + ... + /histograms + /histRange0 + /repetition0 + /average + /histRange1 + ... +``` + +## Coupling to McSAS3GUI + +The sibling `McSAS3GUI` repository is currently coupled to McSAS3 internals in several ways. + +### Direct library coupling + +- `DataLoadingTab` imports `McData1D` directly and plots `rawData`, `clippedData`, and + `binnedData` as `DataFrame` objects. +- `RunSettingsTab` imports `McHat` directly and passes `mds.measData.copy()` into + `McHat.run(...)`. + +### Direct result-file coupling + +- The GUI reads HDF5 paths such as + `/analyses/MCResult1/mcdata/measData/Q` and + `/analyses/MCResult1/optimization/repetition0/modelI` directly. + +### Process-level coupling + +- Histogram tests in the GUI shell out to `python -m mcsas3.mcsas3_cli_histogrammer`. + +This means a data-model migration inside McSAS3 should assume: + +- temporary compatibility for `measData`, +- temporary compatibility for result-file structure, +- or a coordinated update in `McSAS3GUI` at the same time. + +## Strengths of the Current Design + +- The optimization core is relatively isolated from file-format specifics once it receives + `measData`. +- The result file already acts as a reproducible state bundle for re-histogramming. +- SasModels integration is encapsulated behind `McModel`. +- Histogramming is already decoupled from optimization execution. + +## Main Technical Debt Areas + +### Data model fragmentation + +This is the central issue. The same measurement changes shape several times before the optimizer +sees it, and the representations are only loosely documented. + +### `McData` does too much + +`McData` mixes: + +- source loading, +- preprocessing, +- stage caching, +- legacy execution contract generation, +- and HDF5 serialization. + +### 1D and 2D are structurally inconsistent + +2D support exists, but its internal contract is not the same as the 1D path and several +methods remain partial or unimplemented. In particular, the current CLI flow does not route +through `McData2D`, `McData2D.reBin()` is still a no-op passthrough, and restore/load handling is +less complete than the 1D path. + +### Storage schema is implementation-shaped + +The HDF5 layout mirrors current Python structures rather than a stable domain model. + +### GUI depends on internals, not stable APIs + +The GUI currently assumes both in-memory representations and exact HDF5 paths. + +## Immediate Refactor Conclusion + +The best seam for introducing the MoDaCor model is not inside `McCore` first. It is between +`McData` and the rest of the pipeline: + +- replace internal raw/clipped/binned containers with `BaseData`-based structures, +- keep a compatibility adapter that still produces legacy `measData`, +- then migrate optimizer, analysis, storage, and GUI incrementally. diff --git a/design_documentation/generated_module_dependencies.md b/design_documentation/generated_module_dependencies.md new file mode 100644 index 0000000..748efd1 --- /dev/null +++ b/design_documentation/generated_module_dependencies.md @@ -0,0 +1,121 @@ +# McSAS3 Module Dependency Diagram + +Generated on: 2026-04-22 + +This file is generated by `python tools/generate_dependency_diagram.py`. +It captures imports between top-level modules in `src/mcsas3` and is intended as a +maintained structure overview rather than a full call graph. + +```mermaid +flowchart LR + subgraph module_Public API and entry points["Public API and entry points"] + module___init__["__init__"] + module___main__["__main__"] + module_cli_tools["cli_tools"] + module_mc_plot["mc_plot"] + module_mcsas3_cli_histogrammer["mcsas3_cli_histogrammer"] + module_mcsas3_cli_runner["mcsas3_cli_runner"] + module_workflows["workflows"] + end + subgraph module_Canonical data and workflow layer["Canonical data and workflow layer"] + module_data_adapters["data_adapters"] + module_data_model["data_model"] + module_ingestion["ingestion"] + module_mc_hdf["mc_hdf"] + module_optimizer_input["optimizer_input"] + module_preprocessing["preprocessing"] + end + subgraph module_Optimization and analysis core["Optimization and analysis core"] + module_mc_analysis["mc_analysis"] + module_mc_core["mc_core"] + module_mc_hat["mc_hat"] + module_mc_model["mc_model"] + module_mc_model_histogrammer["mc_model_histogrammer"] + module_mc_opt["mc_opt"] + module_osb["osb"] + end + subgraph module_Other internal modules["Other internal modules"] + module__cli_common["_cli_common"] + module_cli_histogram["cli_histogram"] + module_cli_optimize["cli_optimize"] + module_runtime_paths["runtime_paths"] + end + module___init__ --> module_data_adapters + module___init__ --> module_data_model + module___init__ --> module_workflows + module___main__ --> module_cli_histogram + module___main__ --> module_cli_optimize + module_cli_histogram --> module__cli_common + module_cli_histogram --> module_mc_analysis + module_cli_histogram --> module_mc_plot + module_cli_histogram --> module_workflows + module_cli_optimize --> module__cli_common + module_cli_optimize --> module_workflows + module_cli_tools --> module_cli_histogram + module_cli_tools --> module_cli_optimize + module_cli_tools --> module_workflows + module_data_adapters --> module_data_model + module_mc_analysis --> module_data_adapters + module_mc_analysis --> module_data_model + module_mc_analysis --> module_mc_core + module_mc_analysis --> module_mc_hdf + module_mc_analysis --> module_mc_model_histogrammer + module_mc_analysis --> module_optimizer_input + module_mc_core --> module_data_adapters + module_mc_core --> module_mc_hdf + module_mc_core --> module_mc_model + module_mc_core --> module_mc_opt + module_mc_core --> module_optimizer_input + module_mc_core --> module_osb + module_mc_hat --> module_data_adapters + module_mc_hat --> module_mc_core + module_mc_hat --> module_mc_hdf + module_mc_hat --> module_mc_model + module_mc_hat --> module_mc_opt + module_mc_hat --> module_optimizer_input + module_mc_hdf --> module_data_model + module_mc_model --> module_mc_hdf + module_mc_model_histogrammer --> module_mc_core + module_mc_model_histogrammer --> module_mc_hdf + module_mc_model_histogrammer --> module_mc_model + module_mc_model_histogrammer --> module_mc_opt + module_mc_opt --> module_mc_hdf + module_mc_plot --> module_optimizer_input + module_mcsas3_cli_histogrammer --> module_cli_histogram + module_mcsas3_cli_histogrammer --> module_runtime_paths + module_mcsas3_cli_runner --> module_cli_optimize + module_mcsas3_cli_runner --> module_runtime_paths + module_optimizer_input --> module_data_adapters + module_osb --> module_data_adapters + module_osb --> module_optimizer_input + module_preprocessing --> module_data_adapters + module_preprocessing --> module_data_model + module_workflows --> module_data_adapters + module_workflows --> module_data_model + module_workflows --> module_ingestion + module_workflows --> module_mc_hat + module_workflows --> module_mc_hdf + module_workflows --> module_preprocessing + classDef public fill:#e7f0ff,stroke:#3a66b3,color:#1d2b45; + classDef canonical fill:#e8f8ee,stroke:#2a7f45,color:#153523; + classDef core fill:#fff2df,stroke:#b06a00,color:#4d3200; + classDef other fill:#f2f2f2,stroke:#777777,color:#333333; + class module___init__,module___main__,module_cli_tools,module_mc_plot,module_mcsas3_cli_histogrammer,module_mcsas3_cli_runner,module_workflows public; + class module_data_adapters,module_data_model,module_ingestion,module_mc_hdf,module_optimizer_input,module_preprocessing canonical; + class module_mc_analysis,module_mc_core,module_mc_hat,module_mc_model,module_mc_model_histogrammer,module_mc_opt,module_osb core; + class module__cli_common,module_cli_histogram,module_cli_optimize,module_runtime_paths other; +``` + +## Regeneration + +Run: + +```bash +./.venv/bin/python tools/generate_dependency_diagram.py +``` + +## Notes + +- The diagram is generated from import statements, so it shows module coupling rather than runtime call flow. +- External dependencies are intentionally omitted. +- The layer grouping is curated for readability; edges within and across layers remain generated. diff --git a/design_documentation/modacor_data_model_migration.md b/design_documentation/modacor_data_model_migration.md new file mode 100644 index 0000000..adcbade --- /dev/null +++ b/design_documentation/modacor_data_model_migration.md @@ -0,0 +1,295 @@ +# McSAS3 Migration to MoDaCor Data Classes + +> Historical note: +> This document records the migration rationale and staged plan that led from the old `McData*` +> wrappers to the current canonical `ProcessingData` workflow API. The wrapper modules have since +> been removed from the maintained core repo. + +## Goal + +Replace McSAS3's ad hoc mixture of `pandas.DataFrame`, dict-of-arrays, and plain `measData` +dicts with the MoDaCor data stack: + +- `BaseData`: signal + uncertainties + units + metadata +- `DataBundle`: associated `BaseData` objects for one logical dataset +- `ProcessingData`: named collection of bundles + +This should reduce duplicated data-handling logic between McSAS3 and MoDaCor and make 1D and 2D +paths look like the same problem again. + +## What MoDaCor Gives Us + +From the current MoDaCor implementation: + +- `BaseData` is the real value object. + It already carries units, uncertainties, weights, optional axis metadata, copying, slicing, + and arithmetic support. +- `DataBundle` is a light dictionary for associated datasets. + Typical scattering usage is a bundle with keys such as `signal`, `Q`, `Psi`, `mask`, etc. +- `ProcessingData` is a light dictionary of `DataBundle` objects. + +This means the migration target is not "replace McSAS3 with a new framework". It is mostly +"replace McSAS3's measurement containers with an already-existing shared contract". + +## Recommended McSAS3 Mapping + +## 1D bundle shape + +Recommended bundle contents for one 1D dataset: + +```text +DataBundle( + signal = BaseData( + signal=I, + units=1 / (m sr), + uncertainties={"propagate_to_all": ISigma}, + rank_of_data=1, + ), + Q = BaseData( + signal=Q, + units=1 / nm, + uncertainties={"propagate_to_all": QSigma?}, + rank_of_data=1, + ), + mask = BaseData(...) # optional +) +``` + +Notes: + +- `signal` should always mean the dependent intensity. +- `Q` should always mean the independent scattering vector for 1D data. +- Use `propagate_to_all` when the uncertainty is already a single combined one-sigma array. + That matches current McSAS3 behavior well. +- Keep additional rebinning statistics outside the optimizer contract unless they are actively + needed. + +## 2D bundle shape + +Recommended bundle contents for one 2D dataset: + +```text +DataBundle( + signal = BaseData(signal=I2D, units=1 / (m sr), uncertainties={"propagate_to_all": ISigma2D}, rank_of_data=2), + Qx = BaseData(signal=Qx2D, units=1 / nm, rank_of_data=2), + Qy = BaseData(signal=Qy2D, units=1 / nm, rank_of_data=2), + mask = BaseData(signal=mask2D, units=dimensionless, rank_of_data=2), +) +``` + +Notes: + +- For 2D scattering, `Qx` and `Qy` should remain first-class bundle entries rather than trying to + force them into `axes`. +- Flattened fit vectors should become derived adapter output, not the canonical storage form. + +## Stage representation + +Today `McData` stores three stages for the same measurement: + +- raw +- clipped +- binned + +Recommended `ProcessingData` layout: + +```text +processing["sample_raw"] +processing["sample_clipped"] +processing["sample_binned"] +``` + +This is clearer than mixing stage names into bundle keys and will scale better if background, +reference, or calibration bundles are added later. + +## Compatibility Strategy + +Do not try to make every consumer speak `ProcessingData` on day one. + +The lowest-risk migration is: + +1. Make `ProcessingData` / `DataBundle` / `BaseData` the canonical internal representation inside + `McData`. +2. Keep adapters that derive legacy `DataFrame` and `measData` views for old callers. +3. Migrate consumers one boundary at a time. + +## Proposed Adapter Layer + +Add a small compatibility module in McSAS3, for example: + +- `src/mcsas3/data_model.py` + Re-export or import-shim MoDaCor types. +- `src/mcsas3/data_adapters.py` + Conversion helpers between McSAS3 legacy structures and MoDaCor bundles. + +Suggested helper functions: + +- `bundle_from_1d_dataframe(df, *, q_units, i_units) -> DataBundle` +- `bundle_from_2d_arrays(...) -> DataBundle` +- `processing_from_mcdata_stages(...) -> ProcessingData` +- `legacy_measdata_from_bundle(bundle) -> dict` +- `frame_from_bundle(bundle) -> pandas.DataFrame` + +The important point is to centralize the legacy translation. Right now the translation logic is +spread over `McData1D`, `McData2D`, the GUI, and tests. + +## Module-by-Module Impact + +## `McData`, `McData1D`, `McData2D` + +This is the primary refactor site. + +Recommended new responsibility: + +- load source data, +- create `ProcessingData` stages, +- provide compatibility views for legacy callers, +- persist both domain data and processing metadata. + +Recommended de-emphasis: + +- stop treating `DataFrame` as the primary 1D representation, +- stop treating flattened 2D arrays as the primary 2D representation. + +## `McHat` and `McCore` + +These can migrate in two steps. + +### Step 1 + +Leave the optimizer contract alone and keep accepting legacy `measData`. + +### Step 2 + +Change the optimizer boundary to accept a `DataBundle` instead of a plain dict and move the +legacy extraction into a single adapter function. + +That will let `McCore` explicitly request: + +- intensity signal +- intensity uncertainty +- 1D Q or 2D `(Qx, Qy)` + +without caring about the source file format. + +## `McAnalysis` and `McModelHistogrammer` + +These currently re-use the same legacy measurement dict contract. + +They should move to the same adapter boundary as the optimizer so that histogramming and +re-analysis operate on the same canonical bundle representation. + +## `mc_plot.py` + +Plotting should not depend on `pandas.DataFrame` objects being present on `McData`. +It should accept either: + +- a `DataBundle`, or +- a thin plotting DTO derived from a `DataBundle`. + +## HDF5 persistence + +Current HDF5 storage mirrors implementation details. The migration is a good opportunity to make +the stored schema more domain-oriented. + +Recommended transitional approach: + +1. Keep writing the existing HDF5 groups required by old code and the current GUI. +2. Add a parallel, more structured `ProcessingData`-oriented representation. +3. Move readers to the new representation. +4. Remove dual-write only after the GUI and tests stop depending on legacy paths. + +## GUI Impact + +The sibling `McSAS3GUI` repo currently depends on: + +- `McData1D.rawData`, `clippedData`, and `binnedData` being `DataFrame` objects, +- `mds.measData.copy()` existing, +- exact HDF5 paths inside `/analyses/MCResult1/...`. + +That means the GUI should be treated as an external client of this migration, even though it +shares the same project goal. + +Recommended GUI transition plan: + +1. Add a stable McSAS3 API for "get plot-ready datasets" and "get fit preview data". +2. Update the GUI to call those APIs instead of reaching into `DataFrame` attributes and HDF5 + paths. +3. Only then remove the legacy internals. + +## Suggested Staged Plan + +## Stage 0: Introduce a compatibility import layer + +- Decide whether McSAS3 imports MoDaCor directly or through a thin local shim. +- Hide that decision behind one McSAS3 module so the rest of the repo does not care. + +## Stage 1: Canonicalize measurement storage in `McData` + +- Represent raw, clipped, and binned states as `ProcessingData` bundles. +- Keep `rawData`, `clippedData`, `binnedData`, and `measData` as derived compatibility views. + +This should deliver the biggest cleanup for the least optimizer risk. + +## Stage 2: Move the optimizer boundary + +- Change `McHat.run(...)` and `McCore(...)` to accept a `DataBundle` or a small typed adapter + object instead of the raw dict. +- Keep one translation function for legacy callers during the transition. + +## Stage 3: Move analysis and plotting + +- Make `McAnalysis` and `McPlot` consume the same new measurement contract. +- Remove duplicated assumptions about `Q`, `I`, and `ISigma` field packing. + +## Stage 4: Move persistence + +- Add structured HDF5 writing for bundle-based data. +- Keep dual-write until the GUI and result-file readers are migrated. + +## Stage 5: Remove legacy compatibility + +- Remove `measData` as a primary state container. +- Reduce `pandas` usage to places where table semantics are actually useful, such as histogram + summaries. + +## Immediate Risks and Design Decisions + +## 1. Dependency boundary + +Need to decide whether McSAS3 will: + +- depend directly on `modacor`, or +- temporarily import it from a sibling checkout in development only. + +For maintainability, a normal package dependency is better. For short-term local refactoring, a +shim can hide that decision. + +## 2. Result-file compatibility + +Old result files and GUI tooling likely assume the current HDF5 layout. A flag-day storage change +would create avoidable breakage. + +## 3. 2D canonical shape + +The migration should treat 2D as a first-class case, not as a flattened special case. +Otherwise the same technical debt will reappear under new types. + +## 4. Unit normalization + +McSAS3 currently relies heavily on comments and convention for units. Once `BaseData` is used, +unit conversions and checks should become explicit at the data-loading boundary. + +## Recommended First Implementation Slice + +The best first code change is not in the optimizer. + +The best first slice is: + +1. add a McSAS3 import shim for MoDaCor data classes, +2. make `McData1D` build `ProcessingData` internally, +3. derive legacy `rawData`, `clippedData`, `binnedData`, and `measData` from that, +4. leave `McHat`, `McCore`, `McAnalysis`, and the GUI untouched for the first pass. + +That would immediately deduplicate the data handling direction without putting the Monte Carlo +core at risk. diff --git a/design_documentation/upgrade_plan.md b/design_documentation/upgrade_plan.md new file mode 100644 index 0000000..a8d68be --- /dev/null +++ b/design_documentation/upgrade_plan.md @@ -0,0 +1,1007 @@ +# McSAS3 Upgrade Plan + +Last updated: 2026-03-30 + +This is the living implementation plan for upgrading McSAS3 and coordinating the required changes +with the sibling `McSAS3GUI` repository. + +## Working assumptions + +- `McSAS3GUI` is a separate repo in the same workspace and must be treated as a client of McSAS3. +- The target internal data model is MoDaCor `ProcessingData` / `DataBundle` / `BaseData`. +- We should not keep `measData` as a long-term public or stored data model. +- `McData`, `McData1D`, and `McData2D` have now been removed from the maintained core repo; GUI + and downstream callers must use canonical workflows and `ProcessingData` directly. +- Input data should be converted to canonical internal units during ingestion so unit handling does + not add avoidable overhead in optimizer hot paths. +- The canonical carrier needs an explicit concept for the selected analysis stage; `measDataLink` + has been replaced and should not reappear. +- If a temporary bridge is needed during migration, keep it private, local, and short-lived. +- The default developer feedback loop must be fast. + Slow integration tests should become opt-in. +- Ruff is the lint/format source of truth, with flake8-style linting and Black-style formatting. +- Maximum line length is 120. + +## Current status + +- [x] Internal architecture and migration notes written in `design_documentation/`. +- [x] McSAS3 lint/format config moved toward Ruff and 120-column formatting. +- [x] Ruff/pre-commit hooks validated in both `McSAS3` and `McSAS3GUI`. +- [x] The Ruff/pre-commit rule set has been mirrored into `McSAS3GUI`. +- [x] `tox -e check` validated under the new Ruff-based setup. +- [x] McSAS3 now has an explicit fast default pytest path plus opt-in `integration` / `slow` + lanes. +- [x] MoDaCor data classes introduced into McSAS3 behind a stable import layer. +- [x] Canonical 1D/2D `ProcessingData` bundle shapes and stage names defined in code and docs. +- [x] `McData1D` now uses canonical `ProcessingData` stage storage. +- [x] `McData2D` now uses canonical `ProcessingData` stage storage. +- [x] `McData` now holds `ProcessingData` as the canonical in-memory representation. +- [x] Lightweight preprocessing helpers extracted so `McData*` classes can be retired. +- [x] The selected analysis stage is represented canonically without `measData` terminology. +- [x] `McAnalysis`, plotting, and the CLI histogram path now accept canonical selected-stage input. +- [x] Optimizer, analysis, and histogramming now accept direct `DataBundle` / `BaseData` input. +- [x] Input units normalized to standard internal units at ingestion. +- [x] HDF5 persistence migrated to full archival `ProcessingData` output without duplicated legacy + stage groups in new files. +- [x] Public `ProcessingData` workflow helpers exist, and the CLI optimize/histogram path now uses + them instead of constructing `McData*` directly. +- [x] Public 1D file ingestion now goes through shared canonical helpers instead of routing through + `McData1D`. +- [x] Public 2D file ingestion now goes through shared canonical helpers instead of routing through + `McData2D`. +- [x] Transitional wrapper naming now uses `analysisData` / `analysisStage` instead of + `measData` / `measDataLink` on maintained paths. +- [x] `McData*` no longer maintain a parallel flat analysis-data state; that view is now derived on + demand from the selected canonical bundle. +- [x] A supported top-level public Python API now points notebooks/scripts at canonical workflow + functions instead of `McData*`. +- [x] The main in-repo example notebook now uses canonical workflow helpers instead of `McData1D`. +- [x] Notebook execution is no longer part of the required McSAS3 CI test gate; the demo notebook + now lives behind an optional `tox -e notebooks` path instead of the default pytest collection. +- [x] `McData*` preprocessing now runs from canonical stage bundles instead of wrapper-maintained + compatibility views. +- [x] `McData*` compatibility views are now derived on demand from canonical stage bundles instead + of being stored as long-lived wrapper state on supported paths. +- [x] NXsas I/O tests now use temporary files instead of regenerating `testdata/test_nexus_io.nxs`. +- [x] `McData2D` now has an explicit raw-stage ingestion helper so supported tests no longer seed + it by mutating `rawData2D` / `rawData` directly. +- [x] `McData*` wrapper methods now require canonical raw stages instead of bootstrapping from + compatibility-view caches or manual `rawData*` assignment. +- [x] Remaining 2D wrapper stubs and mutable-stage helpers have been trimmed further; unsupported + dataframe-style seed paths are no longer part of the wrapper API. +- [x] Wrapper-specific loader aliases have been collapsed away; transitional wrappers now use + `from_file()` for file ingest, with only `McData1D.from_pandas()` and `McData2D.from_stage()` + remaining as non-file seed helpers. +- [x] Wrapper-only convenience methods such as `is2D()` have been removed from the supported + transition surface. +- [x] Wrapper compatibility-view attributes (`rawData`, `rawData2D`, `clippedData`, `binnedData`, + `measData`) have been removed; wrappers now fail explicitly if old code tries to use them. +- [x] Unused legacy stage-link adapter helpers have been removed from `data_adapters.py`. +- [x] `McData`, `McData1D`, and `McData2D` have been deleted from `src/mcsas3`. +- [x] Wrapper-specific unit tests have been removed or rewritten to assert against canonical + workflows, bundles, and preprocessing helpers directly. +- [x] `qNudge` has been removed from maintained McSAS3 adapter and optimizer-input APIs. +- [x] The execution path no longer accepts flat analysis-data dicts as optimizer input; maintained + runtime entry points now normalize from canonical `DataBundle` / `ProcessingData` only, with + `OptimizerInput` retained only as an internal execution-format escape hatch. +- [x] The flat analysis-data adapter has been removed from the maintained public API; supported + callers now use canonical bundles, `fit_arrays_from_bundle()`, or `OptimizerInput` internally as + appropriate. +- [x] Remaining adapter helper names on maintained paths no longer expose `legacy_*` naming; the + unused `legacy_2d_stage_from_bundle()` bridge has been removed entirely. +- [x] Core-owned stop / interrupt control is now implemented in `McHat` / `McCore`. +- [x] Core API hardening pass `8A` completed on the canonical surface before GUI migration. +- [x] McSAS3GUI updated to the new McSAS3 APIs and storage layout. +- [x] McSAS3GUI now has a clean local Ruff/pre-commit/check-manifest/tox `check` baseline after + the Phase 11 packaging and tooling cleanup. + +## Phase 0: Tooling and test baseline + +Goal: make the repo easier to change safely before touching the data model. + +### Step 0.1: Ruff and pre-commit baseline + +Status: complete. + +Deliverables: + +- Ruff-based `.pre-commit-config.yaml` +- `pyproject.toml` updated to 120 columns +- `tox -e check` aligned with Ruff + +Acceptance criteria: + +- `pre-commit run --all-files` passes +- `tox -e check` passes +- The same lint/format rules can be mirrored in `McSAS3GUI` + +Notes: + +- `pre-commit run --all-files` now passes in both repos. +- The mirror into `McSAS3GUI` caused the expected formatting churn there as well. +- `tox -e check` now passes in `McSAS3`. +- `MANIFEST.in` now includes `design_documentation/` and excludes the ignored local + `testdata/test.yaml` file so `check-manifest` is stable. +- Ruff now excludes `notebooks/` from the enforced baseline. Notebook cleanup remains separate + technical debt and should not block core refactoring work. + +### Step 0.2: Test suite timing baseline + +Status: established for the fast default path and current integration collection cost. + +Deliverables: + +- a short timing summary for the current test suite +- identification of the slowest files and the slowest individual tests + +Acceptance criteria: + +- we can point to the current default wall-clock time +- we know which tests dominate runtime and why + +Notes: + +- current fast default path: + - `python -m pytest tests` + - 28 tests passed in about 2.7 s +- current default collection: + - `python -m pytest tests --collect-only -q` + - 28 tests collected in about 1.0 s +- current opt-in integration collection: + - `python -m pytest tests --run-integration --collect-only -q` + - 37 of 38 tests collected in about 17 s, with the remaining one gated by `--run-slow` +- current opt-in integration execution: + - `python -m pytest tests/test_optimizer_integraltest.py --run-integration -q` + - 9 tests passed, 1 deselected, in about 34 s +- main known cost center remains `tests/test_optimizer_integraltest.py` + +### Step 0.3: Test taxonomy + +Status: implemented for the main heavy optimizer coverage. + +Deliverables: + +- explicit `slow` and `integration` markers +- default local test command that stays fast +- separate command for expensive end-to-end checks + +Acceptance criteria: + +- unmarked/default tests finish quickly enough for normal iteration +- expensive optimizer and multiprocess coverage is still available in a separate lane + +Notes: + +- `tests/test_optimizer_integraltest.py` is now marked as `integration`. +- `test_optimizer_1D_sphere_accuratestate` is additionally marked as `slow`. +- default local runs skip both categories unless explicitly enabled. +- current command patterns: + - fast default: `python -m pytest tests` + - integration lane: `python -m pytest tests --run-integration` + - full heavy lane: `python -m pytest tests --run-integration --run-slow` + +## Phase 1: Make McSAS3 tests cheap enough to support refactoring + +Goal: move from a monolithic slow suite to layered tests with fast deterministic coverage. + +### Step 1.1: Split test layers + +Tasks: + +- move file-backed, multiprocess, large-iteration tests behind `integration` and/or `slow` +- keep fast logic tests unmarked +- stop using the huge `tests/test_optimizer_integraltest.py` file as the main place for all + behavior checks + +Acceptance criteria: + +- default `pytest` no longer runs the full optimizer stress suite +- integration coverage still exists in a separate command + +Status: + +- the main optimizer integration file is now off the default path +- further splitting and reorganization of that file is still pending + +### Step 1.2: Add synthetic fast tests + +Status: complete. + +Tasks: + +- add small deterministic tests for `mc_hdf.py` +- add small deterministic tests for data clipping, omission, binning, and load/restore logic +- add focused tests for optimizer state transitions that do not need large `nRep` or `maxIter` + +Acceptance criteria: + +- core behaviors are covered without requiring large HDF5 fixtures or long optimizer runs + +Notes: + +- the fast lane now includes synthetic tests for: + - `mc_hdf` key/value round-trips and dataframe reconstruction + - `McData1D` clip/omit/rebin behavior from in-memory dataframes + - `McData1D` store/load round-trips from synthetic state files + - `McData2D` clip/mask/q-nudge/reconstruct behavior from synthetic 2D arrays + - `McData2D` store/load round-trips from synthetic 2D state files + - `McHat.fillFitParameterLimits` and `McCore.accept` state bookkeeping +- `McData.loadKeys` now includes `qNudge` so restored processed state matches stored state. +- the synthetic coverage also exposed and fixed two HDF/state issues: + - `ResultIndex` now preserves the requested result index instead of collapsing back to `1` + - `loadKV(..., default=...)` now returns the default when the target HDF5 file is missing +- the 2D stabilization pass also fixed: + - `McData2D(loadFromFile=...)` so it now actually loads prior state + - `McData2D` storage of 2D-only state such as `rawData2D` and orthogonal crop ranges + - `McData2D.clip()` crop bounds so the last valid row/column is not silently dropped + - `McData2D.reBin()` now returns a detached copy of `clippedData` instead of aliasing it +- `McData.load()` now tolerates missing stored `csvargs`, which matters for 2D state files that + intentionally store no CSV reader configuration. +- remaining 2D limitations are still explicit: + - direct 2D dataframe / CSV wrapper ingest was never completed and has since been removed from + the supported wrapper surface + - `McData2D.omit()` remains a warning/no-op + +### Step 1.3: Shrink the expensive tests + +Status: complete. + +Tasks: + +- reduce `nRep`, `maxIter`, and large SasModels usage in tests where the exact heavy load is not + the thing being tested +- prefer one or two representative integration tests over many near-duplicates +- isolate any environment-specific SasModels/OpenCL behavior + +Acceptance criteria: + +- integration coverage remains meaningful +- total CI runtime drops substantially + +Notes: + +- the main integration file now uses lean smoke-test defaults for most optimizer runs: + - `nContrib=96` + - `maxIter=1500` + - `nRep=1` for ordinary smoke coverage, with explicit overrides where multiprocessing is the + thing being exercised + - a fixed seed for the smoke-style coverage +- the statistically meaningful histogram regression test remains the dedicated `slow` case. +- the integration module now sets: + - `MPLBACKEND=Agg` + - `SAS_OPENCL=none` + - a repo-local `SAS_DLL_PATH` + so the tests do not depend on GUI backends, OpenCL availability, or writing compiled SasModels + kernels into the user home directory. +- the simulated-data histogram test no longer depends on prior test order; it can bootstrap its + own state when run alone, while reusing the multicore fit output during a full file run. +- current integration hot spots after this reduction pass: + - `test_optimizer_1D_sim1_multicore` about 5.3 s + - `test_optimizer_1D_sim0_singlecore` about 4.2 s + - `test_optimizer_1D_sphere_poor_inital_guess` about 2.5 s +- near-duplicate sphere smoke tests were collapsed so the integration file now focuses on distinct + behaviors: + - 2D fitting + - internal sphere model plus re-histogramming/plotting + - poor-initial-guess robustness + - hard-sphere structure factor + - single-core and multi-core simulated-data fitting + - restore-state / re-histogramming from saved output + - alternate SasModels kernels and in-place NXsas I/O + +## Phase 2: Introduce the shared data-model boundary + +Goal: make the MoDaCor types available in McSAS3 without immediately rewriting the whole package. + +### Step 2.1: Add a McSAS3 data-model import layer + +Status: complete. + +Tasks: + +- add a local McSAS3 module that imports or re-exports MoDaCor `BaseData`, `DataBundle`, and + `ProcessingData` +- decide whether the dependency is direct package dependency or transitional workspace coupling + +Acceptance criteria: + +- the rest of McSAS3 imports the data classes through one stable local module + +Notes: + +- `src/mcsas3/data_model.py` is now the single McSAS3 import boundary for MoDaCor + `BaseData` / `DataBundle` / `ProcessingData`. +- the shim prefers an installed `modacor` package and falls back to the sibling workspace + checkout at `../MoDaCor/src`. +- fast tests now validate the shim against the real MoDaCor types instead of placeholder local + stand-ins. + +### Step 2.2: Define canonical scattering bundle shapes + +Status: complete. + +Tasks: + +- lock down the canonical 1D bundle contract +- lock down the canonical 2D bundle contract +- define stage naming for raw/clipped/binned data in `ProcessingData` + +Acceptance criteria: + +- all later migration work uses the same agreed bundle keys and units + +Notes: + +- the canonical contract is now documented in `design_documentation/canonical_data_contract.md`. +- `src/mcsas3/data_adapters.py` defines the shared stage names: + - `sample_raw` + - `sample_clipped` + - `sample_binned` +- the same module now centralizes the transitional conversions between: + - legacy 1D `DataFrame` state + - legacy 2D dict-of-arrays state + - canonical MoDaCor `DataBundle` / `ProcessingData` + - legacy `measData` and plotting `DataFrame` views +- `McData.to_processing_data()` now exposes a derived canonical `ProcessingData` view without + changing the current legacy source of truth. This is the intended seam for Phase 3. + +## Phase 3: Refactor `McData` to canonical `ProcessingData` + +Goal: make data loading/preprocessing use the shared model internally. + +### Step 3.1: Canonicalize `McData1D` + +Status: complete. + +Tasks: + +- represent raw, clipped, and binned 1D data as `ProcessingData` +- derive plotting or tabular views from that, rather than storing `DataFrame` as primary state +- remove `measData` from the canonical in-memory path + +Acceptance criteria: + +- `McData1D` has one real source of truth +- unit and uncertainty handling is explicit in the `BaseData` objects + +Notes: + +- `McData1D` now keeps `ProcessingData` as its canonical in-memory stage store. +- `rawData`, `clippedData`, `binnedData`, and `measData` remain compatibility outputs, but + `linkMeasData()` now derives from canonical bundles rather than from `DataFrame` state. +- stage construction now flows through the shared adapter layer for: + - raw input + - clipped data + - rebinned data +- with the breaking migration now accepted, the 1D rebinner no longer carries unused legacy table + statistics such as `IStd`, `ISEM`, `IError`, `QStd`, `QSEM`, and `QError`; the maintained + output contract is now the smaller `Q`, `I`, `ISigma`, `QSigma` table plus the canonical + bundle. +- fast tests now assert that mutating a legacy `rawData` view does not mutate the canonical + `ProcessingData` bundle state. +- this was an intermediate resting point only; reusable preprocessing has since been extracted and + the wrapper module has since been removed. + +### Step 3.2: Canonicalize `McData2D` + +Status: complete. + +Tasks: + +- represent 2D signal, `Qx`, `Qy`, and mask as bundle entries +- stop treating flattened fit arrays as the primary stored form +- make the 2D path structurally consistent with the 1D path + +Acceptance criteria: + +- 1D and 2D data loaders produce the same kind of canonical object graph + +Notes: + +- `McData2D` now keeps `ProcessingData` as its canonical stage store for raw, clipped, and + rebinned image data. +- canonical 2D stages now carry image-shaped `signal`, `Qx`, `Qy`, and optional `mask` bundle + entries; flattened fit vectors are derived compatibility output only. +- `rawData2D`, flattened `rawData`, `clippedData`, `binnedData`, and `measData` remain available + as compatibility views during the optimizer and GUI migration. +- the 2D adapter layer now centralizes the reverse translations from canonical bundles back to: + - `rawData2D` + - flattened `rawData` + - cropped `clippedData` / `binnedData` dictionaries with `invMask`, `Qextent`, and flattened + fit vectors +- fast tests now assert that mutating the legacy `rawData2D` compatibility dict does not mutate + the canonical 2D bundle state. +- this was an intermediate resting point only; reusable preprocessing has since been extracted and + the wrapper module has since been removed. + +## Phase 4: Replace `measData` at the optimizer boundary + +Goal: stop passing the legacy dict through the execution core. + +### Step 4.1: Introduce an explicit optimizer input view + +Status: complete. + +Tasks: + +- define a narrow optimizer-facing adapter or typed view derived from `DataBundle` +- make `McHat`, `McCore`, and `optimizeScalingAndBackground` consume that contract + +Acceptance criteria: + +- the optimizer no longer depends on `measData` +- there is one well-defined translation from bundle data to execution arrays + +Notes: + +- `src/mcsas3/optimizer_input.py` now defines `OptimizerInput` plus the canonical conversions from: + - legacy `measData` + - canonical `DataBundle` +- `McData.to_optimizer_input()` initially provided the preferred bridge from canonical stage data + into the optimizer boundary during the migration. +- `McHat`, `McCore`, and `optimizeScalingAndBackground` now consume `OptimizerInput` internally. +- `McAnalysis` and `mc_plot` now read the same typed optimizer input instead of the legacy dict. +- the CLI flow now passes `McData.to_optimizer_input()` into optimization and histogramming, so + the canonical path is exercised in normal McSAS3 usage. + +### Step 4.2: Remove `measData` from stored state + +Status: complete. + +Tasks: + +- stop persisting `measData` as a primary HDF5 concept +- only retain temporary compatibility shims if absolutely necessary during migration + +Acceptance criteria: + +- no new code relies on `measData` + +Notes: + +- `McData.store()` no longer writes `measData` into the result HDF5 schema. +- overwrite paths now delete any stale legacy `/mcdata/measData` group before writing current + state, so refreshed result files do not carry the deprecated structure forward. +- fast 1D and 2D persistence tests now assert that stored files do not contain a `measData` + group, while reload still reconstructs the compatibility view from canonical stage data plus + stored preprocessing settings. + +## Phase 5: Migrate analysis, histogramming, and plotting + +Goal: use the same data model everywhere around optimization and make `ProcessingData` the real +entry point. + +Status: in progress. + +Tasks: + +- move `McAnalysis` and `McModelHistogrammer` to the same canonical measurement contract +- make plotting read bundle-derived views instead of internal `DataFrame` state +- simplify assumptions around `Q`, `I`, and `ISigma` packing +- replace `measDataLink` with a better canonical concept for the selected analysis stage +- make `McHat` / `McCore` accept a canonical sample `DataBundle` or selected `ProcessingData` + stage directly, with `OptimizerInput` retained only as a private last-mile execution adapter if + still needed for SasModels kernel setup +- make reduced chi-square, scaling/background fitting, and related optimizer math operate against + `BaseData`-backed signal and uncertainty without repeated unit-conversion overhead in hot paths +- move input-unit normalization to ingestion / preprocessing boundaries instead of late execution + stages +- expose the unique clipping / omission / rebinning logic in a form that can be reused directly + from canonical workflows +- switch CLI and notebook-style entry points from the old `McData*` objects to + `ProcessingData`-based carriers as a deliberate breaking API change +- add a stop / interrupt mechanism for `McHat` orchestration so GUI or CLI callers can cancel a + running analysis and all spawned repetition workers terminate cleanly + +Acceptance criteria: + +- optimization and post-processing consume the same data model +- a fit can start from `ProcessingData` plus an explicit selected stage without any wrapper object + +Notes: + +- `ProcessingData.analysis_stage` is now the canonical selected-stage marker, replacing the old + `measDataLink` concept in new code. +- `McData.to_processing_data()` now stamps `analysis_stage`, and `McData.to_analysis_bundle()` + now returns the selected canonical bundle. +- `McAnalysis` now accepts canonical `ProcessingData` or `DataBundle` input, and the CLI + histogram path now uses `ProcessingData` instead of legacy `measData`. +- `McPlot.resultCard()` now reads the measurement series from the canonical analysis bundle when it + is available. +- `McHat` and `McCore` now accept canonical selected-analysis bundles directly. +- `McCore` now derives SasModels kernel Q arrays and scaling/background fit arrays from the + canonical bundle path, while legacy dict support remains a temporary fallback. +- `optimizeScalingAndBackground` now accepts canonical bundle input directly for the optimizer hot + path. +- `OptimizerInput` is now reduced to a private compatibility/execution bridge rather than the + preferred public optimizer contract. +- fast regression coverage now checks that the internal-unit to SasModels-unit bridge preserves + the expected recovered volume fraction for a sphere model with fixed SLD contrast. +- raw 1D and 2D ingestion now normalizes declared or detected source units into canonical + internal units before legacy compatibility views are published. +- `McData.sourceQUnits` and `McData.sourceIntensityUnits` now record the original source-unit + declaration or NeXus unit metadata for reproducibility, while preprocessing operates on + canonical-unit values. +- clipping and omission ranges now apply in canonical internal units because normalization happens + before preprocessing. +- Review of MoDaCor processing modules indicates that the reusable value for this migration is the + `BaseData` / `DataBundle` / `ProcessingData` model itself, not the `ProcessStep` pipeline layer. +- MoDaCor does not currently provide a direct drop-in replacement for McSAS3's 1D clip / omit / + rebin flow. Its closest related pieces are the scattering-specific `IndexPixels` and + `IndexedAverager` modules, which assume `Q`, `Psi`, `pixel_index`, and `ProcessStep` + configuration plumbing. +- For McSAS3, the cleaner path is to extract small local preprocessing helpers that operate + directly on canonical `DataBundle` objects and preserve current McSAS3 semantics such as + `IEmin`, `QEMin`, log-Q binning, and the existing tabular uncertainty statistics. +- We should only revisit MoDaCor module reuse later if McSAS3 grows a fuller scattering + preprocessing pipeline with canonical geometry bundles where `IndexPixels` / + `IndexedAverager` can be adopted without adapter-heavy glue code. +- That helper layer now lives in `mcsas3.preprocessing`: + - `prepare_1d_bundle()` for clip / omit / rebin over canonical 1D bundles + - `prepare_2d_bundle()` for clip plus current 2D pass-through rebin behavior + - standalone helper functions for clip / omit / rebin so the logic can be reused outside + `McData*` +- `src/mcsas3/workflows.py` now exposes the direct canonical workflow helpers for: + - preparing 1D or 2D `ProcessingData` + - loading/storing canonical `ProcessingData` from result files + - running optimization from canonical `ProcessingData` +- `src/mcsas3/cli_tools.py` now uses that workflow layer for both optimization and histogramming, + so the supported CLI path no longer constructs `McData1D` directly. +- `src/mcsas3/ingestion.py` now owns shared 1D file ingestion for CSV, PDH, and 1D NeXus inputs, + including detected source-unit metadata and rejection of 2D NeXus data on the 1D path. +- `src/mcsas3/ingestion.py` now also owns shared 2D NeXus ingestion, including: + - default-path discovery from NeXus metadata + - resolution of combined `Q` datasets into canonical `Qx` / `Qy` + - optional explicit `pathDict` support for either combined `Q` or split `Qx` / `Qy` datasets + - detected source-unit metadata for canonical unit normalization +- `prepare_1d_processing_data_from_file()` now reads files directly through that ingestion helper + instead of constructing `McData1D`. +- `prepare_2d_processing_data_from_file()` now reads files directly through the shared 2D + ingestion helper instead of constructing `McData2D`. +- `mcsas3.workflows` no longer accepts `measDataLink` in read-config input; canonical config must + use `analysisStage`. +- adapter naming on maintained paths now follows the canonical convention: + - `fit_arrays_from_bundle()` is the maintained bridge from canonical bundles to flattened + optimizer arrays + - `frame_from_bundle()` now replaces the old `legacy_dataframe_from_bundle()` helper name +- top-level `mcsas3` now re-exports the maintained canonical workflow entry points and carrier + types, so notebook/script code can stay on: + - `prepare_*_processing_data*` + - `optimize_processing_data()` + - `load_result_processing_data()` + - `BaseData` / `DataBundle` / `ProcessingData` +- the README now documents that as the supported Python API and explicitly removes the old + `McData*` wrappers from the maintained surface. +- the main in-repo `notebooks/McSAS3.ipynb` example now uses `prepare_1d_processing_data_from_file` + plus canonical workflow helpers instead of constructing any wrapper carrier. +- the 1D compatibility tables now expose only canonical stage columns, rather than trying to + preserve arbitrary extra source columns from the original input dataframe. +- the NXsas read/write tests now copy source data into per-test temporary files and no longer + leave generated `.nxs` artifacts in `testdata/`. +- `reconstruct_2d_from_clipped_bundle()` now provides the maintained canonical replacement for + the old 2D wrapper-side `reconstruct2D()` helper. +- the wrapper modules and their dedicated unit-test files have now been removed entirely from the + repo; maintained tests assert against canonical stage bundles, workflow helpers, and + preprocessing functions directly. +- the old legacy-stage naming bridge in `data_adapters.py` (`rawData` / `clippedData` / + `binnedData` stage-link helpers) has been removed because no maintained code still uses it. +- interrupt / stop control should be owned by the McSAS3 core runner lifecycle even if the first + user-facing trigger is implemented in `McSAS3GUI`; the requirement is to stop all active + repetition workers launched by `McHat`. +- the 1D integration lane now exercises the canonical workflow helpers for file ingest, result-file + persistence, and result reload instead of routing those paths through `McData1D`. +- the supported 2D integration lane now also exercises the canonical workflow helper for file + ingest instead of routing the fit setup through `McData2D`. + +## Phase 6: HDF5 schema and persistence cleanup + +Goal: make the result file reflect the real domain model instead of implementation artifacts. + +Status: implemented with canonical-only writes and canonical-only loads. + +Tasks: + +- design a `ProcessingData`-oriented persistence layout +- add readers/writers for canonical bundle data +- write the full archival `ProcessingData` state for original, clipped, and binned stages +- persist the selected analysis stage, preprocessing settings, canonical units, and enough + metadata to reproduce how the fit input was derived +- keep any temporary migration bridge as short as possible + +Notes: + +- result files now store first-class canonical `ProcessingData` under + `/analyses/MCResult*/mcdata/processingData`, including: + - stage bundles for `sample_raw`, `sample_clipped`, and `sample_binned` + - `BaseData` signal arrays, weights, uncertainties, units, and `rank_of_data` + - bundle metadata such as `default_plot` and `description` + - the selected `analysis_stage` +- new result files no longer duplicate legacy `rawData`, `rawData2D`, `clippedData`, or + `binnedData` groups. +- `load_result_processing_data()` now requires canonical `processingData` and raises if a result + file only contains the legacy stage-group layout. + +Acceptance criteria: + +- HDF5 schema maps cleanly onto canonical McSAS3 data objects +- file readers are explicit about requiring the canonical schema +- the result file is archival enough to trace and reproduce how the fit was produced from the + stored canonical data + +## Phase 7: McSAS3GUI coordination + +Status: complete. + +Goal: move the GUI off direct coupling to McSAS3 internals. + +Tasks: + +- replace GUI use of `McData1D.rawData`, `clippedData`, `binnedData` internals with stable + McSAS3 canonical workflow and bundle APIs +- replace GUI reliance on exact HDF5 internal paths where feasible +- update GUI optimization preview and histogram preview to consume new APIs + +Acceptance criteria: + +- McSAS3GUI no longer depends on McSAS3 implementation details that we intend to remove + +Notes: + +- `McSAS3GUI` now centralizes its McSAS3 coupling in a small bridge module instead of importing + removed wrapper types or embedding HDF5 path strings in widgets +- the data-loading tab now prepares canonical `ProcessingData` via McSAS3 workflow helpers and + derives plotting frames from canonical stage bundles rather than `McData1D.rawData`, + `clippedData`, and `binnedData` +- the run-settings tab now triggers preview optimizations through + `optimize_processing_data(...)` and loads preview metrics through the GUI bridge instead of + calling `McHat.run(mds.measData.copy(), ...)` and reading `/analyses/.../mcdata/measData/...` + directly +- the GUI optimization and preview buttons now enter an explicit running/abort state and use the + core-owned `McHat.request_stop()` hook instead of relying on subprocess termination +- targeted GUI bridge tests and GUI-side Ruff checks were run against the current McSAS3 source + tree on `PYTHONPATH`, confirming that the GUI now works against the stabilized canonical API +- GUI-side `pytest` collection now bootstraps the sibling `McSAS3/src` checkout automatically via + `tests/conftest.py`, so local development no longer depends on manually exporting `PYTHONPATH` + just to keep the GUI tests on the current core source tree + +## Phase 8: Maintainability and API hardening + +Goal: make the codebase easier to reason about, safer to change, and cheaper to maintain after +the core migration lands. + +Status: split into `8A` before `McSAS3GUI` migration and `8B` after the GUI validates the +stabilized core API. + +Tasks: + +- simplify module boundaries and reduce duplicated 1D / 2D logic where the behavior is genuinely + shared +- factor clipping, omission, rebinning, optimizer preparation, and persistence translation into + smaller, testable units with clearer ownership +- remove transitional or redundant compatibility code once the replacement path is proven +- tighten `attrs` model definitions: + - add validators and converters where configuration enters the system + - avoid mutable class-level defaults and hidden shared state + - prefer explicit initialization and post-init normalization over ad hoc mutation in methods +- add type hints to remaining untyped methods and narrow overly broad `dict` / `object` usage +- add modest docstrings to public classes and methods, plus non-obvious internal helpers where the + behavior is easy to misread +- replace user-facing `assert` validation with explicit exceptions where the failure mode is part + of normal input or file handling rather than an internal invariant +- replace leftover `print` debugging with structured logging at appropriate levels +- streamline data copying and compatibility-view generation so the code does not maintain more + parallel state than necessary +- normalize naming and API surfaces across the canonical workflow, optimizer, analysis, and + plotting layers +- consider a lightweight static typing gate in CI once the codebase has broad enough annotations + to make it useful + +### Step 8A: Core API hardening before GUI migration + +Status: complete. + +Tasks: + +- remove remaining legacy naming such as `analysisData` on maintained public and semi-public core + paths +- replace user-facing `assert` validation with explicit exceptions on the maintained canonical + surface +- tighten validation and modest typing/docstrings on the canonical workflow, optimizer, analysis, + and histogramming entry points +- define the core-owned stop / interrupt interface for `McHat` orchestration + +Acceptance criteria: + +- the canonical core API is explicit enough that `McSAS3GUI` can migrate against it without + guessing intent from old names or assertion failures +- the remaining breaking changes are deliberate and documented before GUI harmonization starts + +Notes: + +- maintained entry points now use `analysis_input` naming on the core path +- assertion-style validation has been replaced with explicit `ValueError` / `TypeError` failures + across the maintained canonical/public surface, including CLI config handling, HDF helpers, + optimizer preparation, analysis, histogramming, and model configuration +- canonical analysis-data and optimizer-input helpers now use authoritative canonical Q + coordinates directly; `qNudge` has been removed from the maintained surface +- `McHat` now exposes a core-owned stop request path (`request_stop()`, `clear_stop_request()`, + `stop_requested()`, `isRunning`, `lastRunStopped`) and `McCore.optimize()` now honors that stop + callback so in-flight repetitions exit cleanly instead of only stopping between repetitions +- default tests, opt-in integration tests, and `tox -e check` all pass after the hardening pass, + so `McSAS3GUI` can now migrate against the stabilized core API + +### Step 8B: Post-GUI cleanup and simplification + +Status: in progress. + +Tasks: + +- use the `McSAS3GUI` migration as the final client review of the core API +- remove any cleanup targets that turned out not to matter to the GUI or supported workflows +- continue de-duplication, typing, logging cleanup, and structural simplification after the GUI is + off internal assumptions + +Acceptance criteria: + +- major modules have clearer single responsibilities and less duplicated logic +- object validation is explicit and reliable at API boundaries +- most maintained code paths have meaningful type hints and concise docstrings +- internal state transitions are easier to follow and require fewer compatibility shims + +Notes: + +- first `8B` slice completed: maintained runtime progress/error reporting in `mc_core`, + `mc_hat`, `mc_analysis`, and `mc_hdf` now uses module loggers instead of direct `print(...)` + calls, and `McHat.runOnce(bufferStdIO=True)` now attaches a temporary `mcsas3` log handler so + multiprocessing worker output still round-trips back to the parent process cleanly +- second `8B` slice completed: `McOpt` is now an explicit `attrs` model with per-instance list + state instead of mutable class-level defaults and reset-by-hand constructor logic, and the + pseudo-SasModels metadata scaffolding in `mc_model` has been collapsed from nested dummy helper + classes into small local helpers with copied defaults; this keeps the remaining old optimizer / + model layer behavior intact while removing obvious shared-state hazards +- third `8B` slice completed: `McModel` no longer keeps stale instance state as class attributes, + its initialization path is now split into small helpers for reset/config/random-generator / + parameter-set / loader dispatch setup, and the pseudo-model classes now keep their state only on + instances instead of mixing class-level and instance-level attributes +- fourth `8B` slice completed: `optimizeScalingAndBackground` in `osb.py` now has an explicit + coercion path for canonical bundle / optimizer input / raw array inputs, no longer relies on + stale class-level attributes, and preserves caller-provided `xBounds` instead of silently + replacing them with defaults +- fifth `8B` slice completed: `mc_model_histogrammer.py` no longer keeps mutable class-level + analysis state, now copies caller-provided histogram range frames instead of mutating them in + place, and resolves per-range setup through small helpers; `McAnalysis` now explicitly adopts + those resolved histogram ranges instead of depending on histogrammer side effects +- sixth `8B` slice completed: the remaining `mc_model` helper/API surface has been normalized so + model queries and random-parameter generation have explicit snake_case entry points + (`available_models()`, `model_parameters()`, `fit_keys()`, `generate_random_parameter_values()`) + and the old camelCase helper wrappers have been removed entirely from the maintained code path; + model-loader dispatch is now driven by an explicit custom-loader map instead of open-coded + branching +- seventh `8B` slice completed: the maintained canonical API surface now has a modest docstring + pass across `workflows.py`, `preprocessing.py`, `data_adapters.py`, `optimizer_input.py`, + `mc_core.py`, `mc_hat.py`, `mc_analysis.py`, and `osb.py`, so the current supported entry + points and runtime control hooks are documented without reviving legacy compatibility comments +- eighth `8B` slice completed: the maintained canonical workflow and adapter boundary now does + stricter runtime validation for malformed ranges, negative bin counts, non-numeric relative + uncertainty floors, malformed optimizer-input Q payloads, and mismatched bundle component + shapes; the supported workflow/preprocessing/adapter signatures were also narrowed to clearer + canonical type aliases instead of carrying broad unstructured `dict` assumptions + +## Phase 9: Documentation and release delivery + +Goal: leave McSAS3 in a form that users can install, understand, and run without reading the +source tree. + +Status: in progress. + +Tasks: + +- refresh top-level documentation once the new data model and public APIs are stable +- add a concise quickstart covering: + - installation + - preparing input data + - running a basic optimization + - inspecting or plotting results +- document the supported workflows for CLI, Python usage, and result-file handling +- add migration notes where user-visible behavior changed during the refactor +- document the breaking transition from `McData*` / `measData` notebook workflows to + `ProcessingData`-based workflows +- define a release engineering path for packaged user distributions on: + - macOS + - Windows + - Linux +- choose and implement the packaging approach for standalone user-facing builds +- validate packaged builds on each target platform with a minimal smoke-test workflow + +Acceptance criteria: + +- a new user can get from install to first result using the quickstart documentation +- release artifacts exist in a reproducible form for macOS, Windows, and Linux +- the documented workflows match the actual supported interfaces + +Notes: + +- first Phase 9 slice completed: the repo now includes a generated Mermaid structure document at + `design_documentation/generated_module_dependencies.md`, driven by + `tools/generate_dependency_diagram.py`, and the top-level README plus design-documentation index + now link to it as the current module-structure overview +- second Phase 9 slice completed: the user-facing docs now have dedicated `installation`, + `quickstart`, `usage`, `migration`, and `structure` pages in `docs/`, and the README no longer + claims that no GUI exists; the quickstart explicitly documents the maintained 1D CLI workflow + and the canonical Python `ProcessingData` workflow +- third Phase 9 slice completed: the core repo now has a reproducible standalone CLI build path + via `tools/build_standalone.py`, `tox -e standalone`, and `.github/workflows/standalone.yml`, + targeting `mcsas3-runner` and `mcsas3-histogrammer` on macOS, Windows, and Linux; the CLI + runtime path handling was also fixed so bundled example configurations and quickstart data resolve + correctly in both source and frozen executions +- fourth/fifth Phase 9 slices superseded: standalone builds now consume `modacor` as a normal + package dependency, and the temporary sibling-checkout / `MCSAS3_MODACOR_SRC` plumbing has been + removed from the maintained build and CI paths +- sixth Phase 9 slice completed: the CLI support code is now split so the optimization runner and + histogrammer build from separate dependency trees, and the standalone builder excludes + histogram-only plotting/notebook modules from `mcsas3-runner`; on macOS that reduced the local + runner bundle from roughly 175 MB to 140 MB and the combined zip archive from roughly 208 MB to + 160 MB while preserving the smoke-test path + +## Phase 10: Final cleanup + +Goal: remove temporary bridges and freeze the new model. + +Tasks: + +- remove any remaining private compatibility shims +- remove obsolete config and tests tied to the legacy model +- refresh internal docs and user docs + +Acceptance criteria: + +- there is one canonical internal data model +- test and tooling defaults are fast enough to support ongoing maintenance + +Notes: + +- first Phase 10 slice completed: maintained optimizer-entry normalization now accepts only + canonical selected bundles or an existing `OptimizerInput`, and the public notebook wording no + longer refers to the removed `McData*` carrier as if it were still supported +- second Phase 10 slice completed: remaining adapter helper names on the maintained path no longer + expose `legacy_*` vocabulary (`frame_from_bundle()` and `raw_2d_stage_from_bundle()` are the + supported names), and the current canonical contract docs now describe those helpers directly +- third Phase 10 slice completed: the maintained README and example notebook now present the final + installed-package workflow and canonical `ProcessingData` / `DataBundle` API, while migration-era + details are left to the dedicated historical and migration documents +- fourth Phase 10 slice completed: the public `analysis_data_from_bundle()` bridge and the unused + `OptimizerInput.to_analysis_data()` back-conversion have been removed, and the remaining example + notebook/tests now use canonical bundles or `fit_arrays_from_bundle()` directly + +## Phase 11: McSAS3GUI follow-on + +Goal: apply the same architectural cleanup, API hardening, testing discipline, documentation, and +release readiness to McSAS3GUI after the McSAS3 core contract is stable. + +Status: in progress. + +Tasks: + +- realign McSAS3GUI against the stabilized McSAS3 public APIs instead of internal implementation + details +- repeat the same maintainability pass in the GUI repo: + - simplify module boundaries + - improve typing and validation + - remove unnecessary legacy code + - strengthen tests and tooling +- refresh McSAS3GUI user documentation and quickstart material +- provide packaged user-facing GUI builds for macOS, Windows, and Linux if that remains the chosen + delivery model + +Acceptance criteria: + +- McSAS3GUI depends only on stable McSAS3 interfaces +- the GUI repo reaches the same baseline for maintainability, testing, docs, and packaging as the + core repo + +Notes: + +- first Phase 11 slice completed: `mcsas3_bridge.py` no longer reimplements bundle-to-frame or + fit-array conversion locally; it now uses McSAS3's maintained `frame_from_bundle()` and + `fit_arrays_from_bundle()` helpers directly for GUI plotting and preview loading +- second Phase 11 slice completed: GUI histogram launching no longer builds shell-style command + strings with manual quoting; `HistRunTab`, `HistogramSettingsTab`, `TaskRunnerMixin`, and + `BaseWorker` now use explicit subprocess argument lists via `utils/mcsas3_cli.py`, with a + maintained `mcsas3-histogrammer` command preference and a module fallback for environments where + the entry point is not on `PATH` +- third Phase 11 slice completed: the generic GUI task runner now reports structured success / + failure through `BaseWorker.finished_signal(bool, str)` instead of a fire-and-forget completion + ping, `TaskRunnerMixin` surfaces failure messages to the user, and `GettingStartedTab` now uses + explicit exceptions and logger warnings instead of runtime `assert` / `print` behavior +- fourth Phase 11 slice completed: `McSAS3GUI` now has a clean local `tox -e check` baseline with + explicit `MANIFEST.in` include/exclude rules for tracked versus ignored config assets, tox 4 no + longer collides on the package env override, and the repo-local Ruff/import-order baseline now + matches the tox check environment exactly +- fifth Phase 11 slice completed: `optimization_worker.py` now uses shared helper functions for + YAML config loading, stale-result cleanup, preview run coercion, stop detection, and `McHat` + execution setup instead of duplicating that plumbing between the multi-file and preview workers; + direct worker regression coverage was added in `tests/test_optimization_worker.py` +- sixth Phase 11 slice completed: the stray debug `print` in `yaml_editor_widget.py` has been + removed, and the GUI pre-commit Ruff hook is now pinned to the same Ruff version family used by + the local environment and tox check env so hooks and `tox -e check` enforce the same import-order + baseline +- seventh Phase 11 slice completed: repeated file-selector validation/update logic has been + centralized in `gui/file_selection_helpers.py`, the duplicated `load_*_file()` implementations in + the optimization, histogram-run, histogram-settings, and data-loading tabs now share that helper, + and direct regression coverage was added in `tests/test_file_selection_helpers.py` +- eighth Phase 11 slice completed: `TaskRunnerMixin` now owns the shared worker startup, + progress/status wiring, and default completion handling for file-oriented GUI runs, so + `OptimizationRunTab` and `HistRunTab` no longer duplicate that orchestration; direct regression + coverage was added in `tests/test_task_runner_mixin.py` +- ninth Phase 11 slice completed: the shared run/abort button behavior now lives in + `gui/run_control_helpers.py`, so `OptimizationRunTab` and `RunSettingsTab` no longer duplicate + the “running / click to abort / restore default state” logic; direct regression coverage was + added in `tests/test_run_control_helpers.py` +- tenth Phase 11 slice completed: the preview/test-run path now uses + `gui/run_settings_helpers.py` for run-config merging and preview result-file lifecycle, and + `RunSettingsTab` now manages preview startup/cleanup through smaller explicit helper methods + instead of a single large side-effect-heavy code path; direct regression coverage was added in + `tests/test_run_settings_helpers.py` +- eleventh Phase 11 slice completed: the maintained GUI bridge/worker/helper surface now has a + modest typing/docstring pass in `mcsas3_bridge.py`, `optimization_worker.py`, + `run_control_helpers.py`, `run_settings_helpers.py`, `base_worker.py`, `task_runner_mixin.py`, + and `mcsas3_cli.py`, so the remaining internal cleanup target is effectively complete +- twelfth Phase 11 slice completed: the GUI user docs now match the maintained canonical McSAS3 + workflow and launch surface (`mcsas3gui`, `m3gui`, and `python -m mcsas3gui`), dedicated + `quickstart`, `usage`, and `structure` pages have been added, a generated Mermaid dependency + diagram is now tracked via `tools/generate_dependency_diagram.py`, and local validation passes + through `pytest`, `pre-commit`, `tox -e check`, and `tox -e docs` +- thirteenth Phase 11 slice completed: `McSAS3GUI` now has a maintained standalone packaging path + via `tools/build_standalone.py` and `tox -e standalone`, the frozen GUI bundle includes a + bundled `mcsas3-histogrammer` helper for histogramming tabs, local macOS artifact creation is + validated, and a cross-platform `.github/workflows/standalone.yml` matrix has been added for + macOS, Windows, and Linux CI packaging +- fifteenth Phase 11 slice completed: the GUI standalone CI matrix now has stricter post-build + verification. `tools/build_standalone.py` writes an explicit artifact manifest in + `build_info.json`, the workflow validates the unpacked bundle and bundled histogram helper paths + recorded there on every runner, the shell/install steps are now more robust for cross-platform + GitHub Actions execution, and local validation passes through `pytest`, `pre-commit`, + `tox -e check`, `tox -e docs`, and `tox -e standalone` +- sixteenth Phase 11 slice completed: the GUI standalone build path now supports in-flight core + branch alignment during CI. `tox -e standalone` passes through `MCSAS3GUI_MCSAS3_SRC` and + `MCSAS3GUI_MODACOR_SRC`, and `.github/workflows/standalone.yml` now accepts optional + `mcsas3_ref` / `modacor_ref` inputs while defaulting the McSAS3 checkout to the matching GUI + branch (`github.head_ref` / `github.ref_name`) instead of always checking out `McSAS3 main`. + This removes the temporary requirement to merge McSAS3 first before validating McSAS3GUI + standalone builds against the upgraded core +- fourteenth Phase 11 slice completed: the GUI docs build no longer emits the earlier PyQt + autosummary duplicate-object warnings after the autosummary class template stopped pulling in + inherited Qt members, and the README dependency-diagram link now points at a stable tracked + source page instead of a broken published URL +- remaining GUI docs noise is non-blocking external linkcheck churn from historical changelog + commit URLs returning intermittent GitHub `502` / `service unavailable` responses during local + docs builds +- standalone-app usability refinements noted for follow-up: + - resolved: saving a read-configuration YAML file to a new location now preserves the current + editor content immediately; the config dropdown refresh no longer triggers an unintended reload + of another preset while the save completes + - resolved: the run-settings preview optimization now streams live progress into the bottom + information panel, including reduced chi-square updates and accepted/attempt counters together + with the configured iteration limits + +## Immediate next steps + +These are the next three steps I recommend working on in order: + +1. Let the GUI standalone workflow run on macOS, Windows, and Linux and fix any platform-specific + packaging issues it exposes, now that the workflow can check out the matching in-flight + `McSAS3` branch instead of defaulting to `main`. +2. Do a final repo-wide doc and release consistency sweep now that both core and GUI packaging + paths exist. +3. If the standalone matrix is green, close out the remaining release-readiness items and decide + whether any remaining docs-linkcheck noise should be cleaned up or simply documented as + non-blocking external churn. + +## Update rule for this file + +Whenever a step is started or completed: + +- update the `Current status` checklist +- update the relevant phase/step status line +- add or adjust acceptance criteria if the scope changed +- keep the ordering stable unless there is a strong reason to resequence work diff --git a/docs/conf.py b/docs/conf.py index 3ae63d5..2e42b4a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -36,9 +36,7 @@ release = version commit_id = None try: - commit_id = ( - subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]).strip().decode("ascii") - ) + commit_id = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]).strip().decode("ascii") except subprocess.CalledProcessError as e: print(e) @@ -85,7 +83,7 @@ + r".*", # attempted fix of '406 Client Error: Not Acceptable for url' # https://github.com/sphinx-doc/sphinx/issues/1331 - join(project_meta["project"]["urls"]["repository"], "commit", r"[0-9a-fA-F]+") + join(project_meta["project"]["urls"]["repository"], "commit", r"[0-9a-fA-F]+"), ] linkcheck_anchors_ignore_for_url = [ r"https://pypi\.org/project/[^/]+", diff --git a/docs/index.rst b/docs/index.rst index e641100..0893ddb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,7 +7,11 @@ Contents readme installation + quickstart usage + migration + structure + release_delivery reference/index contributing authors diff --git a/docs/installation.rst b/docs/installation.rst index bae7505..96122b2 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -2,6 +2,29 @@ Installation ============ -At the command line:: +Install McSAS3 from PyPI: - pip install mcsas3 +.. code-block:: bash + + pip install mcsas3 + +Notes +===== + +- McSAS3 depends on ``sasmodels``, ``attrs``, ``pandas``, ``h5py``, ``pint``, and ``pyyaml``. +- If SasModels / OpenCL causes clearly wrong fits, disable OpenCL before launching McSAS3: + +.. code-block:: bash + + export SAS_OPENCL=none + +- On Windows, ``pip install tinycc`` can still be useful if SasModels needs a working compiler. + +Development install +=================== + +For local development from a checkout: + +.. code-block:: bash + + pip install -e . diff --git a/docs/migration.rst b/docs/migration.rst new file mode 100644 index 0000000..43bee65 --- /dev/null +++ b/docs/migration.rst @@ -0,0 +1,61 @@ +========================= +Migration from ``McData*`` +========================= + +McSAS3 now uses canonical MoDaCor-style ``ProcessingData`` / ``DataBundle`` / ``BaseData`` +carriers throughout the maintained core API. + +This is a breaking change. + +What changed +============ + +- ``McData``, ``McData1D``, and ``McData2D`` are no longer part of the maintained core package. +- The old ``measData`` / ``measDataLink`` vocabulary has been replaced by canonical selected-stage + handling on ``ProcessingData``. +- New result files store canonical ``processingData`` rather than duplicated legacy stage groups. + +Old to New Mapping +================== + +- ``McData1D(...).from_file(...).prepare()``: + use ``prepare_1d_processing_data_from_file(...)`` +- ``McData2D(...).from_file(...).prepare()``: + use ``prepare_2d_processing_data_from_file(...)`` +- manual in-memory ``McData1D`` construction: + use ``prepare_1d_processing_data(...)`` +- manual in-memory ``McData2D`` construction: + use ``prepare_2d_processing_data(...)`` +- ``mcd.store(...)`` / ``mcd.load(...)``: + use ``store_result_processing_data(...)`` / ``load_result_processing_data(...)`` +- ``mcd.measData``: + use ``selected_bundle_from_processing(processing)`` +- ``mcd.clip()``, ``mcd.omit()``, ``mcd.reBin()``: + use the helpers in ``mcsas3.preprocessing`` + +Minimal Example +=============== + +Instead of: + +.. code-block:: python + + # old style, no longer maintained + # mcd = McData1D(...) + # mcd.prepare() + +Use: + +.. code-block:: python + + from mcsas3 import prepare_1d_processing_data_from_file, selected_bundle_from_processing + + processing = prepare_1d_processing_data_from_file(...) + analysis_bundle = selected_bundle_from_processing(processing) + +Notes +===== + +- The canonical internal units are ``1 / nm`` for ``Q`` and ``1 / (m sr)`` for intensity. +- Input units are normalized at ingestion time. +- If you need the current code structure overview, see :doc:`structure`. diff --git a/docs/quickstart.rst b/docs/quickstart.rst new file mode 100644 index 0000000..7e81cd3 --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,95 @@ +========== +Quickstart +========== + +This page covers the supported fast-start paths for McSAS3: + +- a 1D command-line workflow using YAML configuration files +- a Python workflow built on canonical ``ProcessingData`` and ``DataBundle`` objects + +Command-Line Quickstart +======================= + +The maintained CLI path currently covers 1D source-data ingestion. A minimal end-to-end run from a +source checkout looks like this: + +1. Run the optimizer: + +.. code-block:: bash + + mcsas3-runner \ + -f testdata/quickstartdemo1.csv \ + -F example_configurations/read_config_csv.yaml \ + -R example_configurations/run_config_spheres_auto.yaml \ + -r result.nxs \ + -d + +2. Histogram the stored repetitions and generate a PDF summary: + +.. code-block:: bash + + mcsas3-histogrammer \ + -r result.nxs \ + -H example_configurations/hist_config_dual.yaml + +This produces: + +- ``result.nxs``: the canonical HDF5 result file, including stored ``ProcessingData`` +- ``result.pdf``: the histogram/result summary generated by the histogramming step + +Python Quickstart +================= + +For new scripts and notebooks, use the canonical top-level workflow API: + +.. code-block:: python + + from pathlib import Path + + from mcsas3 import ( + STAGE_CLIPPED, + load_result_processing_data, + optimize_processing_data, + prepare_1d_processing_data_from_file, + selected_bundle_from_processing, + ) + + processing = prepare_1d_processing_data_from_file( + Path("testdata/quickstartdemo1.csv"), + csvargs={"sep": ";", "header": None, "names": ["Q", "I", "ISigma"]}, + nbins=100, + analysis_stage=STAGE_CLIPPED, + ) + + optimize_processing_data( + processing, + Path("result.nxs"), + modelName="mcsas_sphere", + fitParameterLimits={"radius": "auto"}, + staticParameters={"background": 0.0, "scale": 1.0, "sld": 33.4, "sld_solvent": 0.0}, + maxIter=1000, + convCrit=1.0, + nRep=2, + nCores=1, + ) + + restored = load_result_processing_data(Path("result.nxs")) + selected_bundle = selected_bundle_from_processing(restored) + print(selected_bundle["signal"].signal.shape) + +2D Note +======= + +The canonical Python workflow supports 2D preparation through: + +- ``prepare_2d_processing_data(...)`` +- ``prepare_2d_processing_data_from_file(...)`` + +The maintained CLI quickstart above is intentionally documented as a 1D path. + +Next Steps +========== + +- See :doc:`usage` for the supported CLI, Python, and result-file workflows. +- See :doc:`migration` if you are updating notebooks that previously used ``McData*``. +- See :doc:`structure` for the current module-structure overview. diff --git a/docs/release_delivery.rst b/docs/release_delivery.rst new file mode 100644 index 0000000..4c29548 --- /dev/null +++ b/docs/release_delivery.rst @@ -0,0 +1,73 @@ +================ +Release delivery +================ + +McSAS3 now has two distinct delivery tracks: + +- standard Python package delivery via wheel and sdist +- standalone CLI bundles for macOS, Windows, and Linux + +Python packages +=============== + +The existing package build remains: + +- ``python -m build`` +- ``tox -e build`` + +This produces: + +- a source distribution +- a wheel for normal Python installation + +Standalone CLI bundles +====================== + +The standalone build path packages the maintained command-line tools: + +- ``mcsas3-runner`` +- ``mcsas3-histogrammer`` + +Local build command: + +.. code-block:: bash + + tox -e standalone + +This runs ``tools/build_standalone.py`` and produces: + +- ``dist/standalone//``: the unpacked standalone bundle +- ``dist/standalone/mcsas3-standalone-.zip``: the distributable archive + +Current implementation notes +============================ + +- the standalone path uses PyInstaller in ``onedir`` mode +- the standalone build uses the normal ``modacor`` package dependency for canonical data-model + classes (no sibling source checkout override is required) +- bundled example configurations and ``testdata/quickstartdemo1.csv`` are included so the CLI + default paths resolve correctly in frozen builds +- the local builder smoke-tests each generated executable with ``--help`` +- the repo ships a local PyInstaller ``sasmodels`` hook so the standalone bundles keep the runtime + model/kernel data without also bundling the full upstream documentation tree +- the runner build excludes histogram-only plotting and notebook extras, so ``mcsas3-runner`` is + intentionally slimmer than ``mcsas3-histogrammer`` + +CI workflow +=========== + +The repo now includes ``.github/workflows/standalone.yml`` which builds standalone archives on: + +- Linux +- macOS +- Windows + +This workflow is also wired into the top-level CI/CD workflow so the standalone bundles are built +reproducibly alongside the normal package artifacts. + +Scope +===== + +This page describes standalone delivery for the McSAS3 core CLI only. + +Packaged GUI app delivery belongs to the follow-on McSAS3GUI work. diff --git a/docs/structure.rst b/docs/structure.rst new file mode 100644 index 0000000..55aea9f --- /dev/null +++ b/docs/structure.rst @@ -0,0 +1,20 @@ +========= +Structure +========= + +McSAS3 ships a generated module dependency overview for the maintained top-level modules in +``src/mcsas3``. + +The rendered Mermaid version is tracked in the repository at: + +- ``design_documentation/generated_module_dependencies.md`` + +It is generated by: + +- ``tools/generate_dependency_diagram.py`` + +The content below is included from the generated Markdown file. + +.. include:: ../design_documentation/generated_module_dependencies.md + :parser: myst_parser.sphinx_ + :start-line: 3 diff --git a/docs/usage.rst b/docs/usage.rst index 9a728c9..ea9e8fc 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -2,6 +2,68 @@ Usage ===== -To use McSAS3 in a project:: +Supported interfaces +==================== - import mcsas3 +McSAS3 currently supports three maintained user-facing paths: + +- 1D CLI optimization via ``mcsas3-runner`` +- histogramming of stored results via ``mcsas3-histogrammer`` +- canonical Python workflows built on ``ProcessingData`` and ``DataBundle`` + +CLI workflows +============= + +The CLI uses YAML configuration files: + +- a read configuration for source data +- a run configuration for the optimizer +- a histogram configuration for post-processing + +See :doc:`quickstart` for a minimal example command sequence. + +Python workflows +================ + +New scripts and notebooks should use the top-level canonical workflow API: + +.. code-block:: python + + from mcsas3 import ( + load_result_processing_data, + optimize_processing_data, + prepare_1d_processing_data, + prepare_1d_processing_data_from_file, + prepare_2d_processing_data, + prepare_2d_processing_data_from_file, + selected_bundle_from_processing, + ) + +Result files +============ + +McSAS3 result files now store canonical ``ProcessingData`` at: + +- ``/analyses/MCResult*/mcdata/processingData`` + +The maintained load/store helpers are: + +- ``load_result_processing_data(...)`` +- ``store_result_processing_data(...)`` + +Reusable preprocessing helpers +============================== + +Canonical preprocessing helpers live in ``mcsas3.preprocessing``: + +- clipping +- omission +- 1D rebinning +- 2D reconstruction from clipped bundles + +See also +======== + +- :doc:`quickstart` +- :doc:`migration` +- :doc:`structure` diff --git a/example_configurations/read_config_csv.yaml b/example_configurations/read_config_csv.yaml index 4d53190..1868178 100644 --- a/example_configurations/read_config_csv.yaml +++ b/example_configurations/read_config_csv.yaml @@ -1,5 +1,8 @@ --- # configuration used to read files into McSAS3. this is assumed to be a 1D file in csv format # Note that the units are assumed to be 1/(m sr) for I and 1/nm for Q +# Optional overrides for non-NeXus inputs: +# QUnits: "1 / angstrom" +# IUnits: "1 / centimeter / steradian" nbins: 100 dataRange: - 0.0 # minimum diff --git a/example_configurations/read_config_nxs.yaml b/example_configurations/read_config_nxs.yaml index 605c5bb..b6ef46a 100644 --- a/example_configurations/read_config_nxs.yaml +++ b/example_configurations/read_config_nxs.yaml @@ -1,5 +1,8 @@ --- # configuration used to read nexus files into McSAS3. this is assumed to be a 1D file in nexus # Note that the units are assumed to be 1/(m sr) for I and 1/nm for Q +# Dataset units are auto-detected from the NeXus file when available, but can be overridden: +# QUnits: "1 / angstrom" +# IUnits: "1 / centimeter / steradian" # if necessary, the paths to the datasets can be indicated. nbins: 100 dataRange: diff --git a/example_configurations/read_config_nxs_with_omit.yaml b/example_configurations/read_config_nxs_with_omit.yaml index 55fdf97..837bb86 100644 --- a/example_configurations/read_config_nxs_with_omit.yaml +++ b/example_configurations/read_config_nxs_with_omit.yaml @@ -1,5 +1,8 @@ --- # configuration used to read nexus files into McSAS3. this is assumed to be a 1D file in nexus # Note that the units are assumed to be 1/(m sr) for I and 1/nm for Q +# Dataset units are auto-detected from the NeXus file when available, but can be overridden: +# QUnits: "1 / angstrom" +# IUnits: "1 / centimeter / steradian" # if necessary, the paths to the datasets can be indicated. nbins: 100 dataRange: diff --git a/notebooks/McSAS3.ipynb b/notebooks/McSAS3.ipynb index b5b8f38..b5eec94 100644 --- a/notebooks/McSAS3.ipynb +++ b/notebooks/McSAS3.ipynb @@ -22,12 +22,11 @@ " \n", "Usage:\n", "--\n", - "This notebook shows an example usage of McSAS3, leveraging some of the possibilities of the code. However, it does not work automatically, there are bits that need to be adapted. \n", + "This notebook shows an example usage of McSAS3 using the maintained installed-package workflow. It assumes that `mcsas3` and `sasmodels` are available in the active Python environment.\n", "\n", - "Firstly, in the import section, change the two paths to wherever you have the SasView and McSAS3 codebase installed, respectively. # NOTE: this could be solved with git imports maybe?\n", "\n", "Secondly, there is the file loading section. Here, you must make sure that your 1D data is loaded correctly, complete with uncertainty estimates. A plot is provided to help you. \n", - "The McData class supports the following formats:\n", + "The maintained McSAS3 workflow supports the following input formats:\n", " - 3-column ascii files, which require you to set the csv loader arguments correctly. This data must be in units of Q: 1/nm, I: 1/(m sr), ISigma: 1/(m sr). \n", " - NXcanSAS files are supported # NOTE: double-check. \n", " - pdh files are supported (Anton Paar's 1D format)\n", @@ -62,49 +61,50 @@ "Imports\n", "--\n", "\n", - "This section imports the necessary packages (and maybe a few extra for good measure), and shows what models are available in the SasModels library. One way to make sure all required Python modules are installed, is by running:\n", + "This section imports the necessary packages and shows what models are available in the SasModels library. In a clean environment, install the runtime dependencies first, for example with:\n", "\n", - " pip install -r ../requirements.txt" + " pip install mcsas3 sasmodels" ] }, { "cell_type": "code", "execution_count": 1, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-31T19:57:37.624230Z", + "iopub.status.busy": "2026-03-31T19:57:37.624131Z", + "iopub.status.idle": "2026-03-31T19:57:38.896605Z", + "shell.execute_reply": "2026-03-31T19:57:38.896109Z" + } + }, "outputs": [], "source": [ "# import all the necessary bits and bobs\n", "\n", - "import h5py, sys, os\n", + "import os\n", "import numpy as np\n", "import pandas\n", "# import scipy\n", "# import multiprocessing\n", "from pathlib import Path\n", "\n", - "# load required modules\n", - "homedir = os.path.expanduser(\"~\")\n", "# disable OpenCL for multiprocessing on CPU\n", "os.environ[\"SAS_OPENCL\"] = \"none\"\n", "\n", - "# CHANGE to location where the SasView/sasmodels are installed\n", - "#sasviewPath = os.path.join(homedir, \"Code\", \"sasmodels\") # <-- change! \n", - "#if sasviewPath not in sys.path:\n", - "# sys.path.append(sasviewPath)\n", - "# import from this path\n", "import sasmodels\n", "import sasmodels.core\n", "import sasmodels.direct_model\n", "\n", - "# CHANGE this one to whereever you have mcsas3 installed:\n", - "# (if its not in the parent directory)\n", - "mcsasPath = os.path.join(\"..\")\n", - "if mcsasPath not in sys.path:\n", - " sys.path.append(mcsasPath)\n", - "\n", - "# import from this path:\n", + "from mcsas3 import (\n", + " STAGE_BINNED,\n", + " STAGE_CLIPPED,\n", + " STAGE_RAW,\n", + " optimize_processing_data,\n", + " prepare_1d_processing_data_from_file,\n", + " selected_bundle_from_processing,\n", + " store_result_processing_data,\n", + ")\n", "from mcsas3.mc_hat import McHat\n", - "from mcsas3.mc_data_1d import McData1D\n", "from mcsas3.mc_analysis import McAnalysis\n", "# optimizeScalingAndBackground: takes care of the calculation of the reduced chi-squared value, after a least-squares optimization for the scaling and background factors.\n", "# McModel: extends the SasModel with information on the parameter set and methods for calculating a total scattering intensity from multiple contributions. It also tracks parameter bounds, random generators and picks.\n", @@ -137,7 +137,14 @@ { "cell_type": "code", "execution_count": 2, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-31T19:57:38.898603Z", + "iopub.status.busy": "2026-03-31T19:57:38.898406Z", + "iopub.status.idle": "2026-03-31T19:57:38.900986Z", + "shell.execute_reply": "2026-03-31T19:57:38.900510Z" + } + }, "outputs": [], "source": [ "# set a filename for documenting the fit:\n", @@ -152,25 +159,31 @@ "source": [ "Data loading:\n", "--\n", - "We have a class for this, which loads the data, clips and rebins the data as requested. We always recommend rebinning the data to about 100 datapoints per decade in Q as a rule of thumb, to speed up the fitting itself. " + "We now use the canonical workflow helpers, which ingest the data, normalize units, clip, and rebin as requested. We always recommend rebinning the data to about 100 datapoints per decade in Q as a rule of thumb, to speed up the fitting itself. " ] }, { "cell_type": "code", "execution_count": 3, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-31T19:57:38.902549Z", + "iopub.status.busy": "2026-03-31T19:57:38.902431Z", + "iopub.status.idle": "2026-03-31T19:57:39.192389Z", + "shell.execute_reply": "2026-03-31T19:57:39.191889Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "storing in ../testdata/quickstartdemo1_fitResult.h5 at /analyses/MCResult1/mcdata\n", - "data fed to McSAS3 is binnedData\n" + "selected analysis stage is sample_binned\n" ] }, { "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "
" ] @@ -181,37 +194,47 @@ ], "source": [ "# measurement data:\n", - "mds = McData1D(\n", - " filename=Path(\"..\", \"testdata\", \"quickstartdemo1.csv\"),\n", + "quickstart_path = Path(\"..\", \"testdata\", \"quickstartdemo1.csv\")\n", + "processing = prepare_1d_processing_data_from_file(\n", + " quickstart_path,\n", " nbins=0, # no rebinning in this example\n", - " dataRange = [0.01, 1], # this clips the data to the specified range\n", + " dataRange=[0.01, 1], # this clips the data to the specified range\n", "\n", " # arguments for pandas.read_csv:\n", - " csvargs = {\"sep\" : ';', # field delimiter, for flexible whitespace, use: \"\\s+|\\t+|\\s+\\t+|\\t+\\s+\" (https://stackoverflow.com/questions/15026698/how-to-make-separator-in-pandas-read-csv-more-flexible-wrt-whitespace-for-irreg#15026839)\n", - " \"skipinitialspace\" : True, # ignore initial blank spaces\n", - " \"skip_blank_lines\" : True, # ignore lines with nothing in them\n", - " \"skiprows\" : 0, # skip this many rows before reading data (useful for PDH, which I think has five (?) header rows?)\n", - " \"engine\": \"python\", # most flexible\n", - " \"header\" : None, # let's not read any column names since they're unlikely to match with our expected column names:\n", - " \"names\": [\"Q\", \"I\", \"ISigma\"], # our expected column names\n", - " \"index_col\" : False}, # no index column before every row (who does this anyway?)\n", + " csvargs={\"sep\": ';', # field delimiter, for flexible whitespace, use: \"\\s+|\\t+|\\s+\\t+|\\t+\\s+\" (https://stackoverflow.com/questions/15026698/how-to-make-separator-in-pandas-read-csv-more-flexible-wrt-whitespace-for-irreg#15026839)\n", + " \"skipinitialspace\": True, # ignore initial blank spaces\n", + " \"skip_blank_lines\": True, # ignore lines with nothing in them\n", + " \"skiprows\": 0, # skip this many rows before reading data (useful for PDH, which I think has five (?) header rows?)\n", + " \"engine\": \"python\", # most flexible\n", + " \"header\": None, # let's not read any column names since they're unlikely to match with our expected column names:\n", + " \"names\": [\"Q\", \"I\", \"ISigma\"], # our expected column names\n", + " \"index_col\": False}, # no index column before every row (who does this anyway?)\n", ")\n", "\n", - "# store the data and all derivatives in the output file:\n", - "mds.store(resPath)\n", + "# store the canonical processing data in the output file:\n", + "store_result_processing_data(resPath, processing, metadata={\"filename\": quickstart_path})\n", + "\n", + "def plot_1d_bundle(bundle, ax, label):\n", + " q = bundle[\"Q\"].signal\n", + " intensity = bundle[\"signal\"].signal\n", + " sigma = bundle[\"signal\"].uncertainties[\"propagate_to_all\"]\n", + " ax.errorbar(q, intensity, yerr=sigma, fmt='.', label=label)\n", "\n", "# plot the loaded data\n", "fhs, ahs = plt.subplots(nrows = 1, ncols = 1, figsize = [6, 4], dpi=100)\n", - "mds.rawData.plot('Q', 'I', yerr= 'ISigma', ax = ahs, label = 'As provided data')\n", - "mds.clippedData.plot('Q', 'I', yerr= 'ISigma', ax = ahs, label = 'clipped data')\n", - "mds.binnedData.plot('Q', 'I', yerr= 'ISigma', ax = ahs, label = 'binned data')\n", + "plot_1d_bundle(processing[STAGE_RAW], ahs, 'As provided data')\n", + "plot_1d_bundle(processing[STAGE_CLIPPED], ahs, 'clipped data')\n", + "plot_1d_bundle(processing[STAGE_BINNED], ahs, 'binned data')\n", "plt.yscale('log')\n", "plt.xscale('log')\n", "plt.xlabel('Q (1/nm)')\n", "plt.ylabel('I (1/(m sr))')\n", "# plt.xlim([0.1, 3])\n", - "print(f'data fed to McSAS3 is {mds.measDataLink}')\n", - "md = mds.measData.copy() # here we copy the data we want for fitting." + "print(f'selected analysis stage is {processing.analysis_stage}')\n", + "selected_bundle = selected_bundle_from_processing(processing)\n", + "selected_q = selected_bundle[\"Q\"].signal\n", + "selected_i = selected_bundle[\"signal\"].signal\n", + "selected_sigma = selected_bundle[\"signal\"].uncertainties[\"propagate_to_all\"]" ] }, { @@ -226,122 +249,40 @@ { "cell_type": "code", "execution_count": 4, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-31T19:57:39.193988Z", + "iopub.status.busy": "2026-03-31T19:57:39.193867Z", + "iopub.status.idle": "2026-03-31T19:57:39.219877Z", + "shell.execute_reply": "2026-03-31T19:57:39.219461Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "\n", - " \n", - " 1D-only SasModel Models:\n", - "\n", - " -- \n", - "\n", - " \t * adsorbed_layer\n", - " \t * be_polyelectrolyte\n", - " \t * binary_hard_sphere\n", - " \t * broad_peak\n", - " \t * core_multi_shell\n", - " \t * core_shell_sphere\n", - " \t * correlation_length\n", - " \t * dab\n", - " \t * flexible_cylinder\n", - " \t * flexible_cylinder_elliptical\n", - " \t * fractal\n", - " \t * fractal_core_shell\n", - " \t * fuzzy_sphere\n", - " \t * gauss_lorentz_gel\n", - " \t * gaussian_peak\n", - " \t * gel_fit\n", - " \t * guinier\n", - " \t * guinier_porod\n", - " \t * hardsphere\n", - " \t * hayter_msa\n", - " \t * hollow_rectangular_prism_thin_walls\n", - " \t * lamellar\n", - " \t * lamellar_hg\n", - " \t * lamellar_hg_stack_caille\n", - " \t * lamellar_stack_caille\n", - " \t * lamellar_stack_paracrystal\n", - " \t * line\n", - " \t * linear_pearls\n", - " \t * lorentz\n", - " \t * mass_fractal\n", - " \t * mass_surface_fractal\n", - " \t * micromagnetic_FF_3D\n", - " \t * mono_gauss_coil\n", - " \t * multilayer_vesicle\n", - " \t * onion\n", - " \t * peak_lorentz\n", - " \t * pearl_necklace\n", - " \t * poly_gauss_coil\n", - " \t * polymer_excl_volume\n", - " \t * polymer_micelle\n", - " \t * porod\n", - " \t * power_law\n", - " \t * pringle\n", - " \t * raspberry\n", - " \t * rpa\n", - " \t * sphere\n", - " \t * spherical_sld\n", - " \t * spinodal\n", - " \t * squarewell\n", - " \t * star_polymer\n", - " \t * stickyhardsphere\n", - " \t * surface_fractal\n", - " \t * teubner_strey\n", - " \t * two_lorentzian\n", - " \t * two_power_law\n", - " \t * unified_power_Rg\n", - " \t * vesicle\n", - "\n", - " \n", - " 2D- and 1D- SasModel Models:\n", - "\n", - " -- \n", - "\n", - " \t * barbell\n", - " \t * bcc_paracrystal\n", - " \t * capped_cylinder\n", - " \t * core_shell_bicelle\n", - " \t * core_shell_bicelle_elliptical\n", - " \t * core_shell_bicelle_elliptical_belt_rough\n", - " \t * core_shell_cylinder\n", - " \t * core_shell_ellipsoid\n", - " \t * core_shell_parallelepiped\n", - " \t * cylinder\n", - " \t * ellipsoid\n", - " \t * elliptical_cylinder\n", - " \t * fcc_paracrystal\n", - " \t * hollow_cylinder\n", - " \t * hollow_rectangular_prism\n", - " \t * parallelepiped\n", - " \t * rectangular_prism\n", - " \t * sc_paracrystal\n", - " \t * stacked_disks\n", - " \t * superball\n", - " \t * triaxial_ellipsoid\n" + "SasModels inventory loaded successfully.\n", + "Sphere available as a 1D-only model: True\n", + "Cylinder available as a 2D-capable model: True\n" ] } ], "source": [ - "# show me all the available models, 1D and 1D+2D\n", - "\n", - "print(\"\\n \\n 1D-only SasModel Models:\\n\")\n", - "print(\" -- \\n\")\n", - "\n", - "for model in sasmodels.core.list_models():\n", - " modelInfo = sasmodels.core.load_model_info(model)\n", - " if not modelInfo.parameters.has_2d:\n", - " print(f\" \\t * {modelInfo.id}\")\n", - "\n", - "print(\"\\n \\n 2D- and 1D- SasModel Models:\\n\")\n", - "print(\" -- \\n\")\n", - "for model in sasmodels.core.list_models():\n", - " modelInfo = sasmodels.core.load_model_info(model)\n", - " if modelInfo.parameters.has_2d:\n", - " print(f\" \\t * {modelInfo.id}\")" + "# sanity-check the installed SasModels inventory without depending on the full upstream list\n", + "\n", + "models_1d = []\n", + "models_2d = []\n", + "for model in sorted(sasmodels.core.list_models()):\n", + " model_info = sasmodels.core.load_model_info(model)\n", + " if model_info.parameters.has_2d:\n", + " models_2d.append(model_info.id)\n", + " else:\n", + " models_1d.append(model_info.id)\n", + "\n", + "print(\"SasModels inventory loaded successfully.\")\n", + "print(f\"Sphere available as a 1D-only model: {'sphere' in models_1d}\")\n", + "print(f\"Cylinder available as a 2D-capable model: {'cylinder' in models_2d}\")" ] }, { @@ -356,7 +297,14 @@ { "cell_type": "code", "execution_count": 5, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-31T19:57:39.221280Z", + "iopub.status.busy": "2026-03-31T19:57:39.221178Z", + "iopub.status.idle": "2026-03-31T19:57:39.224029Z", + "shell.execute_reply": "2026-03-31T19:57:39.223600Z" + } + }, "outputs": [ { "name": "stdout", @@ -455,7 +403,14 @@ { "cell_type": "code", "execution_count": 6, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-31T19:57:39.225450Z", + "iopub.status.busy": "2026-03-31T19:57:39.225330Z", + "iopub.status.idle": "2026-03-31T19:57:39.230020Z", + "shell.execute_reply": "2026-03-31T19:57:39.229522Z" + } + }, "outputs": [], "source": [ "# configures the model the Monte Carlo optimizer:\n", @@ -484,6 +439,12 @@ "cell_type": "code", "execution_count": 7, "metadata": { + "execution": { + "iopub.execute_input": "2026-03-31T19:57:39.231321Z", + "iopub.status.busy": "2026-03-31T19:57:39.231229Z", + "iopub.status.idle": "2026-03-31T19:57:44.765465Z", + "shell.execute_reply": "2026-03-31T19:57:44.764824Z" + }, "nbreg": { "diff_ignore": [ "/outputs" @@ -493,111 +454,13 @@ }, "outputs": [ { - "name": "stdin", - "output_type": "stream", - "text": [ - "This will run the optimization which can take a few minutes. Are you sure? Y/N Y\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "McSAS analysis with 10 repetitions took 13.5s with 10 threads.\n", - "Optimization of repetition 0 started:\n", - "chiSqr: 4687.677773627522, N accepted: 0 / 0\n", - "chiSqr: 4687.677773627522, N accepted: 0 / 1\n", - "chiSqr: 127.19374283454604, N accepted: 357 / 1001\n", - "chiSqr: 14.379660290820748, N accepted: 528 / 2001\n", - "chiSqr: 3.6506228242696466, N accepted: 600 / 3001\n", - "chiSqr: 1.634707334933592, N accepted: 640 / 4001\n", - "Final chiSqr: 0.8762147088107574, N accepted: 654\n", - "\n", - "Optimization of repetition 1 started:\n", - "chiSqr: 23580.73454347122, N accepted: 0 / 0\n", - "chiSqr: 23312.460561438576, N accepted: 1 / 1\n", - "chiSqr: 137.01541620002726, N accepted: 383 / 1001\n", - "chiSqr: 14.61512809240039, N accepted: 543 / 2001\n", - "chiSqr: 2.491338225694021, N accepted: 631 / 3001\n", - "chiSqr: 1.1045035012102553, N accepted: 662 / 4001\n", - "Final chiSqr: 0.9697502940645237, N accepted: 671\n", - "\n", - "Optimization of repetition 2 started:\n", - "chiSqr: 11962.531116114129, N accepted: 0 / 0\n", - "chiSqr: 11930.192157263367, N accepted: 1 / 1\n", - "chiSqr: 135.74152114861008, N accepted: 365 / 1001\n", - "chiSqr: 11.445116069776486, N accepted: 530 / 2001\n", - "chiSqr: 2.1070886048582476, N accepted: 602 / 3001\n", - "chiSqr: 1.3723279042023635, N accepted: 632 / 4001\n", - "Final chiSqr: 0.9995439429604502, N accepted: 656\n", - "\n", - "Optimization of repetition 3 started:\n", - "chiSqr: 19646.459230836826, N accepted: 0 / 0\n", - "chiSqr: 19646.459230836826, N accepted: 0 / 1\n", - "chiSqr: 107.93219585834112, N accepted: 381 / 1001\n", - "chiSqr: 11.051688325933599, N accepted: 527 / 2001\n", - "chiSqr: 2.1677651004350724, N accepted: 592 / 3001\n", - "Final chiSqr: 0.9262693796097495, N accepted: 624\n", - "\n", - "Optimization of repetition 4 started:\n", - "chiSqr: 18478.40081658848, N accepted: 0 / 0\n", - "chiSqr: 18294.16101638919, N accepted: 1 / 1\n", - "chiSqr: 120.12196497814182, N accepted: 360 / 1001\n", - "chiSqr: 11.233726870933094, N accepted: 516 / 2001\n", - "chiSqr: 1.7610612235209862, N accepted: 586 / 3001\n", - "chiSqr: 1.114662343534314, N accepted: 610 / 4001\n", - "Final chiSqr: 0.9878045456345123, N accepted: 614\n", - "\n", - "Optimization of repetition 5 started:\n", - "chiSqr: 15578.985164253978, N accepted: 0 / 0\n", - "chiSqr: 15578.985164253978, N accepted: 0 / 1\n", - "chiSqr: 169.47294258627232, N accepted: 326 / 1001\n", - "chiSqr: 15.196674105615271, N accepted: 503 / 2001\n", - "chiSqr: 3.8085549070231712, N accepted: 580 / 3001\n", - "chiSqr: 1.9305125871363413, N accepted: 620 / 4001\n", - "chiSqr: 1.1963389568537781, N accepted: 639 / 5001\n", - "chiSqr: 1.0151358264178936, N accepted: 658 / 6001\n", - "Final chiSqr: 0.9947470945856858, N accepted: 660\n", - "\n", - "Optimization of repetition 6 started:\n", - "chiSqr: 4073.9104572450156, N accepted: 0 / 0\n", - "chiSqr: 4073.9104572450156, N accepted: 0 / 1\n", - "chiSqr: 79.31078252722229, N accepted: 352 / 1001\n", - "chiSqr: 7.524405693422794, N accepted: 495 / 2001\n", - "chiSqr: 2.6104999934646407, N accepted: 545 / 3001\n", - "chiSqr: 1.188307277369438, N accepted: 584 / 4001\n", - "Final chiSqr: 0.8542928156521242, N accepted: 591\n", - "\n", - "Optimization of repetition 7 started:\n", - "chiSqr: 16244.759350969192, N accepted: 0 / 0\n", - "chiSqr: 16244.759350969192, N accepted: 0 / 1\n", - "chiSqr: 126.18767194780125, N accepted: 366 / 1001\n", - "chiSqr: 12.76229757957951, N accepted: 524 / 2001\n", - "chiSqr: 2.824146263499322, N accepted: 594 / 3001\n", - "chiSqr: 1.5929933180797655, N accepted: 626 / 4001\n", - "Final chiSqr: 0.9986577449313511, N accepted: 652\n", - "\n", - "Optimization of repetition 8 started:\n", - "chiSqr: 24286.481424869384, N accepted: 0 / 0\n", - "chiSqr: 24222.877309195534, N accepted: 1 / 1\n", - "chiSqr: 103.01693777582258, N accepted: 343 / 1001\n", - "chiSqr: 10.9303663450171, N accepted: 494 / 2001\n", - "chiSqr: 2.2903782991483106, N accepted: 561 / 3001\n", - "chiSqr: 1.1747943691421834, N accepted: 600 / 4001\n", - "Final chiSqr: 0.9940054247173277, N accepted: 615\n", - "\n", - "Optimization of repetition 9 started:\n", - "chiSqr: 13507.912100830796, N accepted: 0 / 0\n", - "chiSqr: 13507.912100830796, N accepted: 0 / 1\n", - "chiSqr: 160.78577103048016, N accepted: 337 / 1001\n", - "chiSqr: 17.347004070862667, N accepted: 522 / 2001\n", - "chiSqr: 3.1022541432888864, N accepted: 598 / 3001\n", - "chiSqr: 1.7690943554480634, N accepted: 629 / 4001\n", - "chiSqr: 1.409729681876782, N accepted: 648 / 5001\n", - "chiSqr: 1.0403335560809301, N accepted: 665 / 6001\n", - "Final chiSqr: 0.9944990238847728, N accepted: 670\n", - "\n" - ] + "data": { + "text/plain": [ + "'This will run the optimization which can take a few minutes. Are you sure? Y/N Y'" + ] + }, + "metadata": {}, + "output_type": "display_data" } ], "source": [ @@ -608,7 +471,7 @@ "else:\n", " ans = input(question)\n", "if ans.lower() == 'y':\n", - " mh.run(md, resPath)" + " optimize_processing_data(processing, resPath, hat=mh, store_processing=False)" ] }, { @@ -624,6 +487,12 @@ "cell_type": "code", "execution_count": 8, "metadata": { + "execution": { + "iopub.execute_input": "2026-03-31T19:57:44.767336Z", + "iopub.status.busy": "2026-03-31T19:57:44.767195Z", + "iopub.status.idle": "2026-03-31T19:57:45.443316Z", + "shell.execute_reply": "2026-03-31T19:57:45.442638Z" + }, "nbreg": { "diff_replace": [ [ @@ -635,22 +504,7 @@ }, "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Getting List of repetitions...\n", - "10 repetitions found in McSAS file ../testdata/quickstartdemo1_fitResult.h5\n", - "Histogramming every repetition and extracting elements to average...\n", - "Averaging population modes...\n", - "Averaging histograms...\n", - "Averaging optimization parameters...\n", - "Averaging model intensity...\n", - "Storing averages...\n" - ] - } - ], + "outputs": [], "source": [ "# histogram the result, This doesn't take so long and can be repeated as required.\n", "histRanges = pandas.DataFrame(\n", @@ -693,17 +547,24 @@ " ]\n", ")\n", "\n", - "mcres = McAnalysis(resPath, md, histRanges, store=True)" + "mcres = McAnalysis(resPath, processing, histRanges, store=True)" ] }, { "cell_type": "code", "execution_count": 9, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-31T19:57:45.445470Z", + "iopub.status.busy": "2026-03-31T19:57:45.445281Z", + "iopub.status.idle": "2026-03-31T19:57:46.609756Z", + "shell.execute_reply": "2026-03-31T19:57:46.609310Z" + } + }, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAC64AAAHpCAYAAAA4B4DKAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/av/WaAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdeXhM1+M/8PdkD7GlpdTS1lqliLaqfFXRUpSq8ilFLREkSGKJBCEhlkY2SUUQldTWKhpSa2wJpbFTS+zUEorIIttMMnN+f+SXK9dkmUkmifB+PY/ncZc599w7d+a+c+fccxRCCAEiIiIiIiIiIiIiIiIiIiIiIiIiIiIiolJiVN4VICIiIiIiIiIiIiIiIiIiIiIiIiIiIqKXGxuuExEREREREREREREREREREREREREREVGpYsN1IiIiIiIiIiIiIiIiIiIiIiIiIiIiIipVbLhORERERERERERERERERERERERERERERKWKDdeJiIiIiIiIiIiIiIiIiIiIiIiIiIiIqFSx4ToRERERERERERERERERERERERERERERlSo2XCciIiIiIiIiIiIiIiIiIiIiIiIiIiKiUsWG60RERERERERERERERERERERERERERERUqthwnYiIiIiIiIiIiIiIiIiIiIiIiIiIiIhKFRuuExEREREREREREREREREREREREREREVGpYsN1IiIiIiIiIiIiIiIiIiIiIiIiIiIiIipVbLhORERERERERERERERERERERERERERERKWKDdeJiIiIiIiIiIiIiIiIiIiIiIiIiIiIqFSx4ToRERERERERERERERERERERERERERERlSo2XCciIiIiIiIiIiIiIiIiIiIiIiIiIiKiUsWG60RERERERERERERERERERERERERERERUqthwnYiIiIiIiIiIiIiIiIiIiIiIiIiIiIhKFRuuExEREREREREREREREREREREREREREVGpYsN1IiIiIiIiIiIiIiIiIiIiIiIiIiIiIipVbLhORERERERERERERERERERERERERERERKWKDdeJiIiIiIiIiIiIiIiIiIiIiIiIiIiIqFSx4ToRERERERERERERERERERERERERERERlSo2XCciIiIiIiIiIiIiIiIiIiIiIiIiIiKiUsWG60RERERERERERERERERERERERERERERU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q7XM1tYWjx8/xqZNm2S9wB06dAgajUY6V5OSknDjxg28/vrrAIDs7GwEBARgypQp0rm2ZMkS6WkwMzMz1KhRA/v27UO3bt2kco8fP4569erp/DTwl19+id9++016ujE8PFw614mIXlQVaXSaL7/8El9++SU2btyIX3/9FUOHDjVY2deuXYNKpUKLFi3yXV7U6DT379/HokWLIITAxYsXsWfPHtSsWRNmZmZYsGABTExMdBqdxlCjdxR3dJq8fvjhBwwfPjzfZb/++iv++usvmJqaQgiBe/fuScv27t2LIUOGwNLSUprn4uKCH374oVj7UpDPP/8cCxYswOHDh2FtbY127doVaxvvvPMOQkJCcPjwYdja2uK7775Dr169DFpXABgyZAgGDx6MZcuWYe/evVLPFIZw6NAhvPvuu3jjjTfyXb5z505cuHBBNk+pVOLcuXNo2rQpLly4gNDQUABAbGws/vvvP5ibm+O1117DrFmzAOg3utLmzZtx9epVWc8shtKrVy9s374dffr0QWZmJp4+fSrtd3p6OiwtLdGkSRNp/Vq1aqFt27a4du2abH5BHj58iO7du2vN79KlCwICAgDk9NqRkZGB1atX4+LFi+jVqxceP36MmjVrAtBtNKuiPH36FC1btpSm+/fvj40bN8rWady4MTp27ChNN2rUCA8ePEDt2rV12kZR58XmzZsxadIk6e9DhUKBqVOnwtvbW6fyiV51zFg5mLG0MWMZ1r179zBnzhwsXLgQlSpV0uu11apVk033798fhw4dknole/68AeTnTllnrNLEjPUMMxbRi6si5avk5GS4urpi6tSpOn9/5Cps9Fig5CMsGypfGQrzle7KKl8BwKZNm4r1dwHz1TPMV88wXxG9uCpSvgKAP//8E3369NH7dcxXzFcFKct8VVwVLV/xN0LmK6q42HDdgPz8/KRhD0xMTDBnzhxZIGnRogV69eqFcePGSUPbdO/eXWpEkpGRgdmzZ+Ps2bP5BgtDiYqKwo4dO3D37l0olUpER0ejS5cuWkOW/fvvvzhz5gwePHigVcb48eMxffp0rF+/HmlpabCzs5P9WFOnTh18+umnsLW1hYWFBerUqYMRI0ZIy9u3b4+0tDSMGTMGVlZW0hf6lClTdN4PCwsL2NraYuTIkbCysoKlpSUWLlwoLd+xYwe2bduGdu3a4eTJkwBy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HUNYWJhs9NjnO+Ip7NoBlE2+0vVeRWliviodS5YswTfffIO6devmu5z5SnfMV8xXRC+yipKvcm3ZsgW1a9dG+/bt813OfGUYzFeG9dtvvyE2NhYXL16UPifffvstOnXqJFuvoucr/kbIfEUvBzZcJyIiIiIiopeORqNBrVq18Pvvvxerp6tXnbOzs3RTioiIiCgXM1bJMGMRERHR85ivSob5ioiIiJ7HfFUyzFdUFozKuwJEREREREREhnb9+nV8+umnvCFVDAkJCTAzMyvvahAREdELiBmr+JixiIiIKD/MV8XHfEVERET5Yb4qPuYrKivscZ2IiIiIiIjoFff3339jw4YNMDIyglKphJeXlzS8HxEREREVDzMWERERkWExXxEREREZFvMVlQc2XCciIiIiIiIiIiIiIiIiIiIiIiIiIiKiUmVU3hUgIiIiIiIiIiIiIiIiIiIiIiIiIiIiopcbG64TERERERERERERERERERERERERERERUakyKe8KvEzGjRuH2NhYrfmHDh3CoUOHMGPGDK1lLi4uGDJkCADgww8/RHZ2tmx5s2bNsGHDBgDA3Llz8ccff2iVsWHDBjRr1gxnzpzBiBEjtJYPGTIELi4uAIC+ffvi9u3bsuVVqlTBoUOHAAArV67EkiVLtMoICAhAly5d8PDhQ3Tv3l1reZcuXRAQEFDkcahSpQp27NjBY8FjwWPBY8FjoeexICIiIiIiIiIiIiIiIiIiIiIiIqrIFEIIUd6VICIiIiIiIiIiIiIiIiIiIiIiIiIiIqKXF3tcJyIiIqIywxFqOOIEjwWPBY8Fj0V+x4KIio/56tX6vuSx4LHgseCxYL4iKn3MV6/W9yWPBY8FjwWPBfMVUdlgxnq1vjN5LHgseCx4LJixqDDscZ2IiIiIiIiIiIiIiIiIiIiIiIiIiIiISpVReVeAiIiIiIiIiIiIiIiIiIiIiIiIiIiIiF5ubLhORERERERERERERERERERERERERERERKWKDdeJiIiIiIiIiIiIiIiIiIiIiIiIiIiIqFSx4ToRERERERERERERERERERERERERERERlSo2XCcyACEEjh49CpVKVd5VKZRarcaRI0fKuxqUR3x8PK5du1be1SAiIiIiIiIiIiIiIiIiIiIiIiIiKlUm5V0B0p+rqyvS0tJgYmKCxYsXF7je22+/jVu3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jB49mo2qiIiICADjK0NgfEXGwBHXi5Fz587Bx8cH5cqVQ7169TBr1qzCLhIRERERERERERERERERERERERERERFRrthwnYiIiIiIiIiIiIiIiIiIiIiIiIiIiIiMSl7YBSAiIiIiIiIiIiIiIiIiIiIiIiIiIiKidxsbrhMRERERERERERERERERERERERERERGRUbHhOhEREREREREREREREREREREREREREREZFRuuExEREREREREREREREREREREREREREZFRseE6ERERERERERERERERERERERERERERERkVG64TERERERERERERERERERERERERERERkVGx4ToRERERERERERERERERERERERERERERGRUbrhMRERERERERERERERERERERERERERGRUbHhOhEREREREREREREREREREREREREREREZFRuuExEREREREREREREREREREREREREREZFRseE6ERERERERERERERERERERERERERERERkVG64TERERERERERERERERERERERERERERkVGx4ToRERERERERERERERERERERERERERERGRUbrhMRERERERERERERERERERERERERERGRUbHhOhEREREREREREREREREREREREREREREZFRuuExEREREREREREREREREREREREREREZFRseE6ERERERERERERERERERERERERERERERkVG64TERERERERERERERERERERERERERERkVGx4ToRERERERERERERERERERERERERERERGRUbrhMRERERERERERERERERERERERERERGRUbHhOhEREREREREREREREREREREREREREREZFRuuExEREREREREREREREREREREREREREZFRseE6ERERERERERERERERERERERERERERERkVG64TERERERERERERERERERERERERERERkVGx4ToRERERERERERERERERERERERERERERGRUbrhMRERERERERERERERERERERERERERGRUbHhOhEREREREREREREREREREREREREREREZlWlhF4CIiIiI8i8oKAiHDx9GXFwcqlWrhtGjR8PZ2Tnb9BcvXsTevXsRHR2NihUrYtiwYWjWrJm4fd++fbhw4QJev34NU1NT1KhRA0OGDEGtWrXENG5uboiOjpbkO3ToUPTt29fgx0dERERERERERERERERERERERMWbTBAEobALQURERER5d+HCBaxduxaurq6oVasWjh49ikuXLsHf3x8lS5bUSn///n3MmzcPQ4cORbNmzfD333/j0KFD8PPzQ9WqVQEAf//9N+zs7FChQgWoVCocPXoUFy9exJo1a2BnZwfgTcP1Dh06oHPnzmLelpaWsLS0LJgDJyIiIiIiIiIiIiIiIiIiIiKiYkNe2AUo6oKCgjB16lTs2LGjsItCRERElKUjR46gU6dO6NChAxwdHeHq6gpzc3OcPn06y/THjh1DkyZN0Lt3bzg6OmLIkCGoUaMGgoKCxDRt2rRBo0aNUKFCBVSpUgVfffUVUlJS8PjxY0leVlZWsLe3F/9jo3UiIiIiIiIiIiIiIiIiIiIiIsqKaWEXoKjr2rUrunbtKi7HxsZCo9EYfD/lypVDdHS0wfMlovzhd5OoaDLUd9PU1BSlSpUyQIkKj0ajQVhYGPr27Suuk8vlaNiwIUJCQrL8TEhICHr27ClZ17hxY1y+fDnbfZw6dQrW1taoVq2aZNvBgwfx22+/oWzZsmjTpg169OgBExOTLPNRq9VQq9Xiskwmg5WVldHiK3o3yGQylC1bFq9evQIniyIqfPxOUm7ehfjqXfA+xlf8/V708RoVfbxGRR+vUdFmrOvD+KroeB9jrJzwb1LB4bkuWDzfBYfnumDxfEsxxioa3sX46n38rvGY3w885vcDj/n9UBTqsNhwXU8ajUbS4MoQZDKZmDdf/BMVHfxuEhVN/G5KJSQkID09Hfb29pL19vb2iIyMzPIzcXFxKFmypGRdyZIlERcXJ1n333//wd/fHyqVCvb29pg9ezbs7OzE7d26dUP16tVha2uL+/fvY/fu3YiNjcWIESOy3G9AQAB+/fVXcbl69erw8/NjxSDppGzZsoVdBCLKhN9JoqLNGPVXRRl/IxR9vEZFH69R0cdrVLTx+rwf3rcYKye85wsOz3XB4vkuODzXBYvnm4qqdy2+eh+/azxmHvO7isfMY35XFZVjZsN1IiIiIspS/fr1sXTpUiQkJOCPP/7AypUr4evrKzZ6zzxqe7Vq1WBqaopNmzZh6NChMDMz08qvX79+ks9kBMTR0dHv3GgKZDgymQwVK1bE8+fP35sfi0RFGb+TlBtTU1OUK1eusItBREREREREREREREREREUQG64TERERFWN2dnaQy+Vao6XHxcVpjcKewd7eHvHx8ZJ18fHxWuktLS1RsWJFVKxYEbVr18bkyZPx559/ol+/flnmW6tWLaSlpSE6OhqVKlXS2m5mZpZlg3YAbPxIuRIEgfcJURHC7yQREREREREREREREREREelLXtgFKOqCgoIwdepULF++vLCLQkRERKTF1NQUNWrUwK1bt8R16enpuHXrFmrXrp3lZ2rXro2bN29K1t24cQO1atXKcV+CIOQ4XV94eDhkMhns7Oz0OAIiIiIiIiIiIiIiIiIiIiIiInofcMT1XHTt2hVdu3Yt7GIQERERZatnz55Yt24datSoAWdnZxw7dgxKpRKffPIJAGDt2rUoXbo0hg4dCgDo3r07vL29cfjwYTRr1gznz5/Hw4cPMW7cOABAamoqDhw4gA8//BClSpVCYmIigoKCEBMTg1atWgEAQkJC8ODBA9SvXx9WVlYICQnB9u3b0bZtW9ja2hbKeSAiIiIiIiIiIiIiIiIiIiIioqKLDdeJiKjQaTQaJCcnF3YxqBhJSUmBSqXSKa21tTVMTd/tkKd169ZISEjAvn37EBcXBycnJ8ycORP29vYAgFevXkEmk4np69Spg8mTJ2PPnj3YvXs3HBwcMH36dFStWhUAIJfLERkZieXLlyMxMRElSpRAzZo1MX/+fFSpUgXAm5HeL1y4gP3790OtVqN8+fLo0aMHevbsWeDHT0RERERERERERERERERERERERd+73YqLiIiKPI1GA4VCgRIlSkAulxd2caiYMDMzg1qtzjVdeno6EhMTYWNj8843Xs9plhhvb2+tda1atRJHT3+bubk5pk2bluP+atSogUWLFuldTiIiIiIiIiIiIiIiIiIiIiIiej+xhSARERWq5ORkNlono5HL5ShRogRH9CciIiIiIiIiIiIiIiIiIiIiIipk7/bQo8VAuiCw9wARvffYaJ2MifcXEREREREREREREREREb2LgoKCcPjwYcTFxaFatWoYPXo0nJ2ds01/8eJF7N27F9HR0ahYsSKGDRuGZs2aidv/+ecfnDx5EmFhYUhKSsKSJUvg5OSklU9ISAh2796N0NBQyOVyODk5YdasWTA3NzfGYRIREdE7hA3XcxEUFITff/8djo6O8PT0NHj+5/78F6sjbVAiLRh2ggoloEYJk3SU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W3jME5kXHpNERERERERERERERERERGRIHHFdj3755RetIfdbXT/NQHZ2Nnbt2gWlUonAwEDcf//9CA4ONnaoRDaFxyaR5eDxSmRbeMwTmRcek0REREREREREREREREREZEjsuE5EREREREREREREREREREREREREREREBiU2dQBEREREREREREREREREREREREREREREZN3YcZ2IiIiIiIiIiIiIiIiIiIiIiIiIiIiIDIod14mIiIiIiIiIiIiIiIiIiIiIiIiIiIjIoNhxnYiIiIiIiIiIiIiIiIiIiIiIiIiIiIgMih3XiYiIiIiIiIiIiIj0ICkpCZs2bTL681ZVVeHuu+/Gr7/+avTnVqlUeOihh7B//36jPzcRERFZP+ZXRERERPrHHIuITEli6gCIiIiIiIiIiIiIiMzZ2bNn8fHHH+PUqVOora2Fq6sr/Pz8MGLECNx+++2mDg/btm2DQqFAaGio0Z9bIpFgxowZ2L59OyZPngwHBwejx0BERESWh/lVx5hfERERUU8xx+oYcywi88ER14mIiIiIiIiIiIiIOnDixAn861//wpkzZzB16lTEx8dj6tSpEIvFyMrK0iq7fv16PPDAA0aNr66uDl999RWmTZtm1Oe93uTJk3Hp0iXk5OSYLAYiIiKyHMyvusb8ioiIiLqLOVbXmGMRmQeOuE5ERERERERERERE1IHt27fDxcUFL730Evr06aO1rra2Vuuxvb29MUMDAHz99dews7PDiBEjjP7crfr06YOwsDB89dVXmDJlisniICIiIsvA/KprzK+IiIiou5hjdY05FpF5YMd1IiIiIiIiIiIiIqIO/Pbbbxg4cGCbC34A4O7urvU4KSkJN954I5KSkgAA8+fP77DejRs3QiaTAQAuXLiA9PR0FBYW4urVqxg4cCDmzZun04W8vLw8BAcHw8nJSWt5cnIyLl26hMceewzvvPMOTp48iT59+iAmJgazZ88Wyv3yyy9YvXo1Hn30UVy4cAEHDhzA5cuXER4ejoceegj29vbYsmULcnJy0NTUhKioKCQkJLS5wBkWFob3338f9fX1cHV17TJuIiIisl3Mr5hfERERkf4xx2KORWQp2HGdiKxScXExVq1ahTfeeAPe3t6mDqfbnnrqKdxwww245557TB0KERERWbG/npQylqqqKjzyyCNYtWoVQkNDjfrcKpUKDz/8MGJjY3HbbbcZ9bmJiIjIMnl7e6OoqAhnz56Fv79/t7ZdunRpm2UZGRmora0VLtKdO3cOq1atgqenJ2JjY+Ho6IgjR45g7dq1WL58OUaNGtVh/SqVCqdOncL06dPbXV9fX4+UlBSMHj0aUVFR+Pbbb7Flyxb4+/tj+PDhWmU//fRTODg4IDY2FuXl5cjOzoadnR3EYjEaGhpw55134uTJk/jyyy8hk8kwb948re2DgoKg0Whw4sQJ3Hzzzd3aT0RERGRbmF8xvyIiIiL9Y47FHIvIUrDjOhGZlXPnzmHHjh345ZdfcOnSJfTt2xdDhw7F3Llz4efnp3M9W7duxdixY7U6rRcXF+PLL7/EyZMncfbsWbS0tCAzM7PTel599VU0Nzfj3//+d4/b1BOzZ8/Gm2++iZkzZ0IqlRr1uYmIiMjynT17Fh9//DFOnTqF2tpauLq6ws/PDyNGjMDtt99u6vCwbds2KBQKo3daBwCJRIIZM2Zg+/btmDx5MhwcHIweAxEREVmWv/3tb3jxxRfxxBNPCDnMsGHDMHToUEgknZ9inzBhgtbjXbt2obKyEkuXLoWbmxsA4L333oOXlxdeeuklYQSo2267Dc888wy2bNnS6UW/qqoqXL16VRj16q9qamqwdOlSIY4pU6YgMTERX3zxRZuLfi0tLUhOThbaVFdXh9zcXERERAjnxm677TaUl5fj0KFDbS769e/fHwBw/vx5XvQjIiKiTjG/Yn5FRERE+sccizkWkaUQmzoAIqJW3333HZ588kkUFhZi8uTJWLx4MSZPnoxffvkFTz75JPLy8nSqp7S0FAUFBW3u0svPz8fBgwchEok6TISup1KpUFBQ0CYBMoYRI0bA2dkZ+/fvN/pzExERkWU7ceIE/vWvf+HMmTOYOnUq4uPjMXXqVIjFYmRlZWmVXb9+PR544AGjxldXV4evvvoK06ZNM+rzXm/y5Mm4dOkScnJyTBYDERERWY6wsDC88MILGDFiBM6cOYNdu3YhJSUFDz74II4ePapzPYWFhfjoo48QHR0tXISrr69HYWEhoqKicPnyZdTV1aGurg6XLl1CeHg4Ll68iOrq6g7rrK+vB4B2p4AGACcnJ4wfP154LJFIoFAoUFFR0absxIkTtS5iBgcHQ6PRYPLkyVrlFAoFqqqq0NLSorW8NYZLly51thuIiIiImF8xvyIiIiIDYI7FHIvIUnDEdSIyC+Xl5di4cSP69++P1atXC3frAUBMTAyeffZZvPnmm3j11Ve77HR+6NAheHl5ITg4WGv59OnTERsbCwcHB7zzzju4ePFip/X8+uuvuHz5MiIjI3vesB4Si8W45ZZb8PXXX2P+/PkQiURGj4GIiIgs0/bt2+Hi4oKXXnqpzcmf2tparcetoyEY09dffw07OzuMGDHC6M/dqk+fPggLC8NXX32FKVOmmCwOIiIishwKhQKPP/44VCoVSktL8f3332Pv3r147bXXsHbt2i5nCvz999+xfv16DBkyBIsWLRKWl5eXQ6PRICMjAxkZGe1uW1tbC09Pz07r12g07S7v169fm/NKffr0wZkzZ9qU9fLy0nrs4uIi1PHX5RqNBo2Njejbt2+ncRERERF1hPmV9nLmV0RERKQPzLG0lzPHIjJP7LhORGZh165daGpqwpIlS7Q6rQOAm5sbEhISkJycjF27dmHx4sWd1pWXl4ebbrqpTUIjlUq7FVN+fj78/PyEjvKbNm3Ct99+iw0bNuA///kPCgoK4ODggIkTJ+Kee+6BWHxtEouKigosXboU99xzDxwcHLBnzx4olUqEhobiwQcfRL9+/fDJJ5/gwIEDwp2HiYmJcHV11Xr+sLAwZGdno7S0FIMGDepW7ERERGS7fvvtNwwcOLDdEQvc3d21HiclJeHGG29EUlISAGD+/Pkd1rtx40YhL7pw4QLS09NRWFiIq1evYuDAgZg3b55OndHz8vIQHBwMJycnreXJycm4dOkSHnvsMbzzzjs4efIk+vTpg5iYGMyePVso98svv2D16tV49NFHceHCBRw4cACXL19GeHg4HnroIdjb22PLli3IyclBU1MToqKikJCQ0KaTflhYGN5//33U19e3ycOIiIiIOtI62pNCoYCvry82b96MI0eO4M477+xwG5VKhXXr1sHe3h6PPfYY7OzshHVqtRrAtamcw8PD291eLpd3WHdrHtPQ0NDu+tbzVbroqGxHy/96obF15Ky/ntsjIiIi6gzzqz8xvyIiIiJ9YY71J+ZYROaHHdeJyCz88MMP8Pb2xg033NDu+htvvBHe3t744YcfOu24Xl1djaqqKr109P7xxx/bjLauVquRkpIChUKBe++9FwUFBdizZw/kcjmmT5+uVTYnJwcqlQrR0dGor6/H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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAC64AAAHjCAYAAACZnKOsAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzsnQe4E8XXxofee+9Il6oiCCiCdERAESmKggiIIkgTRUVQARWU3jtSlC4qiEoRlSJILwIiXVQ6gnSY73nP90z+m9zkZpO7yU3ufX/Ps3CT3WyZmd1598yZc5JorbUihBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQkJE0lDtmBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQggBdFwnhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQElLouE4IIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCEkpNBxnRBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQkhIoeM6IYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEkJBCx3VCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghIYWO64QQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEJCCh3XCSGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhIQUOq4TQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIICSl0XCeEEEIIISQeKVy4sEqSJIksH3/8cUiPtX37djnOkSNHVGInHGVx4cIFOcYPP/yg4pNt27ape+65R6VIkULOJ1J5/PHHVbt27eL7NBI9qIPHHnss0ZcDIST6ocaKH6ixIg9qrMiAGosQkhCgvoofqK8iD+qryID6ihCSEKC+ih+oryIP6qvIgPqKhBM6rhNCCCGEkHgDDr0DBgyIdZuaNWvGuh6/D8YxGA7LxmEcS65cudSTTz6pDh06pMLJ5s2b1V9//aXy588fp/3MmDFDpU+fPtZtypYtK8cqUKCAilRMvfz6668RXRZoczjPM2fO+NwmU6ZMcoxq1aqp+OSNN96Q6/zjjz/kfMJtdPWckIF7+pVXXvFabyNHjgz6WL72S6KTY8eOqQYNGqg0adJI+x03blzQ+4prP4LnEidVkGiDGosayxNqLOehxiLRyOTJk9XDDz+s0qVL5/edyR/UWCSxQX1FfeUJ9ZXzUF+RaOPo0aPq2WefFds+bFilS5dWkyZNCnp/1FcksUF9RX3lCfWV81BfkWjj7NmzMj6YN29elTp1alWiRAk1fPjwoPdHfRW50HGdEEIIIYSEHRhvT5065fp848YN9cknn6hbt27J5w0bNqiVK1e6/Qaf8T3Adtj+5s2brvXYXzBG4RUrVogz79KlS9Xx48dV48aN1e3bt1W4yJEjh8qdO7dKlixZyI+VPHnysB0r0glHWcCxHcdImTKlik/gsA7nlIIFC8r5RCqZM2cWZ39CQPPmzdX169fVxo0b1bvvvqu6deumvv32W9uF40Q/MnfuXLl/DFprNXbsWHX+/HlWEolYqLH+BzVW/ECNFXlQYxErly9fluw6nTt3DqpgqLFIYoT66n9QX8UP1FeRB/UVMfz+++9i+505c6bas2ePev3111WXLl3UrFmzbBcS9RVJjFBf/Q/qq/iB+iryoL4i1vF12K6+/vprtX//fvXRRx+p/v37qylTptguJOqr6ICO64QQQgghJOwgAkmTJk3UkiVLxKBbu3ZtlTRpUnkRAYiui0hwL7/8srp06ZL8j88mOja2w/a1atWS32M/2F8wUcuzZcsmzrxVqlSR2bp79+5VBw8elHXnzp1TrVq1UhkzZpSldevWbg6LiNKLyN19+vRRGTJkkONPmzbNbf8414ULF8b4TSCzinHcfPnyqVSpUqmSJUuqqVOnukWpxjGef/559d9//7kiyFtnD584ccItujwiFniyevVqde+998oxChUqpMaMGeO2Hvvr2LGjatu2rUTnK1WqlPrll19UIOC4jRo1EuMDyvPBBx+U+jPrcG533XWXfK5UqZLrfMNdFkgRWKNGDanTrFmzqjp16qi///7bLdL6I4884jIq4jMii3savcwxfEVynjdvnqpQoYLMFodTOV66rQ6y/fr1kzaP9cWLF49RJ3ZTTMLxFm3Uszzv3Lmj3n77bZUnTx45RvXq1dXOnTtj7Ae/QbvGvYCojNmzZ1eLFi1STtGhQwfXuXmLau2vLFC/+O3atWvFqdjsC+3BgHpu2LChSps2rdzzeKbAKdpaFt27d5d2ifYFw7m3uoutLDAgdf/990u7gQM+HK///PNPt98+99xzcoz3339fyhu/x+QZu6B8cA/17t1bjoF7FYab2DIBWCPR4/mDto3oBHff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VyRSoL6KG9RXhBASOVBfkUiB+ipuUF8lTOi4TkgUd2qDBw9WkUqVKlXUokWL4vs0CCGERCgzZsxwOXBalzfeeENFEp07d1ZFixZVqVOnVvnz51ddunRR//77b0D7mDx5snr44YdVunTpVPr06WPd9tVXX5VyWLhwoXKaMWPGqAIFCqg0adKoBg0aqBMnTrit37Vrl6pWrZpca/HixRN8P162bFn1119/SZkkVJo0aaIKFiwodQrn/bfeekvdunUroH1Abz7wwAMqVapUUmax8fjjj4fEoVlrrfr166dy5swp91Dr1q3VxYsXXevXrl2r6tevr3LkyKEyZMigHnroIfku0hg5cqS0uRYtWqhI4P3331eLFy8O2f5/+OEHaQ9nzpwJ2TEIITGhxvIONVb4SOga6/z58/KOcNddd4muxrvCoEGD1J07dwKeBOa5WLl+/bp66aWXVObMmWV5+eWX1Y0bN8Kqscyg0L333quSJ08eERPvvEGNRQgJNdGgr/Cu36tXL1WyZEmVNm1asQX07t1bXblyJaD9eLtO64R/O31HOGxYgdgqEgIJXV8Z+4G35dSpU7b2QX3lLNRXhJBQEw36ylffgmXz5s2290N9FZkkdH0FvvjiC1W3bl2xKwUzVhMt+opjhMHBMUISCdBxPZGKLPDTTz/JgAMcPIJ1MDl27JgYjWA8Qoc+btw4t/WRYMTauXOneuKJJ1SePHnEYaxixYohddDwBTrnV155RWXPnl3Oo3HjxurPP/8Mal+4pq+++kp169bN7fu///5bPf300zJohjr9+OOP/e4L54Hzcpq3335b2nwgA3fhZtasWapMmTLSbooVK6amTZsW53vdMwqkvxcRpwZNW7VqJfcY7rV33nknzu2CEELCAQbSYBSwLug/IokKFSqomTNnqn379qn58+erH3/8UV66A+Hy5cuiueDgEhvff/+92rFjhwoFy5YtE6eTd999V23cuFFdu3bNzXkWnxs1aiR6asuWLeqFF16QvmX37t0qoQLnm9y5c6tkyZKphAomTCxYsEDt379fTZkyRdrygAEDAtrH1atXRcP7c7bG/qFJQgHeMYYPHy7HwGAmtLD1fvrll19U1apV1ddff622bdumKleurBo2bKgOHDigIolMmTJJm4P2jATg5J81a9b4Pg1CSAigxnKHGiu8JHSN9c8//6izZ8+qsWPHqj179qhhw4apoUOHivN6oKxYscLtXcgK3oswwLh06VJZlixZovr37x9WjWUcITt16qTq1KmjIhVqLEJIOIh0fQWHEQQkgEM3bEvTp08XO1bXrl0D3hcCMFiv0+rMY6fvCLUNKxBbRUIhoesrBNLwvL8QvKt69eoy5hUI1FfOQH1FCAkHka6voIE8zw++T4UKFVL3339/QPuivoo8Erq+AgjEhnHC119/PU77iXR9xTHC4OAYIYkINHGc6dOn67Rp0+q//vrLbbl06VJElfbXX3+t33rrLT1x4kSNprB58+aA91GpUiVds2ZNvX37dj116lSdLFkyvWLFCtf6MWPG6HTp0umlS5fK/kuXLq1btWrl+HXguDg+zqNGjRq6atWqrvUzZ87U3bt31z/++KP+448/9JAhQ3TSpEn1Dz/8oMNJr169dK5cufT333+vt27dqh966CH9wAMPBLWvDh06yOLJoUOHdNeuXfXs2bN17ty59dChQ2Pdz+7du3Xy5Mnld05z69YtnTNnTqmfSATtAe0A7R/tYvLkydKOVq5cGad7/ezZs27b4N7Cvq3boGycpHnz5nJv4R774osv5J6bMGFC0O2CEELCAZ6heF7Fxueff65LlCihU6RIIf/Pnz8/xjbo97t06aIHDBigs2fPLs/lPn36hOy8R40apfPkyeP4NaP/KFasmD5w4ID0HQsWLHBbD41Tq1YtnSZNGl2wYEHdr18/ffPmzRj7adu2re7fv3+M7xs3biz9hVUD4Djbtm2Tz4sWLRJNcP78edc2999/v3711Vd1XLhx44b+7rvv9J07d2z/BudfpkwZvXjxYimTVKlS6WrVqsm6M2fOiJbMmzevTpkypbSLKVOmuP1+//79ogXxO/zfo0cPXahQIdf648ePy7Wb5fDhw26/x2dPbYxybdSokevztWvXdOfOnUXbpU6dWvphb+0zUPbt2ydtIFT07NnTTScHgqkXbxw8eFAXLVpUb9myxet7xerVq3XlypWlTlCnuI+8gfsZ94kn5cqV0717946h/9EevIH2lilTJj1ixAjtFC1atNDPPfec23fffvutPHMuX77suo9btmypM2TIIAva6rlz52Lsy7M92SV//vx63rx5Mb7HswDPBvN+4+958f7777vaP8rck9u3b8u9j2vAvWbeF9esWeN2j0yaNEmXL19enmtP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75gftA+c1q3ZgnDfwG5uJtkGoru9QX1FSPRDfZW49JWvvtr4b1BfBQYm8WECGHSV8aFBIAJvwSz8tQv4yUHzWoMjWOEYYeT5VKHO6tevbzuTpV2or0jY0cRxPv30Uz1lyhS9Z88e/dtvv+lmzZrp4sWLu23z5ptv6hw5cuh58+bpP/74Q69YsUIPHz5c1u3YsQMhz/WYMWNkHfaVLVs2nS5duhjHeuedd3T+/Pn1rl279F9//aVv3rwp31+/fl0+YylTpozu1auX67OhRo0aukuXLq7Pa9askeOePn1aPuOcUqRIoadPn6737t2rGzdurJMnTy6f4wOcG845VGzevFmO8euvv/rcpn///rINysoTlO22bdv0u+++q9OkSSN/Y0FdgE6dOumCBQu66sost27dkvUo96xZs+p27drp7du3S/uZPHmytBVP2rZtK+fiyddff62TJUump02bJu0I5VWlShW3bY4ePSrn9eKLL+qiRYu6ztMOOFfruXsuTvL777/LeaHdVa9eXf5GmdhtF926dZM2jHsIv23durXOmDGjPnbsmKy/evWqLlCggG7RooXevXu3/uCDD6R9o34M/fr107lz59bz58+X/aDeO3fu7LaNPypVqqRr1qwp9YF6Qf3gvAxoH6YOcC1Dhw6Vv4Mpz4oVK+phw4a5fWd9Fngup06d0k6BNotrQzmiPFGuhQoV0teuXXPb7vDhw3KdaMOe+CuLcePGyTNp5syZ+tChQ3rRokU6ffr0+qOPPgrqnFFec+fO1Rs3bpR78+zZs651d+7cke/wPCeEhA70K7iP+/Tpoy9evCjPZgOeJUmSJJHnyv79++X/pEmTiray8ssvv8gz44svvtBXrlxx9bvgvvvu0w0aNJDf//zzz7patWqyzwULFrjt4+DBg7LvCRMmyDPn33//dVu/bt066TPxezx7rEDn4Tg7d+7UW7Zs0XXr1hUdgGeYFewT14q+5eTJk/r8+fOudbNmzdJp06aVZySem48//rhcE7RJoBohVHrpxo0bulGjRvqee+6RZ3Bs/X9c9JK367HWVyBl4YuEopc++eQT/dlnn4lON/cI2vfSpUtttwv014ULF9arVq0SrTN48GC5F9avX+/aBv0vyhkL9FSGDBmkrYZTL3m7Hs/72C7US4GXF/USIdEHNVbi01gA+hP2Q2+6yt/7djg0FrQB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Y4Z16NAh3ZtIn9dNmzbZF198YYsXL3ZDwXrPBd7hR19//XXftIYZkz179tgVV1zhvis0NJiGcr/nnnvccrx0ftYQcFo3DUWmc0O+fPkSvP62bdvccLQpueOOO+zvv/9238nLly+3AQMGuLggFPTe9L2VE7z11lvufKxzdeLhBjX8suKhxLTPFFvrvL5y5Up79913XayQnu/6n376yc477zzftM6HOq9o33gf130lS5b0zaNjxPt4dqRh+aZMmeKGUdSwyxmheEvbUsMwJnbllVe64eIUE33//fcJHnvyySfd895++2375Zdf7Pzzz3cxro75QGh7a1v7xyVpbW+9T53rFVOkhz7nBw4ccMMdZgd6H6LfccEcHlLH9yOPPOJ+YzZq1MjFxIHS95W+LwsUKOB+j2qoQsXSvXv3djFWYi1atLCvv/46aOsPILiIsXJOjDV9+nT3+1Zx7PPPP5/uIXcVj6oN6b///W+a86ptRe2O+o3rbXsgxko4TLLaifxpO/38888WKroupRjFPybIihgro7JbjJWYYhVti//9739Ws2ZNd9OQ0Ip/A6X4Su1F33zzjZtesWKFrVu3zrX/i46P+Pj4BL8ztL01vLiGQk+L9lOzZs2sRIkS7ntAbf36jfvggw+6WFx/J47B9LnfuHFjOrYEgGAgvsqZbVj6/Z2e397169d3sZliNH2/79q1yz7//HPr0qVLsvPruot+O/u3qRBfBVebNm1c2+pff/3lYiddDy9Xrpy71pSYHv/ss8/cuf/ss8/23U98lb72JW1zXdPW9Wxtx5kzZ2Z4/yX3G+R0427asIDcg/gq58VX+j2q66b+59lg/DbXuUcxleKokydPulhMcZnmFeKrpBQLKdft8ssvt99//z2o8ZX+z58/v9vXelxtE0uXLrXOnTv7lkF8FXx333232w/K6ws0N80rrfhKjyuvTNcA/R/fuXOnL6ciNbRfIeiCngoPV6FXw8DWqFHDDTGhoRji4uICrqikHsfaNeqN5H/TfVOmTPH1SFd1O38XX3xxhipB5nXqwaXeQzt27DjtKqKqnK+hWDQEnaq+eisNqQq2luk/FKGqdqua5+zZs32VozXtv89V2VWV2kVDdXjv13y63zutaoii+3S8qVK0erKpSqwqhj/99NMJ1vPPP//0rFq1yvPCCy+4oXW+//77gN6fqpmq8rhu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9ddf9wwbNizJcxUva10Smzt3ridfvnyeN99807Ny5Uq3vc4991zf4zpGFIf07NnTs2bNGheX6LhILp5PjuIHHQMp3YLlhRdecMemYiQdf1rPsLAw91skPcfFsWPH3P54/vnnkzymbfTuu+96/ve//7ltcfvtt7u4ad26db55hg8f7qlQoYJnxowZno0bN7r9fuuttwa8vaR58+aetm3buv2h19T+0WfGa+DAge68rmX6b0tt6/RSTPjiiy8muE/HYkr7S98TwaK4Xtvvww8/TPAaR44cSTbm1jGcmNZVnzvdNI8+T/rbe2wp9lScP2DAALeftF80re+mjND20vfv999/7/aB/+dd36W6b9KkSRlaNoDQIMbKWTHW+vXrPY8++qjnxx9/dLGi4iv9bn7yySeTzJtWjJXSMaAY7o477nDP0zlG3+mKZxXv6DEhxvp/+u2vthudR1esWOFp3769azNKrm0htePCe75W265+D+hvnadFMY3iL8VR/jGB/2tkRoyVVlyRU2Is0e8CrbvWo1evXu5vfb68bbJqS1LMr98QS5Ysce1S+lwE2iY1fvx41zb4zjvvuM+qYquiRYt6Ro0a5ZunR48erq1VbcqKyfU5fO211zL0ftS+e/XVV7v3oLap//znPwke1/GhNjAAmYP4KmfFVzqXd+nSxdO0aVP3nZ1am0xK8ZXOnWoLGjJkiPsuXrx4sTuXdOrUKcF8WqbmmzVrVpJlE18lpO2o8/Nll13mro3rb52/Az0u1E6na3VffPGFi42efvpp11as87oo1nnjjTfcdaINGzZ4/vvf/7prUf7XQImvAqffDGpn1GdE+06xiLZ34mvnKbVhBfIbJD1xdyBowwJyFuKrnBVf6dyqXCfFRJs3b3bf782aNXO3QOOrQH6bK9flrLPO8ixdutSdH3RNSvl0iumE+CrhNUK1TXhjLLUh6Prz7t27gxZfea8H6/qhHtf1c+U83Xbbbb5lEF8Fr/1KbUlqR1I8q+u+Tz31lNsf/jlRabVfBRJfqa3R2xata8t6jeR+zwSC9iucLhLXcyj9AFTDCwD4J54psNQPBoSWgnQlwmYkYQ5A8o1S3gv9arTQ95k3ybNixYou2UMNF7qokZHE9YULF3rq16/vLiQl7hykH9mNGzd2SQhqOE/coUsXPHShSw0jSpju3r27588//0zSAJNSxx1R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4WbdrzP74FWX4P53/kiLKVzGYiLLWkzhsra9cDnbFvnP7Xi+gvZXkQrutsSa/LNC+80Knv+EVTscYzUOxViNz5dbzQ8/sqqHYizCc8olsrvE/L27SWYHkOOpUvmWLVvsk08+sSuvvNLdp7+V0F61atUE88bHx9trr71mU6ZMcZXOy5Yt65LV77rrLneuKleunD3yyCO++atXr+7OY4mpknqdOnVs8ODBrkK6kpz9E6KV8DxixAhfxyxV+W7cuHGCZXTv3j3BtM6LquCuDl9t2rRJ9r326NHD6tWrZ/ny5bMPPvjA6tevb7NmzbLff//dJcH/8MMP7tx6wQUXuNcvXbq0e97cuXNdAvbmzZtdQr46lL311ltuXi2zQYMGrsK4V//+/a148eLJVqpv2bKl+/+mm27yvbfvv//esrP//Oc/rrJ6emh/3nDDDW77AAAAgBgLAAAg1GjDAgAAIH4CAADITLkycb1hw4b266+/Jrhvw4YNVrlyZV8y3VlnnWUrV670VTjNDYKR3K774osUdQnuxU4esWKHjlrdQ1sTVGqPDwu33RElbWtkOfuzSHmXvP6nktgjy7uE9vXFq7mbV7jnlFU7tMNq/7DV6uz50mp9PcNVac9nHqqyA0hAI2LEnfr/75vMFJEvzMK8o1MEqFevXq7yuTdxfdq0ae6+pUuXJpjvmWeesffee88liavT1K5du9x5SaKioty0krDPPffcVF9Py7/qqqtccrcS22fMmGH33HOP73ElwH/55ZeuA1cgCdOqnK5kei1P587UzJw506677jpXUV4OHDhgPXv2tGuuucZVnVf19pEjR9ott9zi5t25c6dLsH/44YetU6dOdujQIZfgrn2cEapAryT8F1980b33iIgIy+7Sm7QerOcCAADkZsRYAAAAxFcAAABZifYpAACAXJa4roqvH3/8sf3xxx+2b98+u++++1ySn78FCxa4qrP79++3atWquQqttWvXTnW5WpY3aV309969ey2vCaSau6uKnkql9nzXDrLySracPN6a7V3rq9R+6oOxtqNQGfujaEXbXDTaNhWr5P4+ULCY/aG/i1Wyzw6b2Tl3W6GTcVb37z+t/jfrrN7cz6zugS1WJD6OquxAHqek9V7T12XJa0/vVdcK5U9f4rqSyJ999lnbunWrm16+fLmrUOSfuK6E7YkTJ7rK5Er09lZU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1Y8//mhFihRxyd7nnnuuS0zXtCgxXIntmzZtsoULF9r777/vKqKryoGS2U/XWWed5W4pURJY+/btrV27dm5aCexKVF+0aJF169bNVq9e7da3aNGi7vFmzZrZ+vXrU0xcP3HihLv5V23wVpP3H6owGLzLC/ZygZws3wWXWviZZ9tNu2Ks4t4we33jKfsyurntKlTKhvw2xUpMHm925tkhrbzOZxPInnLqZ1MNVhMnTrQmTZq4eEVDFHfp0sUKFCjgOgZqWOROnTpl9WoCAADkKMRYAAAAxFcAAABZifYpALnBlr9i7OaebVJ8PH/+/Hby5EmLOBVvs/+9b1C/DhaXLzzNZRctVcnGTJgWxLUFkCcS14cOHeqSxh966CFr1KiR5cuXL9X5T5065RLFFZzpuVOnTrVQ0peiEuaVoO5f/V3rum7dOjddpkwZ9/fx48fdF+mvv/5qF198cYrLnD17ts2aNcs3XaNGDRs1apSVK1cuZO+jQoUKIVs2kCNFR5s1bGTXrlxu5T58yZ5vcK2tKVXb7j3nHrv7f1PtolNxVkjzhBifTSB7ymmfzQULFrik9WHDhtnff//tEtfVke7MM8+0K664wh588EF3PwAAAIixAAAAsgptWAAAANknftKy58yZY/v377dq1apZ//79rXbt2inOv3TpUleYNDY21l1L7du3r1sXrx9++ME+++wzl2OlAqXPPfecVa9ePcEylFc1adIkW7JkiSv4qfd28803uyJcXrt377bXX3/d5V4VKlTILrzwQuvTp0+a+WQAsq+I/Cds/qBNac94zMy+/+fP2QM2/zM6Txo6jz/99QOQBxPXR48ena6q6QpEmjZt6m7btm2zUDt48KDFx8cnCJJE09u3b3d/161b1yXfP/DAA65CqwLEc845J8Vldu/e3bp27eqb9lZ1VXCnRPlg0rIVMO7YscM8Hk9Qlw3kBp78EdZs3zp7dsVYe6FhX/urSAV7vMkA+3XjSesdtsrCd8dYWFTFoFdf57MJZE/B/GyqM1soO6X527lzp1166aXub2+jjTemiIyMtIsuush1+rvssssyZX0AAAByA2IsIGPuvqW3HdqXerttRqonya6dMewWAMjBiK8A5BpKopqS1SsBIC8IVfykxHElkA8YMMDq1Klj8+bNs5EjR9qYMWOsRIkSSeZfu3atvfzyyy6BXMnq3333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N9Y999zjkvHffPNNV039xIkTatGihQYPHnxBkr4lrr/++us6cuSIihYtyucJAABkiPiK+AoAAPgeMRYxFhAqSFwHEJY2b96sRx55RC+88ILKli2rUPPnP/9ZV1xxhW6//fZArwoAAAhj5ydW5ZZ9+/bpvvvuc/FavXr1cvW9T58+rXvvvVc9e/bUb37zm1x9bwAAEJqsbWnjxo3asWOHqlSpclGvHTZs2AXTZs6c6Sq1exLNd+7c6eKiUqVKuRilQIECWrlypcaOHasRI0aoefPmmcY2W7ZsUadOndJ93pLIR48erWuuucYlnH/xxRcuCd2246qrrkoz77vvvqv8+fO7ddizZ48WL17sKrlHR0fr6NGjuuWWW1zy+yeffOIS7i2xPrUaNWooJSVFGzZs0NVXX31R+wkAAEQW4iviKwAAQIyVGm1YQGQhcR1AULELdXPnznWVnqwi1GWXXab69eurd+/eqlSpUraXM336dF133XVpktYtmd0urNkFNrvQeObMGc2aNSvT5TzzzDM6deqU/vSnPyk3WdWrf/7zn+rWrZtKlCiRq+8NAABCn8U6b7/9tktisqQoq3hpsZRV7LzxxhsDvXqaPXu2atWqletJ6yZv3rzq2rWr65K6bdu2LjkLAAAgM7/97W/197//XQ899JA3hmnYsKFrs7LYIjPXX399mvH3339fSUlJLqG9WLFibtprr72mMmXK6KmnnvJWMbcb7P7617+6JPPMEtfthkCr0O6p3H6+AwcOuPfyrEe7du00dOhQffzxxxckrltbmVVp92zT4cOHtWLFCjVp0sTbNmbrZUnty5YtuyBx/fLLL3d/d+3aReI6AADIFPEV8RUAAPA9YixiLCBURAd6BQDA48svv9T//d//uS6RLYlo0KBB7q8lsdt06/Y4O7Zt26Z169ZdUGlqzZo1Wrp0qaKiojK8mHd+xSpbzvkX8XKDJZUVKlRIS5YsyfX3BgAAoc0qXD788MPavn272rdvr4EDB7q/Vilz4cKFaeYdN26c7rrrrlxdP0uA+vTTT9WxY0cFisWYdpPk8uXLA7YOAAAgdDRq1EhPPvmka6+xGMuSz62K+d13363Vq1dneznW5vXWW2+pc+fO3kRyqyZl060a+q+//upiJRssVmncuLESEhK0f//+DJdprzdFihRJ93mr6t66dWvvuCWlW/J9YmLi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VspR6qq2DfJAO3z4cP5Q7N577231pQAAg8jWrVvTpUuX0owZM3JJPSZgHj16NHV0dOTMEzmrmiidb968Oe/2EuWjIjcV4ny33XZbWrp0aVq9enXObHPnzs357N9//82vETvGfPDBB3Wv75FHHknbt29PFy5cSLfffntqZdaaNGlSyYJJAAAAAAAAAKB5FNeB68bMmTNzuWrXrl25rB4FqCif33LLLen999/PJapGRBnr+++/T3/88Ud64IEHuh8/depU2rFjR8mxxc8PPvhgLq6fOHEiF9ynTp2aWunKlSvp559/zu+lfFoqAEA9ixcvTvv378+L8WIyehTXYzHc008/nRYuXJhGjBhR9XkXL15Mly9fTidPnkzr16+v+H08FsX1yGRr165NO3fuTG1tbencuXN5Evv48ePz+XsTmWvbtm3p4MGD3QsHB1qU5qO4/uKLL7bk9QEAAAAAAADgZjSkK1qhANepH374IW3YsCHNmjUrLV++vOHnvf3222nkyJHplVdeuarX++abb9Lu3bvTpk2bWloYj4nx69atS59++ml+HwAAN5KNGzemv//+O2e2VtizZ0/69ttvc9YaNmxYS64BAAAAAAAAAG42Q1t9AQD1zJ49O73wwgupvb09T+ZsVDyno6MjT0+/GqNHj05Llixp+ZTzKNDPmzdPaR0AuCEtWrQodXZ2piNHjgz4a8cE+liouGDBAqV1AAAAAAAAABhAJq4DAAAAAAAAAAAAANBUJq4DAAAAAAAAAAAAANBUiusAAAAAAAAAAAAAADSV4joAAAAAAAAAAAAAAE2luA4AAAAAAAAAAAAAQFMprgMAAAAAAAAAAAAA0FSK6wAAAAAAAAAAAAAANJXiOgAAAAAAAAAAAAAATaW4DgAAAAAAAAAAAABAUymuAwAAAAAAAAAAAADQVIrrAAAAAAAAAAAAAAA0leI6AAAAAAAAAAAAAACpmf4H67WL6yi0NEwAAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -763,13 +624,13 @@ "\n", "# plot data and fit:\n", "plt.sca(ahs[1, 0])\n", - "mds.binnedData.plot('Q', 'I', yerr= 'ISigma', ax = ahs[1,0], label = 'Measured data', zorder = 1)\n", + "ahs[1,0].errorbar(selected_q, selected_i, yerr=selected_sigma, fmt='.', label='Measured data', zorder = 1)\n", "plt.xscale('log')\n", "plt.yscale('log')\n", "plt.xlabel('Q (1/nm)')\n", - "plt.ylabel('I (1/cm)')\n", + "plt.ylabel('I (1/(m sr))')\n", "# plt.xlim(1e-1, 2)\n", - "plt.plot(mcres._measData['Q'][0], mcres.modelIAvg.modelIMean.values, zorder = 2, label = 'McSAS3 result')\n", + "plt.plot(selected_q, mcres.modelIAvg.modelIMean.values, zorder = 2, label = 'McSAS3 result')\n", "plt.legend()\n", "\n", "# plot fitting statistics:\n", @@ -809,7 +670,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.13.12" } }, "nbformat": 4, diff --git a/pyproject.toml b/pyproject.toml index 17cf06e..4b74d81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,7 @@ name = "mcsas3" description = "A refactored McSAS for analysis of X-ray and Neutron scattering data." dynamic = ["version", "readme"] +requires-python = ">=3.12" license = "GPL-3.0-or-later" classifiers = [ # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers @@ -13,9 +14,9 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Framework :: Jupyter :: JupyterLab", "Topic :: Utilities", @@ -24,7 +25,18 @@ classifiers = [ "Intended Audience :: Science/Research", ] dependencies = [ - "pyyaml", "pandas", "attrs", "h5py", "pint", "sasmodels" + "pyyaml", + "pandas", + "attrs", + "h5py", + "pint", + "sasmodels", + "modacor>=1.0.0", +] + +[project.optional-dependencies] +standalone = [ + "pyinstaller>=6.19", ] [[project.authors]] @@ -82,18 +94,33 @@ exclude_commit_patterns = ["chore", ".*\\bGHA\\b.*", ".*\\b[gG][hH] actions?\\b. upload_to_vcs_release = false [tool.black] -line-length = 100 +line-length = 120 preview = true [tool.isort] profile = "black" -line_length = 100 +line_length = 120 group_by_package = true known_first_party = "mcsas3" ensure_newline_before_comments = true extend_skip = ["ci/templates", ".ipynb_checkpoints"] +[tool.ruff] +line-length = 120 +target-version = "py312" +extend-exclude = ["ci/templates", ".ipynb_checkpoints", "notebooks"] + +[tool.ruff.lint] +select = ["E", "F", "W", "I"] +ignore = ["E203"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + [tool.docformatter] recursive = true -wrap-summaries = 100 -wrap-descriptions = 100 +wrap-summaries = 120 +wrap-descriptions = 120 diff --git a/src/mcsas3/McSAS3_and_GUI.code-workspace b/src/mcsas3/McSAS3_and_GUI.code-workspace new file mode 100644 index 0000000..b324304 --- /dev/null +++ b/src/mcsas3/McSAS3_and_GUI.code-workspace @@ -0,0 +1,13 @@ +{ + "folders": [ + { + "path": "../.." + }, + { + "path": "../../../McSAS3GUI" + }, + { + "path": "../../../MoDaCor" + } + ] +} \ No newline at end of file diff --git a/src/mcsas3/__init__.py b/src/mcsas3/__init__.py index f663658..f55da26 100644 --- a/src/mcsas3/__init__.py +++ b/src/mcsas3/__init__.py @@ -1,8 +1,39 @@ -# -*- coding: utf-8 -*- -# __init__.py +"""Public McSAS3 API centered on canonical ProcessingData workflows.""" -""" -Refactored McSAS implementation -""" +from .data_adapters import ( + DEFAULT_ANALYSIS_STAGE, + STAGE_BINNED, + STAGE_CLIPPED, + STAGE_RAW, + selected_bundle_from_processing, +) +from .data_model import BaseData, DataBundle, ProcessingData +from .workflows import ( + load_result_processing_data, + optimize_processing_data, + prepare_1d_processing_data, + prepare_1d_processing_data_from_file, + prepare_2d_processing_data, + prepare_2d_processing_data_from_file, + store_result_processing_data, +) __version__ = "1.0.6" + +__all__ = [ + "BaseData", + "DEFAULT_ANALYSIS_STAGE", + "DataBundle", + "ProcessingData", + "STAGE_BINNED", + "STAGE_CLIPPED", + "STAGE_RAW", + "load_result_processing_data", + "optimize_processing_data", + "prepare_1d_processing_data", + "prepare_1d_processing_data_from_file", + "prepare_2d_processing_data", + "prepare_2d_processing_data_from_file", + "selected_bundle_from_processing", + "store_result_processing_data", +] diff --git a/src/mcsas3/__main__.py b/src/mcsas3/__main__.py index 3ff3841..1d364ad 100644 --- a/src/mcsas3/__main__.py +++ b/src/mcsas3/__main__.py @@ -8,7 +8,8 @@ from pathlib import Path from sys import platform -from mcsas3.cli_tools import McSAS3_cli_histogram, McSAS3_cli_optimize +from mcsas3.cli_histogram import McSAS3_cli_histogram +from mcsas3.cli_optimize import McSAS3_cli_optimize def isMac(): @@ -130,9 +131,7 @@ def isMac(): ] } ] - adict_histogram = [ - {k: v for k, v in adict.items() if k in ["resultFile", "histConfigFile", "resultIndex"]} - ] + adict_histogram = [{k: v for k, v in adict.items() if k in ["resultFile", "histConfigFile", "resultIndex"]}] try: McSAS3_cli_optimize(**adict_optimize) McSAS3_cli_histogram(**adict_histogram) diff --git a/src/mcsas3/_cli_common.py b/src/mcsas3/_cli_common.py new file mode 100644 index 0000000..ccc97ad --- /dev/null +++ b/src/mcsas3/_cli_common.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from pathlib import Path + + +def validate_existing_file(_instance, attribute, value: Path) -> None: + if not value.exists(): + raise FileNotFoundError(f"{attribute.name} file {value} must exist") + + +def validate_yaml_file(_instance, attribute, value: Path) -> None: + validate_existing_file(_instance, attribute, value) + if value.suffix != ".yaml": + raise ValueError(f"{attribute.name} file must be a yaml file and end in .yaml") diff --git a/src/mcsas3/cli_histogram.py b/src/mcsas3/cli_histogram.py new file mode 100644 index 0000000..6fc1d4c --- /dev/null +++ b/src/mcsas3/cli_histogram.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from pathlib import Path + +import pandas as pd +import yaml +from attrs import define, field, validators + +from . import workflows +from ._cli_common import validate_yaml_file +from .mc_analysis import McAnalysis +from .mc_plot import McPlot + + +@define +class McSAS3_cli_histogram(object): + """Runs the McSAS histogrammer (only) from the command line arguments.""" + + resultFile: Path = field(kw_only=True, validator=validators.instance_of(Path)) + histConfigFile: Path = field(kw_only=True, validator=[validators.instance_of(Path), validate_yaml_file]) + resultIndex: int = field(kw_only=True, validator=[validators.instance_of(int)]) + + def __attrs_post_init__(self): + self.run() + + def run(self): + processing = workflows.load_result_processing_data(self.resultFile, result_index=self.resultIndex) + + with open(self.histConfigFile, "r") as f: + histRanges = pd.DataFrame(list(yaml.safe_load_all(f))) + mcres = McAnalysis( + self.resultFile, + processing, + histRanges, + store=True, + resultIndex=self.resultIndex, + ) + + mp = McPlot() + saveHistFile = self.resultFile.with_suffix(".pdf") + if saveHistFile.is_file(): + saveHistFile.unlink() + mp.resultCard(mcres, saveHistFile=saveHistFile) diff --git a/src/mcsas3/cli_optimize.py b/src/mcsas3/cli_optimize.py new file mode 100644 index 0000000..c1010da --- /dev/null +++ b/src/mcsas3/cli_optimize.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from pathlib import Path + +import yaml +from attrs import define, field, validators + +from . import workflows +from ._cli_common import validate_existing_file, validate_yaml_file + + +@define +class McSAS3_cli_optimize(object): + """Runs the McSAS optimizer (only) from the command line arguments.""" + + dataFile: Path = field(kw_only=True, validator=validators.instance_of(Path)) + resultFile: Path = field(kw_only=True, validator=validators.instance_of(Path)) + readConfigFile: Path = field(kw_only=True, validator=[validators.instance_of(Path), validate_yaml_file]) + runConfigFile: Path = field(kw_only=True, validator=[validators.instance_of(Path), validate_yaml_file]) + resultIndex: int = field(kw_only=True, validator=[validators.instance_of(int)]) + deleteIfExists: bool = field(kw_only=True, validator=[validators.instance_of(bool)]) + nThreads: int = field(kw_only=True, validator=[validators.instance_of(int)]) + + @dataFile.validator + def fileExists(self, attribute, value): + validate_existing_file(self, attribute, value) + + def __attrs_post_init__(self): + self.run() + + def run(self): + if self.resultFile.is_file(): + if (self.resultFile != self.dataFile) & (self.deleteIfExists): + self.resultFile.unlink() + with open(self.readConfigFile, "r") as f: + readDict = yaml.safe_load(f) or {} + processing = workflows.prepare_1d_processing_data_from_file( + self.dataFile, + result_index=self.resultIndex, + **readDict, + ) + with open(self.runConfigFile, "r") as f: + optDict = yaml.safe_load(f) or {} + if self.nThreads > 0: + optDict["nCores"] = self.nThreads + processing_metadata = dict(readDict) + processing_metadata["filename"] = self.dataFile + workflows.optimize_processing_data( + processing, + self.resultFile, + result_index=self.resultIndex, + processing_metadata=processing_metadata, + seed=None, + **optDict, + ) diff --git a/src/mcsas3/cli_tools.py b/src/mcsas3/cli_tools.py index 430e413..c1f0ca2 100644 --- a/src/mcsas3/cli_tools.py +++ b/src/mcsas3/cli_tools.py @@ -1,103 +1,9 @@ -# src/mcsas3/cli_tools.py +"""Compatibility re-export for the CLI helper entry points.""" -from pathlib import Path +from __future__ import annotations -import pandas as pd -import yaml -from attrs import define, field, validators +from . import workflows +from .cli_histogram import McSAS3_cli_histogram +from .cli_optimize import McSAS3_cli_optimize -from mcsas3 import mc_data_1d, mc_hat, mc_plot -from mcsas3.mc_analysis import McAnalysis - - -@define -class McSAS3_cli_optimize(object): - """Runs the McSAS optimizer (only) from the command line arguments""" - - def checkConfig(self, attribute, value): - assert value.exists(), f"configuration file {value} must exist" - assert value.suffix == ".yaml", "configuration file must be a yaml file (and end in .yaml)" - - dataFile: Path = field(kw_only=True, validator=validators.instance_of(Path)) - resultFile: Path = field(kw_only=True, validator=validators.instance_of(Path)) - readConfigFile: Path = field( - kw_only=True, validator=[validators.instance_of(Path), checkConfig] - ) - runConfigFile: Path = field(kw_only=True, validator=[validators.instance_of(Path), checkConfig]) - resultIndex: int = field(kw_only=True, validator=[validators.instance_of(int)]) - deleteIfExists: bool = field(kw_only=True, validator=[validators.instance_of(bool)]) - nThreads: int = field(kw_only=True, validator=[validators.instance_of(int)]) - - @dataFile.validator - def fileExists(self, attribute, value): - assert value.exists(), f"input data file {value} must exist" - - # init is auto-generated by attrs!!! - def __attrs_post_init__(self): - self.run() - - def run(self): - # remove any prior results file: - if self.resultFile.is_file(): - # only remove result file if it is not the main file! - # This way, you can add McSAS to an existing nexus file - if (self.resultFile != self.dataFile) & (self.deleteIfExists): - self.resultFile.unlink() - # read the configuration file - with open(self.readConfigFile, "r") as f: - readDict = yaml.safe_load(f) - # load the data - mds = mc_data_1d.McData1D(filename=self.dataFile, resultIndex=self.resultIndex, **readDict) - # store the full data in the result file: - mds.store(self.resultFile) - # read the configuration file - with open(self.runConfigFile, "r") as f: - optDict = yaml.safe_load(f) - if self.nThreads > 0: - optDict["nCores"] = self.nThreads - # run the Monte Carlo method - mh = mc_hat.McHat(seed=None, resultIndex=self.resultIndex, **optDict) - md = mds.measData.copy() - mh.run(md, self.resultFile, resultIndex=self.resultIndex) - - -@define -class McSAS3_cli_histogram(object): - """Runs the McSAS histogrammer (only) from the command line arguments""" - - def checkConfig(self, attribute, value): - assert value.exists(), f"configuration file {value} must exist" - assert value.suffix == ".yaml", "configuration file must be a yaml file (and end in .yaml)" - - resultFile: Path = field(kw_only=True, validator=validators.instance_of(Path)) - histConfigFile: Path = field( - kw_only=True, validator=[validators.instance_of(Path), checkConfig] - ) - resultIndex: int = field(kw_only=True, validator=[validators.instance_of(int)]) - - def __attrs_post_init__(self): - self.run() - - def run(self): - # read the configuration file - - # load the data - mds = mc_data_1d.McData1D(loadFromFile=self.resultFile, resultIndex=self.resultIndex) - - # read the configuration file - with open(self.histConfigFile, "r") as f: - histRanges = pd.DataFrame(list(yaml.safe_load_all(f))) - # run the Monte Carlo method - md = mds.measData.copy() - mcres = McAnalysis( - self.resultFile, md, histRanges, store=True, resultIndex=self.resultIndex - ) - - # plotting: - # plot the histogram result - mp = mc_plot.McPlot() - # output file for plot: - saveHistFile = self.resultFile.with_suffix(".pdf") - if saveHistFile.is_file(): - saveHistFile.unlink() - mp.resultCard(mcres, saveHistFile=saveHistFile) +__all__ = ["McSAS3_cli_histogram", "McSAS3_cli_optimize", "workflows"] diff --git a/src/mcsas3/data_adapters.py b/src/mcsas3/data_adapters.py new file mode 100644 index 0000000..be8d2c4 --- /dev/null +++ b/src/mcsas3/data_adapters.py @@ -0,0 +1,460 @@ +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import Any, TypeAlias + +import numpy as np +import pandas + +from .data_model import BaseData, DataBundle, ProcessingData, ureg + +STAGE_RAW = "sample_raw" +STAGE_CLIPPED = "sample_clipped" +STAGE_BINNED = "sample_binned" +CANONICAL_STAGE_NAMES = (STAGE_RAW, STAGE_CLIPPED, STAGE_BINNED) +DEFAULT_ANALYSIS_STAGE = STAGE_BINNED +ANALYSIS_STAGE_ATTRIBUTE = "analysis_stage" +CANONICAL_1D_KEYS = ("signal", "Q", "mask") +CANONICAL_2D_KEYS = ("signal", "Qx", "Qy", "mask") + +DEFAULT_Q_UNITS = ureg.Unit("1 / nanometer") +DEFAULT_INTENSITY_UNITS = ureg.Unit("1 / meter / steradian") +DEFAULT_UNCERTAINTY_KEY = "propagate_to_all" +CanonicalBundleLike: TypeAlias = Mapping[str, BaseData] + + +def _require_supported_rank(signal: np.ndarray) -> None: + if signal.ndim not in (1, 2): + raise ValueError(f"Canonical scattering bundles must be 1D or 2D, got rank {signal.ndim}.") + if signal.size == 0: + raise ValueError("Canonical scattering bundles cannot be empty.") + + +def _require_matching_shape(name: str, array: np.ndarray, expected_shape: Sequence[int]) -> None: + if array.shape != tuple(expected_shape): + raise ValueError(f"{name} shape {array.shape} does not match signal shape {tuple(expected_shape)}.") + + +def _resolve_unit(unit_value: Any, *, default) -> Any: + if unit_value is None: + return default + if isinstance(unit_value, str): + normalized = unit_value.strip() + reciprocal_angstrom_aliases = { + "1/A": "1 / angstrom", + "A^-1": "1 / angstrom", + "Å^-1": "1 / angstrom", + "1/Å": "1 / angstrom", + } + if normalized in reciprocal_angstrom_aliases: + unit_value = reciprocal_angstrom_aliases[normalized] + return ureg.Unit(unit_value) + + +def _as_array(data: Any, *, dtype: Any = float) -> np.ndarray: + return np.asarray(data, dtype=dtype) + + +def _combine_uncertainties(data: BaseData) -> np.ndarray: + if not data.uncertainties: + raise ValueError("Legacy conversion requires at least one uncertainty array.") + + variance = np.zeros_like(_as_array(data.signal, dtype=float), dtype=float) + for uncertainty in data.uncertainties.values(): + variance += _as_array(uncertainty, dtype=float) ** 2 + return np.sqrt(variance) + + +def _optional_uncertainties(signal: Any) -> dict[str, np.ndarray]: + if signal is None: + return {} + return {DEFAULT_UNCERTAINTY_KEY: np.array(signal, dtype=float, copy=True)} + + +def _normalize_bundle_units( + bundle: DataBundle, + *, + q_units=DEFAULT_Q_UNITS, + intensity_units=DEFAULT_INTENSITY_UNITS, +) -> DataBundle: + target_q_units = _resolve_unit(q_units, default=DEFAULT_Q_UNITS) + target_intensity_units = _resolve_unit(intensity_units, default=DEFAULT_INTENSITY_UNITS) + + bundle["signal"].to_units(target_intensity_units) + if "Q" in bundle: + bundle["Q"].to_units(target_q_units) + if "Qx" in bundle: + bundle["Qx"].to_units(target_q_units) + if "Qy" in bundle: + bundle["Qy"].to_units(target_q_units) + return bundle + + +def _stage_bundle( + *, + signal: Any, + signal_uncertainty: Any, + q0: Any, + q1: Any | None = None, + q_uncertainty: Any = None, + mask: Any = None, + q_units=DEFAULT_Q_UNITS, + intensity_units=DEFAULT_INTENSITY_UNITS, + source_q_units=None, + source_intensity_units=None, +) -> DataBundle: + bundle = DataBundle() + signal_array = np.array(signal, dtype=float, copy=True) + _require_supported_rank(signal_array) + rank_of_data = signal_array.ndim + signal_uncertainty_array = None + if signal_uncertainty is not None: + signal_uncertainty_array = np.array(signal_uncertainty, dtype=float, copy=True) + _require_matching_shape("signal_uncertainty", signal_uncertainty_array, signal_array.shape) + q0_array = np.array(q0, dtype=float, copy=True) + _require_matching_shape("q0", q0_array, signal_array.shape) + q1_array = None + if q1 is not None: + q1_array = np.array(q1, dtype=float, copy=True) + _require_matching_shape("q1", q1_array, signal_array.shape) + q_uncertainty_array = None + if q_uncertainty is not None: + q_uncertainty_array = np.array(q_uncertainty, dtype=float, copy=True) + _require_matching_shape("q_uncertainty", q_uncertainty_array, signal_array.shape) + mask_array = None + if mask is not None: + mask_array = np.array(mask, dtype=bool, copy=True) + _require_matching_shape("mask", mask_array, signal_array.shape) + source_q_units = _resolve_unit(source_q_units, default=_resolve_unit(q_units, default=DEFAULT_Q_UNITS)) + source_intensity_units = _resolve_unit( + source_intensity_units, + default=_resolve_unit(intensity_units, default=DEFAULT_INTENSITY_UNITS), + ) + bundle["signal"] = BaseData( + signal=signal_array, + units=source_intensity_units, + uncertainties=_optional_uncertainties(signal_uncertainty_array), + rank_of_data=rank_of_data, + ) + if q1 is None: + bundle["Q"] = BaseData( + signal=q0_array, + units=source_q_units, + uncertainties=_optional_uncertainties(q_uncertainty_array), + rank_of_data=rank_of_data, + ) + else: + bundle["Qy"] = BaseData( + signal=q0_array, + units=source_q_units, + uncertainties={}, + rank_of_data=rank_of_data, + ) + bundle["Qx"] = BaseData( + signal=q1_array, + units=source_q_units, + uncertainties={}, + rank_of_data=rank_of_data, + ) + if mask_array is not None: + bundle["mask"] = BaseData( + signal=mask_array, + units=ureg.dimensionless, + uncertainties={}, + rank_of_data=rank_of_data, + ) + bundle.default_plot = "signal" + return _normalize_bundle_units(bundle, q_units=q_units, intensity_units=intensity_units) + + +def bundle_from_1d_dataframe( + df: pandas.DataFrame, + *, + q_units=DEFAULT_Q_UNITS, + intensity_units=DEFAULT_INTENSITY_UNITS, + source_q_units=None, + source_intensity_units=None, +) -> DataBundle: + """Build a canonical 1D bundle from a dataframe in source or canonical units.""" + + required_columns = {"Q", "I", "ISigma"} + missing_columns = required_columns.difference(df.columns) + if missing_columns: + raise KeyError(f"1D dataframe is missing required columns: {sorted(missing_columns)}") + + return _stage_bundle( + signal=df["I"].to_numpy(dtype=float), + signal_uncertainty=df["ISigma"].to_numpy(dtype=float), + q0=df["Q"].to_numpy(dtype=float), + q_uncertainty=df["QSigma"].to_numpy(dtype=float) if "QSigma" in df.columns else None, + mask=df["mask"].to_numpy(dtype=bool) if "mask" in df.columns else None, + q_units=q_units, + intensity_units=intensity_units, + source_q_units=source_q_units, + source_intensity_units=source_intensity_units, + ) + + +def bundle_from_2d_arrays( + *, + intensity: Any, + intensity_sigma: Any, + qx: Any, + qy: Any, + mask: Any = None, + q_units=DEFAULT_Q_UNITS, + intensity_units=DEFAULT_INTENSITY_UNITS, + source_q_units=None, + source_intensity_units=None, +) -> DataBundle: + """Build a canonical 2D bundle from raw array components.""" + + return _stage_bundle( + signal=intensity, + signal_uncertainty=intensity_sigma, + q0=qy, + q1=qx, + mask=mask, + q_units=q_units, + intensity_units=intensity_units, + source_q_units=source_q_units, + source_intensity_units=source_intensity_units, + ) + + +def bundle_from_2d_stage( + stage_data: Mapping[str, Any], + *, + q_units=DEFAULT_Q_UNITS, + intensity_units=DEFAULT_INTENSITY_UNITS, + source_q_units=None, + source_intensity_units=None, +) -> DataBundle: + """Build a canonical 2D bundle from raw or clipped legacy-style stage mappings.""" + + raw_keys = {"I", "ISigma", "Qx", "Qy"} + clipped_keys = {"I2D", "ISigma2D", "Q0Crop2D", "Q1Crop2D"} + + if raw_keys.issubset(stage_data): + return bundle_from_2d_arrays( + intensity=stage_data["I"], + intensity_sigma=stage_data["ISigma"], + qx=stage_data["Qx"], + qy=stage_data["Qy"], + mask=stage_data.get("mask"), + q_units=q_units, + intensity_units=intensity_units, + source_q_units=source_q_units, + source_intensity_units=source_intensity_units, + ) + if clipped_keys.issubset(stage_data): + return bundle_from_2d_arrays( + intensity=stage_data["I2D"], + intensity_sigma=stage_data["ISigma2D"], + qx=stage_data["Q1Crop2D"], + qy=stage_data["Q0Crop2D"], + mask=stage_data.get("mask2D"), + q_units=q_units, + intensity_units=intensity_units, + source_q_units=source_q_units, + source_intensity_units=source_intensity_units, + ) + raise KeyError( + "2D stage data must provide either raw keys " + "('I', 'ISigma', 'Qx', 'Qy') or clipped keys ('I2D', 'ISigma2D', 'Q0Crop2D', 'Q1Crop2D')." + ) + + +def normalize_analysis_stage(stage_name: str) -> str: + """Validate and normalize the selected analysis stage name.""" + + if stage_name not in CANONICAL_STAGE_NAMES: + raise ValueError(f"Invalid analysis stage '{stage_name}'. Expected one of: {', '.join(CANONICAL_STAGE_NAMES)}.") + return stage_name + + +def set_processing_analysis_stage(processing: ProcessingData, stage_name: str) -> ProcessingData: + """Store the selected analysis stage on a `ProcessingData` carrier.""" + + normalized_stage = normalize_analysis_stage(stage_name) + setattr(processing, ANALYSIS_STAGE_ATTRIBUTE, normalized_stage) + return processing + + +def get_processing_analysis_stage( + processing: ProcessingData, + *, + default: str = DEFAULT_ANALYSIS_STAGE, +) -> str: + """Read the selected analysis stage from a `ProcessingData` carrier.""" + + stage_name = getattr(processing, ANALYSIS_STAGE_ATTRIBUTE, default) + return normalize_analysis_stage(stage_name) + + +def selected_bundle_from_processing( + processing: ProcessingData, + *, + stage_name: str | None = None, +) -> DataBundle: + """Return the selected canonical stage bundle from a processing carrier.""" + + if stage_name is None: + resolved_stage = get_processing_analysis_stage(processing) + else: + resolved_stage = normalize_analysis_stage(stage_name) + if resolved_stage not in processing: + raise KeyError(f"Selected analysis stage '{resolved_stage}' is not available in the supplied ProcessingData.") + return processing[resolved_stage] + + +def is_canonical_bundle(data: Any) -> bool: + """Return whether the object matches the canonical 1D or 2D bundle contract.""" + + return isinstance(data, Mapping) and "signal" in data and ("Q" in data or {"Qx", "Qy"}.issubset(data.keys())) + + +def as_analysis_bundle(data: Any) -> DataBundle: + """Coerce a processing carrier or bundle into the selected canonical bundle.""" + + if isinstance(data, ProcessingData): + return selected_bundle_from_processing(data) + if is_canonical_bundle(data): + return data + raise TypeError("Analysis input must be a canonical DataBundle or a ProcessingData carrier.") + + +def bundle_dimension(bundle: CanonicalBundleLike) -> int: + """Return the scattering dimensionality encoded by a canonical bundle.""" + + if {"signal", "Q"}.issubset(bundle): + return 1 + if {"signal", "Qx", "Qy"}.issubset(bundle): + return 2 + raise ValueError("Bundle does not match the canonical 1D or 2D scattering contract.") + + +def fit_arrays_from_bundle(bundle: CanonicalBundleLike) -> tuple[tuple[np.ndarray, ...], np.ndarray, np.ndarray]: + """Flatten a canonical bundle into the Q, intensity, and sigma arrays used for fitting.""" + + ndim = bundle_dimension(bundle) + signal = _as_array(bundle["signal"].signal, dtype=float) + signal_sigma = _combine_uncertainties(bundle["signal"]) + + if ndim == 1: + q = _as_array(bundle["Q"].signal, dtype=float).reshape(-1) + return (q,), signal.reshape(-1), signal_sigma.reshape(-1) + + qy = _as_array(bundle["Qy"].signal, dtype=float) + qx = _as_array(bundle["Qx"].signal, dtype=float) + mask = np.zeros_like(signal, dtype=bool) + if "mask" in bundle: + mask = _as_array(bundle["mask"].signal, dtype=bool) + + valid = np.isfinite(signal) & np.isfinite(signal_sigma) & (signal_sigma != 0) & np.invert(mask) + return ( + ( + qy[valid].flatten(), + qx[valid].flatten(), + ), + signal[valid].flatten(), + signal_sigma[valid].flatten(), + ) + + +def model_q_arrays_from_bundle(bundle: CanonicalBundleLike) -> list[np.ndarray]: + """Return Q arrays in the shape expected by SasModels kernels.""" + + q_arrays, _signal, _signal_sigma = fit_arrays_from_bundle(bundle) + return [q_component.copy() for q_component in q_arrays] + + +def q_support_from_bundle(bundle: CanonicalBundleLike) -> np.ndarray: + """Return absolute Q support for limit auto-scaling.""" + + q_arrays, _signal, _signal_sigma = fit_arrays_from_bundle(bundle) + if len(q_arrays) == 1: + return np.abs(q_arrays[0]) + return np.sqrt(np.sum(np.stack([q_component**2 for q_component in q_arrays], axis=0), axis=0)) + + +def frame_from_bundle(bundle: CanonicalBundleLike) -> pandas.DataFrame: + """Project a canonical bundle into the stage dataframe representation.""" + + ndim = bundle_dimension(bundle) + signal = _as_array(bundle["signal"].signal, dtype=float) + signal_sigma = _combine_uncertainties(bundle["signal"]) + + if ndim == 1: + frame = pandas.DataFrame( + { + "Q": _as_array(bundle["Q"].signal, dtype=float), + "I": signal, + "ISigma": signal_sigma, + } + ) + if bundle["Q"].uncertainties: + frame["QSigma"] = _combine_uncertainties(bundle["Q"]) + if "mask" in bundle: + frame["mask"] = _as_array(bundle["mask"].signal, dtype=bool) + return frame + + frame = pandas.DataFrame( + { + "Qx": _as_array(bundle["Qx"].signal, dtype=float).flatten(), + "Qy": _as_array(bundle["Qy"].signal, dtype=float).flatten(), + "I": signal.flatten(), + "ISigma": signal_sigma.flatten(), + } + ) + if "mask" in bundle: + frame["mask"] = _as_array(bundle["mask"].signal, dtype=bool).flatten() + return frame + + +def raw_2d_stage_from_bundle(bundle: CanonicalBundleLike) -> dict[str, np.ndarray]: + """Project a canonical 2D bundle into the raw-stage array mapping.""" + + ndim = bundle_dimension(bundle) + if ndim != 2: + raise ValueError("raw_2d_stage_from_bundle requires a canonical 2D scattering bundle.") + + raw_stage = { + "Qx": _as_array(bundle["Qx"].signal, dtype=float).copy(), + "Qy": _as_array(bundle["Qy"].signal, dtype=float).copy(), + "I": _as_array(bundle["signal"].signal, dtype=float).copy(), + "ISigma": _combine_uncertainties(bundle["signal"]).copy(), + } + if "mask" in bundle: + raw_stage["mask"] = _as_array(bundle["mask"].signal, dtype=bool).copy() + return raw_stage + + +__all__ = [ + "ANALYSIS_STAGE_ATTRIBUTE", + "CANONICAL_1D_KEYS", + "CANONICAL_2D_KEYS", + "CANONICAL_STAGE_NAMES", + "DEFAULT_ANALYSIS_STAGE", + "DEFAULT_INTENSITY_UNITS", + "DEFAULT_Q_UNITS", + "DEFAULT_UNCERTAINTY_KEY", + "STAGE_BINNED", + "STAGE_CLIPPED", + "STAGE_RAW", + "as_analysis_bundle", + "bundle_dimension", + "bundle_from_1d_dataframe", + "bundle_from_2d_arrays", + "bundle_from_2d_stage", + "fit_arrays_from_bundle", + "get_processing_analysis_stage", + "is_canonical_bundle", + "frame_from_bundle", + "model_q_arrays_from_bundle", + "normalize_analysis_stage", + "q_support_from_bundle", + "raw_2d_stage_from_bundle", + "selected_bundle_from_processing", + "set_processing_analysis_stage", +] diff --git a/src/mcsas3/data_model.py b/src/mcsas3/data_model.py new file mode 100644 index 0000000..1db0db6 --- /dev/null +++ b/src/mcsas3/data_model.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from modacor import ureg +from modacor.dataclasses.basedata import BaseData +from modacor.dataclasses.databundle import DataBundle +from modacor.dataclasses.processing_data import ProcessingData + +MODACOR_IMPORT_MODE = "installed" +MODACOR_SOURCE = None + +__all__ = [ + "BaseData", + "DataBundle", + "MODACOR_IMPORT_MODE", + "MODACOR_SOURCE", + "ProcessingData", + "ureg", +] diff --git a/src/mcsas3/ingestion.py b/src/mcsas3/ingestion.py new file mode 100644 index 0000000..e93f07f --- /dev/null +++ b/src/mcsas3/ingestion.py @@ -0,0 +1,325 @@ +from __future__ import annotations + +from collections.abc import Mapping +from pathlib import Path +from typing import Any + +import attrs +import h5py +import numpy as np +import pandas + +DEFAULT_1D_CSVARGS = { + "sep": r"\s+", + "header": None, + "names": ["Q", "I", "ISigma"], +} + + +@attrs.frozen +class Loaded1DData: + """Raw 1D table plus the metadata detected during ingestion.""" + + frame: pandas.DataFrame = attrs.field(validator=attrs.validators.instance_of(pandas.DataFrame)) + loader: str = attrs.field(validator=attrs.validators.instance_of(str)) + source_q_units: str | None = attrs.field( + default=None, + validator=attrs.validators.optional(attrs.validators.instance_of(str)), + ) + source_intensity_units: str | None = attrs.field( + default=None, + validator=attrs.validators.optional(attrs.validators.instance_of(str)), + ) + + +@attrs.frozen +class Loaded2DData: + """Raw 2D stage plus the metadata detected during ingestion.""" + + stage: dict[str, np.ndarray] = attrs.field(validator=attrs.validators.instance_of(dict)) + frame: pandas.DataFrame = attrs.field(validator=attrs.validators.instance_of(pandas.DataFrame)) + loader: str = attrs.field(validator=attrs.validators.instance_of(str)) + source_q_units: str | None = attrs.field( + default=None, + validator=attrs.validators.optional(attrs.validators.instance_of(str)), + ) + source_intensity_units: str | None = attrs.field( + default=None, + validator=attrs.validators.optional(attrs.validators.instance_of(str)), + ) + + +def _obj_bytes_to_str(value: Any) -> Any: + if isinstance(value, bytes): + return value.decode("utf-8") + if isinstance(value, np.ndarray): + return value.astype("str") + return value + + +def _dataset_units(h5f: h5py.File, dataset_path: str) -> str | None: + if dataset_path not in h5f: + return None + dataset = h5f[dataset_path] + for attr_name in ("units", "unit"): + if attr_name in dataset.attrs: + return str(_obj_bytes_to_str(dataset.attrs[attr_name])) + return None + + +def _detected_q_units_from_path_dict(h5f: h5py.File, path_dict: Mapping[str, str]) -> str | None: + if "Q" in path_dict: + return _dataset_units(h5f, str(path_dict["Q"])) + if "Qx" in path_dict and "Qy" in path_dict: + qx_units = _dataset_units(h5f, str(path_dict["Qx"])) + qy_units = _dataset_units(h5f, str(path_dict["Qy"])) + if qx_units is not None and qy_units is not None and qx_units != qy_units: + raise ValueError("pathDict provides inconsistent Q units for 'Qx' and 'Qy' datasets.") + return qx_units if qx_units is not None else qy_units + return None + + +def _validated_1d_frame(frame: pandas.DataFrame) -> pandas.DataFrame: + required_columns = {"Q", "I", "ISigma"} + missing_columns = required_columns.difference(frame.columns) + if missing_columns: + raise KeyError(f"1D input data is missing required columns: {sorted(missing_columns)}") + + validated = frame.copy() + for column in ("Q", "I", "ISigma"): + validated[column] = pandas.to_numeric(validated[column], errors="raise").astype(float) + if "QSigma" in validated.columns: + validated["QSigma"] = pandas.to_numeric(validated["QSigma"], errors="raise").astype(float) + if "mask" in validated.columns: + validated["mask"] = validated["mask"].astype(bool) + return validated + + +def _load_nexus_raw_data( + filename: Path, + *, + path_dict: Mapping[str, str] | None = None, +) -> tuple[dict[str, np.ndarray], str | None, str | None]: + raw_data: dict[str, np.ndarray] = {} + detected_q_units = None + detected_intensity_units = None + + with h5py.File(filename, "r") as h5f: + if path_dict is not None: + required_signal_keys = {"I", "ISigma"} + has_combined_q = "Q" in path_dict + has_split_q = {"Qx", "Qy"}.issubset(path_dict.keys()) + if not isinstance(path_dict, Mapping) or not required_signal_keys.issubset(path_dict.keys()): + raise ValueError("pathDict must provide 'I' and 'ISigma' dataset paths.") + if not has_combined_q and not has_split_q: + raise ValueError("pathDict must provide either 'Q' or both 'Qx' and 'Qy' dataset paths.") + for key, dataset_path in path_dict.items(): + raw_data[key] = h5f[str(dataset_path)][()].squeeze() + detected_q_units = _detected_q_units_from_path_dict(h5f, path_dict) + detected_intensity_units = _dataset_units(h5f, str(path_dict["I"])) + else: + signal_path = "/" + while "default" in h5f[signal_path].attrs: + signal_path_add = _obj_bytes_to_str(h5f[signal_path].attrs["default"]) + signal_path += f"{signal_path_add}/" + + if "signal" not in h5f[signal_path].attrs: + raise ValueError("No signal dataset found at the default NeXus path.") + + signal_label = _obj_bytes_to_str(h5f[signal_path].attrs["signal"]) + signal_dataset_path = f"{signal_path}{signal_label}" + raw_data["I"] = h5f[signal_dataset_path][()].squeeze() + detected_intensity_units = _dataset_units(h5f, signal_dataset_path) + + if f"{signal_label}_uncertainty" in h5f[signal_path].attrs: + uncertainty_label = _obj_bytes_to_str(h5f[signal_path].attrs[f"{signal_label}_uncertainty"]) + raw_data["ISigma"] = h5f[f"{signal_path}{uncertainty_label}"][()].squeeze() + elif "uncertainties" in h5f[signal_dataset_path].attrs: + uncertainty_label = _obj_bytes_to_str(h5f[signal_dataset_path].attrs["uncertainties"]) + raw_data["ISigma"] = h5f[f"{signal_path}{uncertainty_label}"][()].squeeze() + else: + raw_data["ISigma"] = raw_data["I"] * 0.001 + + if "mask" in h5f[signal_path].attrs: + mask_label = _obj_bytes_to_str(h5f[signal_path].attrs["mask"]) + raw_data["mask"] = h5f[f"{signal_path}{mask_label}"][()].squeeze() + + axes_label = None + if "axes" in h5f[signal_path].attrs: + axes_label = "axes" + elif "I_axes" in h5f[signal_path].attrs: + axes_label = "I_axes" + if axes_label is None: + raise ValueError("Could not find the axes label associated with the NeXus signal dataset.") + + axes_object = _obj_bytes_to_str(h5f[signal_path].attrs[axes_label]) + q_label = next((candidate for candidate in ("q", "Q") if candidate in axes_object), None) + if q_label is None: + raise ValueError("Could not find a Q axis associated with the NeXus signal dataset.") + raw_data["Q"] = h5f[f"{signal_path}{q_label}"][()].squeeze() + detected_q_units = _dataset_units(h5f, f"{signal_path}{q_label}") + + return raw_data, detected_q_units, detected_intensity_units + + +def _load_1d_csv(filename: Path, *, csvargs: Mapping[str, Any] | None = None) -> Loaded1DData: + local_csvargs = dict(DEFAULT_1D_CSVARGS) + if csvargs is not None: + local_csvargs.update(dict(csvargs)) + frame = pandas.read_csv(filename, **local_csvargs) + return Loaded1DData(frame=_validated_1d_frame(frame), loader="from_csv") + + +def _load_1d_pdh(filename: Path, *, csvargs: Mapping[str, Any] | None = None) -> Loaded1DData: + skiprows = 5 + stop_line = None + with open(filename) as fd: + for line_number, line in enumerate(fd): + if line.startswith(" Loaded1DData: + raw_data, detected_q_units, detected_intensity_units = _load_nexus_raw_data(filename, path_dict=path_dict) + + if np.asarray(raw_data["Q"]).ndim > 1: + raise ValueError("1D ingestion helpers cannot read 2D NeXus data directly.") + + return Loaded1DData( + frame=_validated_1d_frame(pandas.DataFrame(data=raw_data)), + loader="from_nexus", + source_q_units=None if detected_q_units is None else str(detected_q_units), + source_intensity_units=None if detected_intensity_units is None else str(detected_intensity_units), + ) + + +def _loaded_2d_from_raw_data( + raw_data: dict[str, np.ndarray], + *, + detected_q_units: str | None, + detected_intensity_units: str | None, +) -> Loaded2DData: + if "Qx" in raw_data and "Qy" in raw_data: + qx = np.array(raw_data["Qx"], copy=True) + qy = np.array(raw_data["Qy"], copy=True) + else: + if "Q" not in raw_data: + raise ValueError("2D NeXus ingestion requires either 'Q' or explicit 'Qx'/'Qy' datasets.") + q_data = np.asarray(raw_data["Q"]) + if q_data.ndim < 3: + raise ValueError("2D ingestion helpers require a multidimensional Q dataset.") + nonzero_axes = [axis_index for axis_index in range(q_data.shape[0]) if np.any(q_data[axis_index])] + if len(nonzero_axes) < 2: + raise ValueError("2D ingestion helpers could not resolve non-zero Qx/Qy components from the Q dataset.") + qy = np.array(q_data[nonzero_axes[0]].squeeze(), copy=True) + qx = np.array(q_data[nonzero_axes[1]].squeeze(), copy=True) + + intensity = np.array(raw_data["I"], copy=True) + intensity_sigma = np.array(raw_data["ISigma"], copy=True) + if intensity.ndim != 2 or intensity_sigma.ndim != 2 or qx.ndim != 2 or qy.ndim != 2: + raise ValueError("2D ingestion helpers require image-shaped I, ISigma, Qx, and Qy arrays.") + if intensity.shape != intensity_sigma.shape or intensity.shape != qx.shape or intensity.shape != qy.shape: + raise ValueError("2D ingestion helpers require matching I, ISigma, Qx, and Qy shapes.") + + stage = { + "Qx": qx, + "Qy": qy, + "I": intensity, + "ISigma": intensity_sigma, + } + if "mask" in raw_data: + stage["mask"] = np.array(raw_data["mask"], dtype=bool, copy=True) + + frame = pandas.DataFrame({key: value.flatten() for key, value in stage.items()}) + return Loaded2DData( + stage=stage, + frame=frame, + loader="from_nexus", + source_q_units=None if detected_q_units is None else str(detected_q_units), + source_intensity_units=None if detected_intensity_units is None else str(detected_intensity_units), + ) + + +def load_1d_dataframe_from_file( + filename: Path, + *, + loader: str | None = None, + csvargs: Mapping[str, Any] | None = None, + path_dict: Mapping[str, str] | None = None, +) -> Loaded1DData: + """Load a raw 1D table plus detected metadata from a file.""" + + source = Path(filename) + if not source.is_file(): + raise FileNotFoundError(f"Input data file {source} must exist.") + + effective_loader = loader + if effective_loader is None: + suffix = source.suffix.lower() + if suffix == ".pdh": + effective_loader = "from_pdh" + elif suffix in {".csv", ".dat", ".txt"}: + effective_loader = "from_csv" + elif suffix in {".h5", ".hdf5", ".nx", ".nxs"}: + effective_loader = "from_nexus" + else: + raise ValueError(f"Could not determine a supported loader for input file {source}.") + + if effective_loader == "from_pdh": + return _load_1d_pdh(source, csvargs=csvargs) + if effective_loader == "from_csv": + return _load_1d_csv(source, csvargs=csvargs) + if effective_loader == "from_nexus": + return _load_1d_nexus(source, path_dict=path_dict) + raise ValueError(f"Unsupported 1D loader '{effective_loader}'.") + + +def load_2d_stage_from_file( + filename: Path, + *, + loader: str | None = None, + path_dict: Mapping[str, str] | None = None, +) -> Loaded2DData: + """Load a raw 2D stage plus detected metadata from a file.""" + + source = Path(filename) + if not source.is_file(): + raise FileNotFoundError(f"Input data file {source} must exist.") + + effective_loader = loader + if effective_loader is None: + suffix = source.suffix.lower() + if suffix in {".h5", ".hdf5", ".nx", ".nxs"}: + effective_loader = "from_nexus" + else: + raise ValueError(f"Could not determine a supported 2D loader for input file {source}.") + + if effective_loader != "from_nexus": + raise ValueError(f"Unsupported 2D loader '{effective_loader}'.") + + raw_data, detected_q_units, detected_intensity_units = _load_nexus_raw_data(source, path_dict=path_dict) + return _loaded_2d_from_raw_data( + raw_data, + detected_q_units=detected_q_units, + detected_intensity_units=detected_intensity_units, + ) + + +__all__ = [ + "DEFAULT_1D_CSVARGS", + "Loaded1DData", + "Loaded2DData", + "load_1d_dataframe_from_file", + "load_2d_stage_from_file", +] diff --git a/src/mcsas3/mc_analysis.py b/src/mcsas3/mc_analysis.py index 28508fb..212523b 100644 --- a/src/mcsas3/mc_analysis.py +++ b/src/mcsas3/mc_analysis.py @@ -1,7 +1,10 @@ # src/mcsas3/mcanalysis.py +import logging import os.path +from collections.abc import Mapping from pathlib import Path +from typing import Any import h5py import matplotlib.pyplot as plt @@ -10,8 +13,13 @@ from mcsas3.mc_hdf import ResultIndex, storeKVPairs +from .data_adapters import as_analysis_bundle, get_processing_analysis_stage +from .data_model import BaseData, DataBundle, ProcessingData from .mc_core import McCore from .mc_model_histogrammer import McModelHistogrammer +from .optimizer_input import as_optimizer_input + +logger = logging.getLogger(__name__) class McAnalysis: @@ -30,18 +38,17 @@ class McAnalysis: """ # base: - _core = None # instance of core through which _model, _measData, _opt should be accessed - _measData = None # measurement data dict with entries for Q, I, ISigma, - # will be replaced by sasview data model + _core = None # instance of core through which _model, _optimizerInput, _opt should be accessed + _optimizerInput = None # typed optimizer-facing measurement input + _analysisBundle = None # canonical bundle selected for fitting / analysis + _analysisStage = None # canonical stage name inside the processing carrier, when known # specifics for analysis _histRanges = ( pandas.DataFrame() ) # pandas dataframe with one row per range, and the parameters as developed in McSAS, # this gets passed on to McModelHistogrammer as well - _concatI = ( - dict() - ) # for now, just a simple concatenation of the entire set, one row per repetition, + _concatI = dict() # for now, just a simple concatenation of the entire set, one row per repetition, # not separated to indivudual histogram ranges.. _concatOpts = ( pandas.DataFrame() @@ -56,20 +63,16 @@ class McAnalysis: # _averagedModelData = pandas.DataFrame() _averagedHistograms = dict() # dict of dataFrames, one per histogram range, each containing # pandas.DataFrame(columns = ['xMean','xWidth','yMean','yStd','Obs','cdfMean','cdfStd']) - _averagedOpts = ( - pandas.DataFrame() - ) # a dataFrame containing mean and std of optimization parameters. + _averagedOpts = pandas.DataFrame() # a dataFrame containing mean and std of optimization parameters. # Some will be useful, some will be pointless. - _repetitionList = ( - [] - ) # list of values after "repetition", just in case an optimization didn't make it + _repetitionList = [] # list of values after "repetition", just in case an optimization didn't make it _modeKeys = ["totalValue", "mean", "variance", "skew", "kurtosis"] _optKeys = ["scaling", "background", "gof", "accepted", "step"] def __init__( self, inputFile: Path, - measData: dict, + analysis_input: Any, histRanges: pandas.DataFrame, store: bool = False, resultIndex: int = 1, @@ -87,19 +90,17 @@ def __init__( # reset everything to make sure we're not inheriting anything: # base: - self._core = ( - None # instance of core through which _model, _measData, _opt should be accessed - ) - self._measData = None # measurement data dict with entries for Q, I, ISigma, + self._core = None # instance of core through which _model, _optimizerInput, _opt should be accessed + self._optimizerInput = None # typed optimizer-facing measurement input + self._analysisBundle = None # canonical bundle selected for fitting / analysis + self._analysisStage = None # canonical stage name inside the processing carrier, when known # specifics for analysis self._histRanges = ( pandas.DataFrame() ) # pandas dataframe with one row per range, and the parameters as developed in McSAS, # this gets passed on to McModelHistogrammer as well - self._concatI = ( - dict() - ) # for now, just a simple concatenation of the entire set, one row per repetition, + self._concatI = dict() # for now, just a simple concatenation of the entire set, one row per repetition, # not separated to indivudual histogram ranges.. self._concatOpts = ( pandas.DataFrame() @@ -113,60 +114,78 @@ def __init__( # with one row per histogram range. It's pretty cool. self._averagedI = None # averaged model intensity # _averagedModelData = pandas.DataFrame() - self._averagedHistograms = ( - dict() - ) # dict of dataFrames, one per histogram range, each containing + self._averagedHistograms = dict() # dict of dataFrames, one per histogram range, each containing # pandas.DataFrame(columns = ['xMean','xWidth','yMean','yStd','Obs','cdfMean','cdfStd']) - self._averagedOpts = ( - pandas.DataFrame() - ) # a dataFrame containing mean and std of optimization parameters. + self._averagedOpts = pandas.DataFrame() # a dataFrame containing mean and std of optimization parameters. # Some will be useful, some will be pointless. self._averagedAcceptedSteps = [] # averaged steps at which the optimization was accepted - self._averagedAcceptedGofs = ( - [] - ) # not sure how to average these two... not same size, not same location... - self._repetitionList = ( - [] - ) # list of values after "repetition", just in case an optimization didn't make it + self._averagedAcceptedGofs = [] # not sure how to average these two... not same size, not same location... + self._repetitionList = [] # list of values after "repetition", just in case an optimization didn't make it self._modeKeys = ["totalValue", "mean", "variance", "skew", "kurtosis"] self._optKeys = ["scaling", "background", "gof", "accepted", "step"] - assert os.path.isfile(inputFile), "A valid McSAS3 project filename must be provided. " - assert isinstance( - histRanges, pandas.DataFrame - ), "A pandas dataframe with histogram ranges must be provided" - assert measData is not None, "measurement data must be provided for analysis" + if not os.path.isfile(inputFile): + raise ValueError("A valid McSAS3 project filename must be provided.") + if not isinstance(histRanges, pandas.DataFrame): + raise TypeError("A pandas dataframe with histogram ranges must be provided.") + if analysis_input is None: + raise ValueError("Measurement input must be provided for analysis.") self._concatOpts = pandas.DataFrame(columns=self._optKeys) - self._histRanges = histRanges - self._measData = measData + self._histRanges = histRanges.copy(deep=True) + try: + self._analysisBundle = as_analysis_bundle(analysis_input) + except TypeError: + self._analysisBundle = None + else: + if isinstance(analysis_input, ProcessingData): + self._analysisStage = get_processing_analysis_stage(analysis_input) + + analysis_source = self._analysisBundle if self._analysisBundle is not None else analysis_input + self._optimizerInput = as_optimizer_input(analysis_source) # make sure we store and read from the right place. self.resultIndex = ResultIndex(resultIndex) # defines the HDF5 root path - print("Getting List of repetitions...") + logger.info("Getting list of repetitions...") self.getNRep(inputFile) - print("Histogramming every repetition and extracting elements to average...") + logger.info("Histogramming every repetition and extracting elements to average...") self.histAndLoadReps(inputFile, store, resultIndex) - print("Averaging population modes...") + logger.info("Averaging population modes...") self.averageModes() - print("Averaging histograms...") + logger.info("Averaging histograms...") self.averageHistograms() - print("Averaging optimization parameters...") + logger.info("Averaging optimization parameters...") self.averageOpts() - print("Averaging model intensity...") + logger.info("Averaging model intensity...") self.averageI() if store: - print("Storing averages...") + logger.info("Storing averages...") self.store(inputFile) @property def modelIAvg(self) -> pandas.DataFrame: + """Return the averaged model intensity table across repetitions.""" + return self._averagedI @property def optParAvg(self) -> pandas.DataFrame: + """Return averaged optimization parameters across repetitions.""" + return self._averagedOpts + @property + def analysisBundle(self) -> DataBundle | Mapping[str, BaseData] | None: + """Return the canonical bundle used for analysis, when available.""" + + return self._analysisBundle + + @property + def analysisStage(self) -> str | None: + """Return the selected canonical analysis stage, when known.""" + + return self._analysisStage + def histAndLoadReps(self, inputFile: Path, store: bool, resultIndex: int = 1) -> None: """For every repetition, runs its mcModelHistogrammer, and loads the results into the local namespace for further processing.""" @@ -174,8 +193,9 @@ def histAndLoadReps(self, inputFile: Path, store: bool, resultIndex: int = 1) -> # to avoid losing track of what goes where. for repi, repetition in enumerate(self._repetitionList): # for every repetition, load a core: + measurement_input = self._analysisBundle if self._analysisBundle is not None else self._optimizerInput self._core = McCore( - measData=self._measData, + analysis_input=measurement_input, loadFromFile=inputFile, loadFromRepetition=repetition, resultIndex=resultIndex, @@ -186,6 +206,7 @@ def histAndLoadReps(self, inputFile: Path, store: bool, resultIndex: int = 1) -> mh = McModelHistogrammer( self._core, self._histRanges, resultIndex=resultIndex ) # switched from supplying model instance, to supplying complete core instance. + self._histRanges = mh._histRanges.copy(deep=True) if store: mh.store(inputFile, repetition) @@ -203,9 +224,7 @@ def histAndLoadReps(self, inputFile: Path, store: bool, resultIndex: int = 1) -> # Possible avenue for improvement... # tabulate the intensity and scale them with x0 - self._concatI[repetition] = ( - self._core._opt.modelI * self._core._opt.x0[0] + self._core._opt.x0[1] - ) + self._concatI[repetition] = self._core._opt.modelI * self._core._opt.x0[0] + self._core._opt.x0[1] """ this is going to need some reindexing: @@ -230,6 +249,8 @@ def ensureConcatEssentials(self, histIndex: int) -> None: self._concatBinEdges[histIndex] = dict() def averageI(self) -> None: + """Average stored model intensities across repetitions.""" + self._averagedI = pandas.DataFrame( data={ "modelIMean": np.array([i for k, i in self._concatI.items()]).mean(axis=0), @@ -276,8 +297,7 @@ def averageHistograms(self) -> None: self._averagedHistograms[histIndex][key].astype(aH[key].dtype) def averageHistogram(self, histIndex: int) -> None: - """Produces a single averaged histogram for a given histogram range index. - Returns a DataFrame.""" + """Produce one averaged histogram dataframe for a single histogram range.""" # these are the columns and datatypes I want in my histograms. # forced datatypes to prevent issues later on when storing cols = { @@ -297,9 +317,7 @@ def averageHistogram(self, histIndex: int) -> None: averagedHistogram[key].astype(keyType) # histogram bar height: - hists = np.array( - [self._concatHistograms[histIndex][repetition] for repetition in self._repetitionList] - ) + hists = np.array([self._concatHistograms[histIndex][repetition] for repetition in self._repetitionList]) averagedHistogram["yMean"] = hists.mean(axis=0) averagedHistogram["yStd"] = hists.std(axis=0, ddof=1 if hists.shape[0] > 1 else 0) @@ -307,22 +325,20 @@ def averageHistogram(self, histIndex: int) -> None: if len(self._repetitionList) > 1: # assuming (!) that the binEdges for all are the same, # so no 'auto' bin edge selection possible - assert all( + if not all( self._concatBinEdges[histIndex][self._repetitionList[0]] == self._concatBinEdges[histIndex][self._repetitionList[1]] - ) + ): + raise ValueError("All repetitions must use identical histogram bin edges before averaging histograms.") - binEdges = self._concatBinEdges[histIndex][ - self._repetitionList[0] - ] # these are the left edges + binEdges = self._concatBinEdges[histIndex][self._repetitionList[0]] # these are the left edges averagedHistogram["xWidth"] = np.diff(binEdges) averagedHistogram["xMean"] = binEdges[:-1] + 0.5 * averagedHistogram["xWidth"] return averagedHistogram def debugPlot(self, histIndex: int, **kwargs: dict) -> None: - """Plots a single histogram, for debugging purposes only, - can only be done after histogramming is complete.""" + """Plot a single averaged histogram for debugging.""" histDataFrame = self._averagedHistograms[histIndex] plt.bar( histDataFrame["xMean"], @@ -345,8 +361,7 @@ def debugReport(self, histIndex: int) -> str: # for histIndex, histRange in self._histRanges.iterrows(): oString = f"*** Population statistics for Histogram number {histIndex} ***\n" oString += ( - f"For {histRange.rangeMin: 0.02e} ≤ {histRange.parameter} ≤" - f" {histRange.rangeMax: 0.02e}, vol-weighted \n" + f"For {histRange.rangeMin: 0.02e} ≤ {histRange.parameter} ≤ {histRange.rangeMax: 0.02e}, vol-weighted \n" ) oString += "\n".rjust(48, "-") for fieldName in statFieldNames: @@ -361,8 +376,7 @@ def debugAddString( # does a bit of error checking to avoid division by zero for debug*Report methods if valMean != 0: oString = ( - f"{fieldName.ljust(10)}: {valMean: 0.02e} ± {valStd: 0.02e} (±" - f" {valStd/valMean * 100: 0.02f} %) \n" + f"{fieldName.ljust(10)}: {valMean: 0.02e} ± {valStd: 0.02e} (± {valStd / valMean * 100: 0.02f} %) \n" ) else: oString = f"{fieldName.ljust(10)}: {valMean: 0.02e} ± {valStd: 0.02e} \n" @@ -373,14 +387,9 @@ def debugRunReport(self) -> str: the original McSAS). Should be plotted with a fixed-width font because nothing says 2020 like misaligned text.""" statFieldNames = self._optKeys - oString = ( - f"*** Optimization statistics average over {len(self._repetitionList)} repetitions" - " ***\n" - ) - oString += ( - f"For {np.min(self._measData['Q']): 0.02e} ≤ Q (1/nm) ≤" - f" {np.max(self._measData['Q']): 0.02e}\n" - ) + oString = f"*** Optimization statistics average over {len(self._repetitionList)} repetitions ***\n" + q_support = self._optimizerInput.q_support + oString += f"For {np.min(q_support): 0.02e} ≤ Q (1/nm) ≤ {np.max(q_support): 0.02e}\n" oString += "\n".rjust(50, "-") for fieldName in statFieldNames: valMean = self.optParAvg["valMean"][fieldName] @@ -397,7 +406,7 @@ def getNRep(self, inputFile: Path) -> None: for key in h5f[str(self.resultIndex.nxsEntryPoint / "model")].keys(): if "repetition" in key: self._repetitionList.append(int(key.strip("repetition"))) - print(f"{len(self._repetitionList)} repetitions found in McSAS file {inputFile}") + logger.info("%s repetitions found in McSAS file %s", len(self._repetitionList), inputFile) def store(self, filename: Path) -> None: # store averaged histograms, for arhcival purposes only, diff --git a/src/mcsas3/mc_core.py b/src/mcsas3/mc_core.py index 1907d81..7123338 100644 --- a/src/mcsas3/mc_core.py +++ b/src/mcsas3/mc_core.py @@ -1,17 +1,21 @@ # src/mcsas3/mccore.py +import logging from pathlib import Path -from typing import Optional +from typing import Any, Callable, Optional import numpy as np # import scipy.optimize from mcsas3.mc_hdf import ResultIndex +from .data_adapters import as_analysis_bundle, model_q_arrays_from_bundle from .mc_model import McModel from .mc_opt import McOpt from .osb import optimizeScalingAndBackground +logger = logging.getLogger(__name__) + class McCore: """ @@ -19,62 +23,48 @@ class McCore: Parameters ---------- - modelFunc: - SasModels function - measData: dict - measurement data dictionary with Q, I, ISigma containing arrays. - For 2D data, Q is a two-element list with [Qx, Qy]. - This is why it's not a Pandas Dataframe. - pickParameters: dict - dict of values with new random picks, named by parameter names - modelParameterLimits: dict - dict of value pairs (tuples) with random pick bounds, - named by parameter names - x0: - continually updated new guess for total scaling, background values. - weighting: - volume-weighting / compensation factor for the contributions - nContrib: - number of contributions - + analysis_input: + Preferred input is the canonical selected-analysis `DataBundle`. + `OptimizerInput` remains supported as the internal execution-format escape hatch. """ - _measData = None # measurement data dict with entries for Q, I, ISigma + _analysisBundle = None # canonical bundle selected for fitting, when available _model = None # instance of McModel _opt = None # instance of McOpt _OSB = None # optimizeScalingAndBackground instance for this data _outputFilename = None # store output data in here (HDF5) + _stopRequested = None # optional callback checked during optimization def __init__( self, - measData: dict = None, - model: McModel = None, - opt: McOpt = None, + analysis_input: Any = None, + model: McModel | None = None, + opt: McOpt | None = None, loadFromFile: Optional[Path] = None, loadFromRepetition: Optional[int] = None, resultIndex: int = 1, - ): + stop_requested: Callable[[], bool] | None = None, + ) -> None: # make sure we reset state: - self._measData = None + self._analysisBundle = None self._model = None self._opt = None self._OSB = None self._outputFilename = None - - assert measData is not None, "measurement data must be provided to McCore" - assert isinstance( - measData, dict - ), "measurement data must be a dict with (Qx, Qy), I, and Isigma" - - self._measData = measData + self._stopRequested = stop_requested # make sure we store and read from the right place. self.resultIndex = ResultIndex(resultIndex) # defines the HDF5 root path + if analysis_input is None: + raise ValueError("Measurement input must be provided to McCore.") + if loadFromFile is not None: self.load(loadFromFile, loadFromRepetition, resultIndex=resultIndex) testGof, testX0 = self._opt.gof, self._opt.x0 else: + if model is None or opt is None: + raise ValueError("McCore requires both a model and optimization state when not loading from file.") self._model = model self._opt = opt # McOpt instance self._opt.step = 0 # number of iteration steps @@ -82,17 +72,27 @@ def __init__( self._opt.acceptedSteps = [] self._opt.acceptedGofs = [] - self._OSB = optimizeScalingAndBackground(measData["I"], measData["ISigma"]) + try: + self._analysisBundle = as_analysis_bundle(analysis_input) + except TypeError: + self._analysisBundle = None + + osb_input = self._analysisBundle if self._analysisBundle is not None else analysis_input + self._OSB = optimizeScalingAndBackground(osb_input) # set default parameters: self._model.func.info.parameters.defaults.update(self._model.staticParameters) # generate kernel - self._model.kernel = self._model.func.make_kernel(self._measData["Q"]) + if self._analysisBundle is not None: + model_q = model_q_arrays_from_bundle(self._analysisBundle) + else: + from .optimizer_input import as_optimizer_input + + model_q = as_optimizer_input(analysis_input).q_for_model + self._model.kernel = self._model.func.make_kernel(model_q) # calculate scattering intensity by combining intensities from all contributions self.initModelI() - self._opt.gof = ( - self.evaluate() - ) # calculate initial GOF measure, initial happens when x0 is None + self._opt.gof = self.evaluate() # calculate initial GOF measure, initial happens when x0 is None # store the initial background and scaling optimization as new initial guess: self._opt.x0 = self._opt.testX0 @@ -124,27 +124,25 @@ def __init__( def initModelI(self) -> None: """calculate the total intensity from all contributions""" # set initial shape: - I, V = self._model.calcModelIV(self._model.parameterSet.loc[0].to_dict()) + intensity, _volume = self._model.calcModelIV(self._model.parameterSet.loc[0].to_dict()) # zero-out all previously stored values for intensity and volume - self._opt.modelI = np.zeros(I.shape) + self._opt.modelI = np.zeros(intensity.shape) self._model.volumes = np.zeros(self._model.nContrib) # add the intensity of every contribution for contribi in range(self._model.nContrib): - I, V = self._model.calcModelIV(self._model.parameterSet.loc[contribi].to_dict()) + intensity, volume = self._model.calcModelIV(self._model.parameterSet.loc[contribi].to_dict()) # V = self.returnModelV() # intensity is added, NOT normalized by number of contributions. # volume normalization is already done in SasModels (!), # so we have volume-weighted intensities from there... - self._opt.modelI += I # / self._model.nContrib + self._opt.modelI += intensity # / self._model.nContrib # we store the volumes anyway since we may want to use them later # for showing alternatives of number-weighted, or volume-squared weighted histograms - self._model.volumes[contribi] = V + self._model.volumes[contribi] = volume def evaluate( self, testData: Optional[dict] = None - ) -> ( - float - ): # , initial: bool = True): # takes 20 ms! initial is taken care of in osb when x0 is None + ) -> float: # , initial: bool = True): # takes 20 ms! initial is taken care of in osb when x0 is None """scale and calculate goodness-of-fit (GOF) from all contributions""" if testData is None: testData = self._opt.modelI @@ -154,15 +152,15 @@ def evaluate( return gof def contribIndex(self) -> int: + """Return the contribution index updated on the current iteration step.""" + return self._opt.step % self._model.nContrib def reEvaluate(self) -> float: """replace single contribution with new contribution, recalculate intensity and GOF""" # calculate old intensity to subtract: - Iold, dummy = self._model.calcModelIV( - self._model.parameterSet.loc[self.contribIndex()].to_dict() - ) + Iold, dummy = self._model.calcModelIV(self._model.parameterSet.loc[self.contribIndex()].to_dict()) # calculate new intensity to add: Ipick, Vpick = self._model.calcModelIV(self._model.pickParameters) @@ -211,29 +209,33 @@ def iterate(self) -> None: # increment step counter in either case: self._opt.step += 1 - def optimize(self) -> None: - """iterate until target GOF or maxiter reached""" - print("Optimization of repetition {} started:".format(self._opt.repetition)) - print( - "chiSqr: {}, N accepted: {} / {}".format( - self._opt.gof, self._opt.accepted, self._opt.step - ) - ) + def optimize(self) -> bool: + """Iterate until convergence, stop, or configured iteration limits are reached.""" + logger.info("Optimization of repetition %s started.", self._opt.repetition) + logger.info("chiSqr: %s, N accepted: %s / %s", self._opt.gof, self._opt.accepted, self._opt.step) # continue optimizing until we reach any of these targets: while ( (self._opt.accepted < self._opt.maxAccept) # max accepted moves & (self._opt.step < self._opt.maxIter) # max iterations & (self._opt.gof > self._opt.convCrit) # max number of tries + & (not self.stop_requested()) ): # convergence criterion reached self.iterate() # show me every 1000 steps where you are in the optimization: if self._opt.step % 1000 == 1: - print( - "chiSqr: {}, N accepted: {} / {}".format( - self._opt.gof, self._opt.accepted, self._opt.step - ) - ) + logger.info("chiSqr: %s, N accepted: %s / %s", self._opt.gof, self._opt.accepted, self._opt.step) + if self.stop_requested(): + logger.info("Optimization of repetition %s interrupted.", self._opt.repetition) + return False + return True + + def stop_requested(self) -> bool: + """Return whether the current optimization run has been asked to stop.""" + + if self._stopRequested is None: + return False + return bool(self._stopRequested()) def store(self, filename: Path) -> None: """stores the resulting model parameter-set of a single repetition in the NXcanSAS object, @@ -243,17 +245,14 @@ def store(self, filename: Path) -> None: self._model.store(filename=self._outputFilename, repetition=self._opt.repetition) self._opt.store( filename=self._outputFilename, - path=self.resultIndex.nxsEntryPoint - / "optimization" - / f"repetition{self._opt.repetition}", + path=self.resultIndex.nxsEntryPoint / "optimization" / f"repetition{self._opt.repetition}", ) def load(self, loadFromFile: Path, loadFromRepetition: int, resultIndex: int = 1) -> None: - """loads the configuration and set-up from the extended NXcanSAS file""" + """Load model and optimizer state for a stored repetition from the result file.""" # not implemented yet - assert ( - loadFromRepetition is not None - ), "When you are loading from a file, a repetition index must be specified" + if loadFromRepetition is None: + raise ValueError("When loading McCore from a file, a repetition index must be specified.") self._model = McModel( loadFromFile=loadFromFile, loadFromRepetition=loadFromRepetition, diff --git a/src/mcsas3/mc_data.py b/src/mcsas3/mc_data.py deleted file mode 100644 index 1be7217..0000000 --- a/src/mcsas3/mc_data.py +++ /dev/null @@ -1,352 +0,0 @@ -# src/mcsas3/mcdata.py - -import logging -from pathlib import Path, PurePosixPath -from typing import List, Optional - -import attrs -import h5py -import numpy as np -import pandas - -from mcsas3.mc_hdf import ResultIndex, loadKV, storeKVPairs - -# todo use attrs to @define a McData dataclass - - -@attrs.define -class McData: - """ - A simple base class for a data carrier object that can load from a range of sources, - and do rebinning for too large datasets. - This is inherited by the McData1D and McData2D classes intended for actual use. - """ - - filename: Optional[Path] = attrs.field( - default=None, validator=attrs.validators.optional(attrs.validators.instance_of(Path)) - ) - _outputFilename: Optional[Path] = attrs.field( - default=None, validator=attrs.validators.optional(attrs.validators.instance_of(Path)) - ) - loader: Optional[str] = attrs.field( - default=None, validator=attrs.validators.optional(attrs.validators.instance_of(str)) - ) - rawData: Optional[pandas.DataFrame] = attrs.field(default=None) - rawData2D: Optional[pandas.DataFrame] = attrs.field(default=None) - clippedData: Optional[pandas.DataFrame] = attrs.field(default=None) - binnedData: Optional[pandas.DataFrame] = attrs.field(default=None) - measData: Optional[dict] = attrs.field(default=None) - measDataLink: str = attrs.field( - default="binnedData", - validator=attrs.validators.in_(["rawData", "clippedData", "binnedData"]), - ) - dataRange: Optional[list] = attrs.field(default=None) - nbins: int = attrs.field(default=100, validator=attrs.validators.instance_of(int)) - IEmin: float = attrs.field(default=0.01, validator=attrs.validators.instance_of(float)) - pathDict: Optional[dict] = attrs.field(default=None) - binning: str = attrs.field( - default="logarithmic", validator=attrs.validators.in_(["logarithmic"]) - ) - csvargs: dict = attrs.field(factory=dict) - qNudge: Optional[float | List] = attrs.field( - default=None - ) # , validator=attrs.validators.optional(attrs.validators.instance_of(float))) - omitQRanges: Optional[list] = attrs.field(default=None) - resultIndex: ResultIndex = attrs.field( - default=ResultIndex(1), validator=attrs.validators.instance_of(ResultIndex) - ) - - storeKeys = [ # keys to store in an HDF5 output file - "filename", - "rawData", - "clippedData", - "binnedData", - "measData", - "measDataLink", - "nbins", - "IEmin", - "binning", - "dataRange", - "pathDict", - "csvargs", - "loader", - "qNudge", - "omitQRanges", - ] - loadKeys = ( - { # keys to store in an HDF5 output file, values are types to cast to using _HDFLoadKV. - "filename": Path, - "measDataLink": "str", - "nbins": int, - "IEmin": float, - "binning": "str", - "dataRange": None, # not sure what this is.. array? - "csvargs": "dict", - "loader": "str", - "omitQRanges": list, # not sure if this works? - } - ) - - def __init__( - self, - df: Optional[pandas.DataFrame] = None, - loadFromFile: Optional[Path] = None, - resultIndex: int = 1, - **kwargs: dict, - ) -> None: - """loadFromFile must be a previous optimization. - Else, use any of the other 'from_*' functions""" - - # reset everything so we're sure not to inherit anything from elsewhere: - self.filename = None # input filename - self._outputFilename = None # output filename for storing - self.loader = None # can be set to one of the available loaders - self.rawData = None # as read from the file, - self.rawData2D = None # only filled if a 2D NeXus file is loaded - self.clippedData = None # clipped to range, dataframe object - self.binnedData = None # clipped and rebinned - self.measData = ( - self.binnedData - ) # measurement data dict, translated from binnedData dataframe - self.measDataLink = "binnedData" # indicate what measData links to - self.dataRange = None # min-max for data range to fit. overwritten in subclass - self.nbins = 100 # default, set to zero for no rebinning - self.IEmin = 0.01 # default minimum relative uncertainty on the intensity. - self.pathDict = None # for loading HDF5 files without pointers to the data - self.binning = "logarithmic" # the only option that makes sense - self.csvargs = {} # overwritten in subclass - self.qNudge = 0 # can adjust/offset the q values in case of misaligned q vector, - # in particular visible in 2D data... - self.omitQRanges = None # to skip or omit unwanted data ranges, for example with sharp - # XRD peaks, must be a list of [[qmin, qmax], ...] pairs - - # make sure we store and read from the right place. - self.resultIndex = ResultIndex(resultIndex) # defines the HDF5 root path - - if loadFromFile is not None: - self.load(loadFromFile) - - def processKwargs(self, **kwargs: dict) -> None: - for key, value in kwargs.items(): - assert key in self.storeKeys, "Key {} is not a valid option".format(key) - setattr(self, key, value) - - def linkMeasData(self, measDataLink: str = None) -> None: - assert False, "defined in 1D and 2D subclasses" - pass - - def from_file(self, filename: Optional[Path] = None) -> None: - if filename is None: - assert ( - self.filename is not None - ), "at least filename or self.filename must be set for loading from file" - else: - self.filename = Path(filename) - self.filename = Path(self.filename) # cast into pathlib if not already - # make sure file exists - assert self.filename.is_file(), f"input filename: {self.filename} must exist" - - if (self.filename.suffix == ".pdh") or (self.loader == "from_pdh"): - self.loader = "from_pdh" # ensure this is set - self.from_pdh(self.filename) - elif (self.filename.suffix in [".csv", ".dat", ".txt"]) or (self.loader == "from_csv"): - self.loader = "from_csv" # ensure this is set - self.from_csv(self.filename) - elif (self.filename.suffix in [".h5", ".hdf5", ".nx", ".nxs"]) or ( - self.loader == "from_nexus" - ): - self.loader = "from_nexus" - self.from_nexus(self.filename) - # load first, then find out if 1D or 2D - else: - assert False, ( - "Input file type could not be determined. Use from_pandas to load a dataframe or" - " use df = [DataFrame] in input, or use 'loader' = 'from_pdh' or 'from_csv' in" - " input" - ) - - def from_pandas(self, df: pandas.DataFrame = None) -> None: - assert False, "defined in 1D and 2D subclasses" - pass - - def from_csv(self, filename: Path = None, csvargs=None) -> None: - assert False, "defined in 1D and 2D subclasses" - pass - - def from_pdh(self, filename: Path = None) -> None: - assert False, "defined in 1D subclass only" - pass - - # universal reader for 1D and 2D! - def from_nexus(self, filename: Optional[Path] = None) -> None: - # optionally, path can be defined as a dict to point at Q, I and ISigma entries. - def objBytesToStr(inObject): - outObject = inObject - if isinstance(inObject, bytes): - outObject = inObject.decode("utf-8") - if isinstance(inObject, np.ndarray): - outObject = inObject.astype("str") - return outObject - - if filename is None: - assert ( - self.filename is not None - ), "either filename or self.filename must be set to a data source" - filename = self.filename - else: - self.filename = filename # reset to new source if not already set - self.rawData = {} - - if self.pathDict is not None: - assert isinstance( - self.pathDict, dict - ), "provided path must be dict with keys 'Q', 'I', and 'ISigma'" - assert all( - [j in self.pathDict.keys() for j in ["Q", "I", "ISigma"]] - ), "provided path must be dict with keys 'Q', 'I', and 'ISigma'" - with h5py.File(filename, "r") as h5f: - [ - self.rawData.update({key: h5f[f"{val}"][()].squeeze()}) - for key, val in self.pathDict.items() - ] - - else: - sigPath = "/" - with h5py.File(filename, "r") as h5f: - while "default" in h5f[sigPath].attrs: - # this is what we find as a new default to add to the path - sigPathAdd = h5f[sigPath].attrs["default"] - # make sure it's not a bytes string: - sigPathAdd = objBytesToStr(sigPathAdd) - # if isinstance(sigPathAdd, bytes): sigPathAdd = sigPathAdd.decode("utf-8") - # add to the path - sigPath += sigPathAdd + "/" - # make sure we now have access to a signal: - assert "signal" in h5f[sigPath].attrs, "no signal in default neXus path" - signalLabel = objBytesToStr(h5f[sigPath].attrs["signal"]) - # if isinstance(signalLabel, bytes): signalLabel = signalLabel.decode("utf-8") - sigPathI = sigPath + signalLabel - # extract intensity along qDim... sorry, don't know how (qDim is found below): - self.rawData.update({"I": h5f[sigPathI][()].squeeze()}) - # and ISigma: - uncertaintiesAvailable = False - maskAvailable = False - if f"{signalLabel}_uncertainty" in h5f[sigPath].attrs: - uncLabel = objBytesToStr(h5f[sigPath].attrs[f"{signalLabel}_uncertainty"]) - uncertaintiesAvailable = True - elif "uncertainties" in h5f[sigPathI].attrs: - uncLabel = objBytesToStr(h5f[sigPathI].attrs["uncertainties"]) - uncertaintiesAvailable = True - else: - # some default: - self.rawData.update({"ISigma": self.rawData["I"] * 0.001}) - if "mask" in h5f[sigPath].attrs: - maskLabel = objBytesToStr(h5f[sigPath].attrs["mask"]) - maskAvailable = True - - if uncertaintiesAvailable: # load them - # if isinstance(uncLabel, bytes): uncLabel = uncLabel.decode("utf-8") - sigPathISigma = sigPath + uncLabel - self.rawData.update({"ISigma": h5f[sigPathISigma][()].squeeze()}) - if maskAvailable: # load them - sigPathMask = sigPath + maskLabel - self.rawData.update({"mask": h5f[sigPathMask][()].squeeze()}) - - # now we have I, we search for Q in the "axes" attribute: - axesLabel = None - if "axes" in h5f[sigPath].attrs: - axesLabel = "axes" - elif "I_axes" in h5f[sigPath].attrs: - axesLabel = "I_axes" - assert ( - axesLabel is not None - ), "could not find axes label associated with dataset signal in HDF5 file" - axesObj = objBytesToStr(h5f[sigPath].attrs[axesLabel]) - # q can have many names in here: - ques = ["q", "Q"] # q options - # ques = ['q', 'Q', b'q', b'Q'] # q options - # check where we may have a match: - quesTest = [i in axesObj for i in ques] - # assert one of them is there - assert any(quesTest), "q (or Q) not found in signal axes description" - # this is what our q label is in the axes attribute: - qLabel = ques[np.argwhere(np.array(quesTest)).squeeze()] - # if isinstance(qLabel, bytes): qLabel = qLabel.decode("utf-8") - self.rawData.update({"Q": h5f[sigPath + qLabel][()].squeeze()}) - if self.rawData["Q"].ndim > 1: - # we have a three-dimensional Q array, in the order of [dim, y, x] - # find out which dimensions are nonzero (the remainder is Qz): - QxyIndices = np.argwhere( - [self.rawData["Q"][i, :, :].any() for i in range(self.rawData["Q"].shape[0])] - ) - self.rawData["Q"] = self.rawData["Q"][QxyIndices, :, :].squeeze() - self.rawData["Qx"] = self.rawData["Q"][QxyIndices[1], :, :].squeeze() - self.rawData["Qy"] = self.rawData["Q"][QxyIndices[0], :, :].squeeze() - self.rawData2D = self.rawData.copy() # intermediate storage of original data - # but we also need to prepare a Pandas-compatible list-format data - del self.rawData["Q"] - for key in self.rawData.keys(): - self.rawData[key] = self.rawData[key].flatten() - - self.rawData = pandas.DataFrame(data=self.rawData) - self.prepare() - - def is2D(self) -> bool: - return self.rawData2D is not None - - def clip(self) -> None: - assert False, "defined in 1D and 2D subclasses" - pass - - def omit(self) -> None: - assert False, "defined in the 1D and (maybe) 2D subclasses" - pass - - def reBin(self) -> None: - assert False, "defined in 1D and 2D subclasses" - pass - - def prepare(self) -> None: - """runs the clipping and binning (in that order), populates clippedData and binnedData""" - self.clip() - self.omit() - if self.nbins != 0: - self.reBin() - else: - self.binnedData = self.clippedData.copy() - self.linkMeasData() - - def store(self, filename: Path, path: Optional[PurePosixPath] = None) -> None: - """stores the settings in an output file (HDF5)""" - if path is None: - path = self.resultIndex.nxsEntryPoint / "mcdata" - print(f"storing in {filename} at {path}") - pairs = [(key, getattr(self, key, None)) for key in self.storeKeys] - if pairs is None: - print("I don't understand, there's supposed to be a list of pairs here.. ") - if pairs is not None: - storeKVPairs(filename=filename, path=path, pairs=pairs) - - def load(self, filename: Path, path: Optional[PurePosixPath] = None) -> None: - # this loads the data from a prior McSAS run. - if path is None: - path = self.resultIndex.nxsEntryPoint / "mcdata" - for key, datatype in self.loadKeys.items(): - value = loadKV(filename, path / key, datatype=datatype, default=None, dbg=True) - if key == "csvargs": - self.csvargs.update(value) - else: - if value is not None: - setattr(self, key, value) - # load rawData if availalbe in the result file - try: - self.rawData = pandas.DataFrame( - data=loadKV(filename, path / "rawData", datatype="dict") - ) - except AttributeError: - logging.warning( - f"could not load rawData from {filename=}. Are you sure this is a prior McSAS run? " - "Attempting to load original data...." - ) - self.from_file() # try loading the data from the original file - self.prepare() diff --git a/src/mcsas3/mc_data_1d.py b/src/mcsas3/mc_data_1d.py deleted file mode 100644 index b0b32e9..0000000 --- a/src/mcsas3/mc_data_1d.py +++ /dev/null @@ -1,238 +0,0 @@ -# src/mcsas3/mcdata_1d.py - -from pathlib import Path -from typing import Optional - -import numpy as np -import pandas - -from .mc_data import McData - - -class McData1D(McData): - """subclass for managing 1D datasets.""" - - csvargs = None # default for 1D, overwritten in subclass - dataRange = None # min-max for data range to fit - qNudge = None # nudge in case of misaligned centers. Applied to measData - omitQRanges = None # to skip or omit unwanted data ranges, for example with sharp XRD peaks - - def __init__( - self, - df: Optional[pandas.DataFrame] = None, - loadFromFile: Optional[Path] = None, - resultIndex: int = 1, - **kwargs: dict, - ) -> None: - super().__init__(loadFromFile=loadFromFile, resultIndex=resultIndex, **kwargs) - self.csvargs = { - "sep": r"\s+", - "header": None, - "names": ["Q", "I", "ISigma"], - } # default for 1D, overwritten in subclass - self.dataRange = [-np.inf, np.inf] # min-max for data range to fit - self.qNudge = 0 # nudge in case of misaligned centers. Applied to measData - self.processKwargs(**kwargs) # redo kwargs in case the reset values have been updated - - # load from dataframe if provided - if df is not None: - self.loader = "from_pandas" # TODO: need to handle this on restore state - self.from_pandas(df) - elif loadFromFile is not None: - pass # do not try loading the file, the information is already there. - elif self.filename is not None: # filename has been set - self.from_file(self.filename) - # link measData to the requested value - - def linkMeasData(self, measDataLink: Optional[str] = None) -> None: # measDataLink:str|None - if measDataLink is None: - measDataLink = self.measDataLink - assert measDataLink in [ - "rawData", - "clippedData", - "binnedData", - ], ( - f"measDataLink value: {measDataLink} not valid. Must be one of 'rawData', 'clippedData'" - " or 'binnedData'" - ) - measDataObj = getattr(self, measDataLink) - self.measData = dict( - Q=[measDataObj.Q.values + self.qNudge], - I=measDataObj.I.values, - ISigma=measDataObj.ISigma.values, - ) - - def from_pdh(self, filename: Path) -> None: - """reads from a PDH file, re-uses Ingo Bressler's code from the notebook example""" - assert filename is not None, "from_pdh requires an input filename of a PDH file" - skiprows, nrows = 5, -1 - with open(filename) as fd: - nrows = [ln for ln, line in enumerate(fd.readlines()) if line.startswith(" None: - """uses a dataframe as input, should contain 'Q', 'I', and 'ISigma'""" - assert isinstance( - df, pandas.DataFrame - ), "from_pandas requires a pandas DataFrame with 'Q', 'I', and 'ISigma'" - # maybe add a check for the keys: - assert all( - [key in df.keys() for key in ["Q", "I", "ISigma"]] - ), "from_pandas requires the dataframe to contain 'Q', 'I', and 'ISigma'" - assert all( - [df[key].dtype.kind in "f" for key in ["Q", "I", "ISigma"]] - ), "data could not be read correctly. If csv, did you supply the right csvargs?" - self.rawData = df - self.prepare() - - def from_csv(self, filename: Path, csvargs: dict = {}) -> None: - """reads from a three-column csv file, takes pandas from_csv arguments""" - assert filename is not None, "from_csv requires an input filename of a csv file" - localCsvargs = self.csvargs.copy() - localCsvargs.update(csvargs) - self.from_pandas(pandas.read_csv(filename, **localCsvargs)) - - def clip(self) -> None: - self.clippedData = ( - self.rawData.query(f"{self.dataRange[0]} <= Q < {self.dataRange[1]}").dropna().copy() - ) - assert len(self.clippedData) != 0, "Data clipping range too small, no datapoints found!" - - def omit(self) -> None: - """This can skip/omit unwanted ranges of data (for example a data range with an unwanted - XRD peak in it). Requires an "omitQRanges" list of [[qmin, qmax]]-data ranges to omit. - """ - - # nothng to do: - if self.omitQRanges is None: - return - assert isinstance(self.omitQRanges, list), "omitQRanges must be a list" - for omitQRange in self.omitQRanges: - assert ( - len(omitQRange) == 2 - ), "each omitQRange must contain two elements: a minimum and maximum value" - # we drop the matches: - self.clippedData.drop( - self.clippedData.query(f"{omitQRange[0]} <= Q < {omitQRange[1]}").index, - inplace=True, - ) - - def reBin( - self, nbins: Optional[int] = None, IEmin: Optional[float] = None, QEMin: float = 0.01 - ) -> None: - """Unweighted rebinning funcionality with extended uncertainty estimation, - adapted from the datamerge methods, as implemented in Paulina's notebook of spring 2020 - """ - if nbins is None: - nbins = self.nbins - - if IEmin is None: - IEmin = self.IEmin - - qMin = self.clippedData.Q.dropna().min() - qMax = self.clippedData.Q.dropna().max() - - # prepare bin edges: - binEdges = np.logspace(np.log10(qMin), np.log10(qMax), num=nbins + 1) - binDat = pandas.DataFrame( - data={ - "Q": np.full(nbins, np.nan), # mean Q - "I": np.full(nbins, np.nan), # mean intensity - "IStd": np.full(nbins, np.nan), # standard deviation of the mean intensity - "ISEM": np.full( - nbins, np.nan - ), # standard error on mean of the mean intensity (maybe, but weighted is hard.) - "IError": np.full(nbins, np.nan), # Propagated errors of the intensity - "ISigma": np.full(nbins, np.nan), # Combined error estimate of the intensity - "QStd": np.full(nbins, np.nan), # standard deviation of the mean Q - "QSEM": np.full(nbins, np.nan), # standard error on the mean Q - "QError": np.full(nbins, np.nan), # Propagated errors on the mean Q - "QSigma": np.full(nbins, np.nan), # Combined error estimate on the mean Q - } - ) - - # add a little to the end to ensure the last datapoint is captured: - binEdges[-1] = binEdges[-1] + 1e-3 * (binEdges[-1] - binEdges[-2]) - - # now do the binning per bin. - for binN in range(len(binEdges) - 1): - dfRange = self.clippedData.query( - "{} <= Q < {}".format(binEdges[binN], binEdges[binN + 1]) - ).copy() - if len(dfRange) == 0: - # no datapoints in the range - pass - - elif len(dfRange) == 1: - # only one datapoint in the range - # might not be necessary to do this.. - # can't do stats on this: - # FutureWarning fix: - binDat.loc[binN, "Q"] = float(dfRange.Q.iloc[0]) - binDat.loc[binN, "QStd"] = binDat.loc[binN, "Q"] * QEMin - binDat.loc[binN, "QSEM"] = binDat.loc[binN, "Q"] * QEMin - binDat.loc[binN, "QError"] = binDat.loc[binN, "Q"] * QEMin - - binDat.loc[binN, "I"] = float(dfRange.I.iloc[0]) - binDat.loc[binN, "IStd"] = float(dfRange.ISigma.iloc[0]) - binDat.loc[binN, "ISEM"] = float(dfRange.ISigma.iloc[0]) - binDat.loc[binN, "IError"] = float(dfRange.ISigma.iloc[0]) - binDat.loc[binN, "ISigma"] = np.max( - [binDat.loc[binN, "ISEM"], float(dfRange.I.iloc[0]) * IEmin] - ) - - if "QSigma" in dfRange.keys(): - binDat.loc[binN, "QError"] = float(dfRange.QSigma.iloc[0]) - binDat.loc[binN, "QStd"] = float(dfRange.QSigma.iloc[0]) - binDat.loc[binN, "QSEM"] = float(dfRange.QSigma.iloc[0]) - - binDat.loc[binN, "QSigma"] = np.max( - [ - binDat.loc[binN, "QSEM"], - binDat.loc[binN, "QError"], - binDat.loc[binN, "Q"] * QEMin, - ] - ) - - # binDat.QSigma.loc[binN] = np.max( - # [float(binDat.QSEM.loc[binN]), float(dfRange.Q.iloc[0]) * QEMin] - # ) - - else: - # multiple datapoints in the range - # fixing FutureWarning - binDat.loc[binN, "I"] = dfRange.I.mean(skipna=True) - binDat.loc[binN, "IStd"] = dfRange.I.std(ddof=1, skipna=True) - binDat.loc[binN, "ISEM"] = dfRange.I.sem(ddof=1, skipna=True) - binDat.loc[binN, "IError"] = np.sqrt(((dfRange.ISigma) ** 2).sum()) / len(dfRange) - binDat.loc[binN, "ISigma"] = np.max( - [ - binDat.loc[binN, "ISEM"], - binDat.loc[binN, "IError"], - binDat.loc[binN, "I"] * IEmin, - ] - ) - - binDat.loc[binN, "Q"] = dfRange.Q.mean(skipna=True) - binDat.loc[binN, "QStd"] = dfRange.Q.std(ddof=1, skipna=True) - binDat.loc[binN, "QSEM"] = dfRange.Q.sem(ddof=1, skipna=True) - binDat.loc[binN, "QError"] = binDat.loc[binN, "Q"] * QEMin - - if "QSigma" in dfRange.keys(): - binDat.loc[binN, "QError"] = np.sqrt(((dfRange.QSigma) ** 2).sum()) / len( - dfRange - ) - - binDat.loc[binN, "QSigma"] = np.max( - [ - binDat.loc[binN, "QSEM"], - binDat.loc[binN, "QError"], - binDat.loc[binN, "Q"] * QEMin, - ] - ) - - # remove empty bins - binDat.dropna(thresh=4, inplace=True) - self.binnedData = binDat diff --git a/src/mcsas3/mc_data_2d.py b/src/mcsas3/mc_data_2d.py deleted file mode 100644 index c1f4842..0000000 --- a/src/mcsas3/mc_data_2d.py +++ /dev/null @@ -1,162 +0,0 @@ -# src/mcsas3/mcdata_2d.py - -import logging -from pathlib import Path -from typing import Optional - -import numpy as np -import pandas - -from .mc_data import McData - - -# @define -class McData2D(McData): - """Subclass for managing 2D datasets. - Copied from 1D dataset handler, not every functionality is enabled""" - - csvargs: dict = { - "sep": r"\s+", - "header": None, - "names": ["Q", "I", "ISigma"], - } # default for 1D, overwritten in subclass - dataRange = [0, np.inf] # min-max for data range to fit - orthoQ1Range = [0, np.inf] # min-max for abs(Qx) in case of square masking - orthoQ0Range = [0, np.inf] # min-max for abs(Qy) in case of square masking - qNudge = [ - 0, - 0, - ] # nudge in direction 0 and 1 in case of misaligned centers. Applied to measData - - def __init__(self, df=None, loadFromFile=None, resultIndex: int = 1, **kwargs: dict) -> None: - super().__init__(resultIndex=resultIndex, **kwargs) - self.csvargs = ( - {} - ) # not sure you'd want to load 2D from a CSV.... though I've seen stranger things - self.dataRange = [0, np.inf] # min-max for data range to fit - self.orthoQ1Range = [0, np.inf] - self.orthoQ0Range = [0, np.inf] - self.qNudge = [0, 0] # nudge in case of misaligned centers. Applied to measData - self.processKwargs(**kwargs) - - # load from dataframe if provided - if df is not None: - self.loader = "from_pandas" # TODO: need to handle this on restore state - self.from_pandas(df) - - # TODO not sure why loadFromFile is not used.. - elif self.filename is not None: # filename has been set - self.from_file(self.filename) - # link measData to the requested value - - def linkMeasData(self, measDataLink: Optional[str] = None) -> None: - if measDataLink is None: - measDataLink = self.measDataLink - assert measDataLink in [ - "rawData", - "clippedData", - "binnedData", - ], ( - f"measDataLink value: {measDataLink} not valid. Must be one of 'rawData', 'clippedData'" - " or 'binnedData'" - ) - measDataObj = getattr(self, measDataLink) - self.measData = dict( - Q=[ - measDataObj["Q"][0] + self.qNudge[0], - measDataObj["Q"][1] + self.qNudge[1], - ], - I=measDataObj["I"], - ISigma=measDataObj["ISigma"], - ) - - def from_pandas(self, df: pandas.DataFrame = None) -> None: - assert False, "2D data from_pandas not implemented yet" - pass - - def from_csv(self, filename: Path, csvargs: dict = {}) -> None: - assert False, "2D data from_csv not implemented yet" - pass - - def clip(self) -> None: - # copied from a jupyter notebook: - # test with directly imported data - Int = self.rawData2D["I"] - ISigma = self.rawData2D["ISigma"] - Q1 = self.rawData2D["Qx"] - Q0 = self.rawData2D["Qy"] - if "mask" in self.rawData2D.keys(): - mask = self.rawData2D["mask"] - else: - mask = np.zeros(Int.shape) - newMask = mask.astype(bool) - - withinLimits = ( - (np.abs(Q1) > self.orthoQ1Range[0]) - & (np.abs(Q1) < self.orthoQ1Range[1]) - & (np.abs(Q0) > self.orthoQ0Range[0]) - & (np.abs(Q0) < self.orthoQ0Range[1]) - & (np.sqrt(Q1**2 + Q0**2) > self.dataRange[0]) - & (np.sqrt(Q1**2 + Q0**2) < self.dataRange[1]) - ).astype(bool) * np.invert(newMask) - - # find crop envelope: - Q0Lim = ( - np.argwhere(withinLimits.sum(axis=1) > 0).min(), - np.argwhere(withinLimits.sum(axis=1) > 0).max(), - ) - Q1Lim = ( - np.argwhere(withinLimits.sum(axis=0) > 0).min(), - np.argwhere(withinLimits.sum(axis=0) > 0).max(), - ) - assert Q0Lim[0] < Q0Lim[1], "Could not determine valid crop limits for axis 0 (y)" - assert Q1Lim[0] < Q1Lim[1], "Could not determine valid crop limits for axis 1 (x)" - - # a0l, a0h, a1l, a1h = 200, 600, 300, 700 - self.clippedData = dict() - self.clippedData["I2D"] = Int[Q0Lim[0] : Q0Lim[1], Q1Lim[0] : Q1Lim[1]] - self.clippedData["mask2D"] = newMask[Q0Lim[0] : Q0Lim[1], Q1Lim[0] : Q1Lim[1]] - self.clippedData["ISigma2D"] = ISigma[Q0Lim[0] : Q0Lim[1], Q1Lim[0] : Q1Lim[1]] - self.clippedData["Q0Crop2D"] = Q0[Q0Lim[0] : Q0Lim[1], Q1Lim[0] : Q1Lim[1]] - self.clippedData["Q1Crop2D"] = Q1[Q0Lim[0] : Q0Lim[1], Q1Lim[0] : Q1Lim[1]] - - self.clippedData["kansas"] = self.clippedData["I2D"].shape - # remove infinite intensities and zero-uncertainty datapoints as well (add to mask): - bArr = np.invert((np.isinf(self.clippedData["I2D"]) | (self.clippedData["ISigma2D"] == 0))) - self.clippedData["invMask"] = bArr * np.invert(self.clippedData["mask2D"]).astype(bool) - - self.clippedData["I"] = (self.clippedData["I2D"][self.clippedData["invMask"]]).flatten() - self.clippedData["ISigma"] = ( - self.clippedData["ISigma2D"][self.clippedData["invMask"]] - ).flatten() - self.clippedData["Q"] = [ - self.clippedData["Q0Crop2D"][self.clippedData["invMask"]].flatten(), - self.clippedData["Q1Crop2D"][self.clippedData["invMask"]].flatten(), - ] - - self.clippedData["Qextent"] = [ - (self.clippedData["Q"][0]).min(), - (self.clippedData["Q"][0]).max(), - (self.clippedData["Q"][1]).min(), - (self.clippedData["Q"][1]).max(), - ] - - def omit(self) -> None: - """This can skip/omit unwanted ranges of data (for example a data range with an unwanted - XRD peak in it). Requires an "omitQRanges" list of [[qmin, qmax]]-data ranges to omit.""" - logging.warning("Omitting ranges not implemented yet for 2D") - pass - - def reconstruct2D(self, modelI1D: np.ndarray) -> np.ndarray: - """Reconstructs a masked 2D data array from the (1D) model intensity, skipping the masked - and clipped pixels (left as NaN). This function can be used to plot the resulting model - intensity and comparing it with self.clippedData["I2D"]. - """ - # RMI = reconstructedModelI - RMI = np.full(self.clippedData["I2D"].shape, np.nan) - RMI[np.where(self.clippedData["invMask"])] = modelI1D - return RMI - - def reBin(self, nbins: Optional[int] = None, IEmin: float = 0.01, QEMin: float = 0.01) -> None: - print("2D data rebinning not implemented, binnedData = clippedData for now") - self.binnedData = self.clippedData diff --git a/src/mcsas3/mc_hat.py b/src/mcsas3/mc_hat.py index 864be0b..6fcd4fb 100644 --- a/src/mcsas3/mc_hat.py +++ b/src/mcsas3/mc_hat.py @@ -1,34 +1,66 @@ # src/mcsas3/mc_hat.py +import logging import sys +import threading import time from io import StringIO from pathlib import Path, PurePosixPath -from typing import Optional +from typing import Any, Optional import numpy as np from mcsas3.mc_hdf import ResultIndex, loadKVPairs, storeKVPairs +from .data_adapters import as_analysis_bundle, q_support_from_bundle from .mc_core import McCore from .mc_model import McModel from .mc_opt import McOpt STORE_LOCK = None +STOP_EVENT = None +logger = logging.getLogger(__name__) -def initStoreLock(lock): - global STORE_LOCK +def initWorkerState(lock, stop_event): + """Initialize multiprocessing worker globals for synchronized store/stop handling.""" + + global STORE_LOCK, STOP_EVENT STORE_LOCK = lock + STOP_EVENT = stop_event + + +def worker_stop_requested() -> bool: + """Return whether the process-shared stop event has been set for a worker.""" + + return STOP_EVENT is not None and STOP_EVENT.is_set() + + +def _attach_buffer_log_handler(output_buffer: StringIO) -> tuple[logging.Logger, logging.Handler, int, bool]: + """Attach a temporary log handler for buffered worker output capture.""" + + logger_namespace = logging.getLogger("mcsas3") + handler = logging.StreamHandler(output_buffer) + handler.setLevel(logging.INFO) + handler.setFormatter(logging.Formatter("%(message)s")) + previous_level = logger_namespace.level + previous_propagate = logger_namespace.propagate + logger_namespace.addHandler(handler) + logger_namespace.setLevel(logging.INFO) + logger_namespace.propagate = False + return logger_namespace, handler, previous_level, previous_propagate # TODO: use attrs to @define a mchatataclass class McHat: """ - The hat sits on top of the McCore. It takes care of parallel processing of each repetition. + The hat sits on top of `McCore` and orchestrates repeated optimization runs. + + Preferred measurement input is the canonical selected-analysis `DataBundle`. + `OptimizerInput` remains supported as an execution-format escape hatch. """ - _measData = None # measurement data dict with entries for Q, I, ISigma + _analysisBundle = None # canonical bundle selected for fitting, when available _modelArgs = None # dict with settings to be passed on to the model instance _optArgs = None # dict with optimization settings to be passed on to the optimization instance _model = None # McModel instance for multiple repetitions @@ -36,6 +68,10 @@ class McHat: nCores = 0 # number of cores to use for parallelization, # 0: autodetect, 1: without multiprocessing nRep = 10 # number of independent repetitions to opitimize + _stopEvent = None # thread-local stop signal for this McHat instance + _processStopEvent = None # process-shared stop signal for active worker pool + _runActive = False # whether run() is currently active + lastRunStopped = False # whether the last run ended due to a stop request storeKeys = [ # keys to store in an output file "nCores", @@ -43,20 +79,20 @@ class McHat: ] loadKeys = storeKeys - def __init__( - self, loadFromFile: Optional[Path] = None, resultIndex: int = 1, **kwargs: dict - ) -> None: + def __init__(self, loadFromFile: Optional[Path] = None, resultIndex: int = 1, **kwargs: dict) -> None: # reset to make sure we're not inheriting any settings from another instance: - self._measData = None # measurement data dict with entries for Q, I, ISigma + self._analysisBundle = None # canonical bundle selected for fitting, when available self._modelArgs = None # dict with settings to be passed on to the model instance - self._optArgs = ( - None # dict with optimization settings to be passed on to the optimization instance - ) + self._optArgs = None # dict with optimization settings to be passed on to the optimization instance self._model = None # McModel instance for multiple repetitions self._opt = None # McOpt instance for multiple repetitions self.nCores = 0 # number of cores to use for parallelization, # 0: autodetect, 1: without multiprocessing self.nRep = 10 # number of independent repetitions to opitimize + self._stopEvent = threading.Event() + self._processStopEvent = None + self._runActive = False + self.lastRunStopped = False """kwargs accepts all parameters from McModel and McOpt.""" # make sure we store and read from the right place. @@ -67,113 +103,222 @@ def __init__( self._optArgs = dict([(key, kwargs.pop(key)) for key in McOpt.storeKeys if key in kwargs]) self._optArgs.update({"resultIndex": resultIndex}) - self._modelArgs = dict( - [(key, kwargs.pop(key)) for key in McModel.settables if key in kwargs] - ) + self._modelArgs = dict([(key, kwargs.pop(key)) for key in McModel.settables if key in kwargs]) self._modelArgs.update({"resultIndex": resultIndex}) for key, value in kwargs.items(): - assert key in self.storeKeys, "Key {} is not a valid option".format(key) + if key not in self.storeKeys: + raise ValueError(f"Key {key} is not a valid option") setattr(self, key, value) - assert self.nRep > 0, "Must optimize for at least one repetition" + if self.nRep <= 0: + raise ValueError("Must optimize for at least one repetition.") + + def __getstate__(self) -> dict[str, Any]: + state = self.__dict__.copy() + state["_stopEvent"] = None + state["_processStopEvent"] = None + state["_runActive"] = False + return state + + def __setstate__(self, state: dict[str, Any]) -> None: + self.__dict__.update(state) + if self._stopEvent is None: + self._stopEvent = threading.Event() + self._processStopEvent = None + + @property + def isRunning(self) -> bool: + """Return whether `run()` is currently active on this instance.""" + + return self._runActive + + def request_stop(self) -> None: + """Request that the active run stop as soon as practical.""" + + self._stopEvent.set() + if self._processStopEvent is not None: + self._processStopEvent.set() + + def clear_stop_request(self) -> None: + """Clear any previously requested stop flags before a new run starts.""" + + self._stopEvent.clear() + if self._processStopEvent is not None: + self._processStopEvent.clear() - def fillFitParameterLimits(self, measData: dict) -> None: + def stop_requested(self) -> bool: + """Return whether a local or process-shared stop has been requested.""" + + return self._stopEvent.is_set() or (self._processStopEvent is not None and self._processStopEvent.is_set()) + + def fillFitParameterLimits(self, analysis_input: Any) -> None: + """Resolve any `auto` fit parameter limits against the supplied measurement support.""" + + try: + q_support = q_support_from_bundle(as_analysis_bundle(analysis_input)) + except TypeError: + from .optimizer_input import as_optimizer_input + + q_support = as_optimizer_input(analysis_input).q_support for key, val in self._modelArgs["fitParameterLimits"].items(): if isinstance(val, str): - assert val == "auto", ( - "Only fit parameter options are either providing [min, max] limits or setting" - ' to "auto"' - ) + if val != "auto": + raise ValueError('Fit parameter limits must be explicit [min, max] pairs or the string "auto".') # auto-fill values - assert ( - np.min(measData["Q"]) > 0 - ), "for auto-scaling of measurement limits, the smallest Q value cannot be zero" + if np.min(q_support) <= 0: + raise ValueError("For auto-scaling of measurement limits, the smallest Q value must be > 0.") self._modelArgs["fitParameterLimits"][key] = [ - np.pi / np.max(measData["Q"]), - np.pi / np.min(measData["Q"]), + np.pi / np.max(q_support), + np.pi / np.min(q_support), ] - def run(self, measData: dict, filename: Path, resultIndex: int = 1) -> None: - """runs the full sequence: multiple repetitions of optimizations, to be parallelized. - This probably needs to be taken out of core, and into a new parent""" - - # ensure the fit parameter limits are filled in based on the data limits if auto - self.fillFitParameterLimits(measData) - - if (self.nCores == 1) or (self.nRep == 1): - for rep in range(self.nRep): - self.runOnce(measData, filename, rep, resultIndex=resultIndex) - # elif self.nCores == 2: - # print([(measData, filename, r) for r in range(self.nRep)]) - else: - import multiprocessing - - if self.nCores == 0: - # don't run more processes than we need... - self.nCores = np.minimum(multiprocessing.cpu_count(), self.nRep) - start = time.time() - lock = multiprocessing.Lock() - pool = multiprocessing.Pool(self.nCores, initializer=initStoreLock, initargs=(lock,)) - runArgs = [(measData, filename, r, True, resultIndex) for r in range(self.nRep)] - outputs = pool.starmap(self.runOnce, runArgs) - pool.close() - pool.join() - print( - "McSAS analysis with {} repetitions took {:.1f}s with {} threads.".format( - self.nRep, time.time() - start, min(self.nCores, self.nRep) + def run(self, analysis_input: Any, filename: Path, resultIndex: int = 1) -> None: + """Run all configured repetitions, optionally in parallel, and store completed results.""" + + self.clear_stop_request() + self.lastRunStopped = False + self._runActive = True + try: + try: + resolved_input = as_analysis_bundle(analysis_input) + self._analysisBundle = resolved_input + except TypeError: + resolved_input = analysis_input + self._analysisBundle = None + # ensure the fit parameter limits are filled in based on the data limits if auto + self.fillFitParameterLimits(resolved_input) + if (self.nCores == 1) or (self.nRep == 1): + for rep in range(self.nRep): + if self.stop_requested(): + break + self.runOnce(resolved_input, filename, rep, resultIndex=resultIndex) + # elif self.nCores == 2: + # print([(analysis_input, filename, r) for r in range(self.nRep)]) + else: + import multiprocessing + + if self.nCores == 0: + # don't run more processes than we need... + self.nCores = np.minimum(multiprocessing.cpu_count(), self.nRep) + start = time.time() + lock = multiprocessing.Lock() + self._processStopEvent = multiprocessing.Event() + pool = multiprocessing.Pool( + self.nCores, + initializer=initWorkerState, + initargs=(lock, self._processStopEvent), ) - ) - # for args in runArgs: - # buf = args[-1] - # print(buf, buf.getvalue()) # last argument is stdio buffer - for output in sorted(outputs, key=lambda x: x[0]): - print(output) + runArgs = [(resolved_input, filename, r, True, resultIndex) for r in range(self.nRep)] + async_result = pool.starmap_async(self.runOnce, runArgs) + outputs = None + while outputs is None: + try: + outputs = async_result.get(timeout=0.2) + except multiprocessing.TimeoutError: + continue + pool.close() + pool.join() + logger.info( + "McSAS analysis with %s repetitions took %.1fs with %s threads.", + self.nRep, + time.time() - start, + min(self.nCores, self.nRep), + ) + for repetition, output, _completed in sorted(outputs, key=lambda value: value[0]): + if output: + logger.info("%s", output.rstrip()) + finally: + self.lastRunStopped = self.stop_requested() + self._runActive = False + self._processStopEvent = None def runOnce( self, - measData: dict, + analysis_input: Any, filename: Path, repetition: int = 0, bufferStdIO: bool = False, resultIndex: int = 1, - ) -> None: - """runs the full sequence: multiple repetitions of optimizations, to be parallelized. - This probably needs to be taken out of core, and into a new parent""" + ) -> tuple[int, str, bool] | None: + """Run a single optimization repetition and optionally return buffered worker output.""" + original_stdout = None + original_stderr = None + output_buffer = None + buffer_logger = None + buffer_handler = None + buffer_logger_level = logging.NOTSET + buffer_logger_propagate = True + completed = False if bufferStdIO: # buffer stdout/err in an individual StringIO object for each repetition - sys.stderr = sys.stdout = StringIO() + output_buffer = StringIO() + buffer_logger, buffer_handler, buffer_logger_level, buffer_logger_propagate = _attach_buffer_log_handler( + output_buffer + ) + original_stdout = sys.stdout + original_stderr = sys.stderr + sys.stderr = sys.stdout = output_buffer if self._opt is None: self._opt = McOpt(**self._optArgs) if self._model is None: self._model = McModel(**self._modelArgs) self._opt.repetition = repetition + base_seed = self._modelArgs.get("seed") + if base_seed is not None: + try: + effective_seed = int(base_seed) + int(repetition) + except (TypeError, ValueError) as exc: + raise ValueError(f"Configured seed must be an integer-compatible value, got {base_seed!r}.") from exc + self._model.seed = effective_seed + self._model.randomGenerators = None + self._model._initialize_random_generators() self._model.resetParameterSet() - mc = McCore(measData, model=self._model, opt=self._opt, resultIndex=resultIndex) - mc.optimize() - try: - self._model.kernel.release() - except AttributeError: - pass # can happen with a simulation model - except Exception as e: - print(f"{mc}: {e}: {str(e)}\n") - print("Final chiSqr: {}, N accepted: {}".format(self._opt.gof, self._opt.accepted)) - - # storing the results - if STORE_LOCK is not None: - # prevent multiple threads writing HDF5 file simultaneously - STORE_LOCK.acquire() try: - mc.store(filename=filename) - self.store(filename=filename) - except Exception as e: - print(f"{mc}: {e}: {str(e)}\n") + stop_callback = worker_stop_requested if bufferStdIO else self.stop_requested + mc = McCore( + analysis_input, + model=self._model, + opt=self._opt, + resultIndex=resultIndex, + stop_requested=stop_callback, + ) + completed = mc.optimize() + try: + self._model.kernel.release() + except AttributeError: + pass # can happen with a simulation model + except Exception as e: + logger.warning("%s: %s", mc, e) + if completed: + logger.info("Final chiSqr: %s, N accepted: %s", self._opt.gof, self._opt.accepted) + if STORE_LOCK is not None: + # prevent multiple threads writing HDF5 file simultaneously + STORE_LOCK.acquire() + try: + mc.store(filename=filename) + self.store(filename=filename) + except Exception as e: + logger.warning("%s: %s", mc, e) + finally: + if STORE_LOCK is not None: + STORE_LOCK.release() + else: + logger.info("Optimization of repetition %s stopped before completion.", repetition) finally: - if STORE_LOCK is not None: - STORE_LOCK.release() + if bufferStdIO and buffer_logger is not None and buffer_handler is not None: + buffer_logger.removeHandler(buffer_handler) + buffer_handler.close() + buffer_logger.setLevel(buffer_logger_level) + buffer_logger.propagate = buffer_logger_propagate + if bufferStdIO and original_stdout is not None and original_stderr is not None: + sys.stdout = original_stdout + sys.stderr = original_stderr if bufferStdIO: # return buffered output if desired - return sys.stdout.getvalue() + if output_buffer is None: + raise RuntimeError("Buffered output was requested but no output buffer was initialized.") + return repetition, output_buffer.getvalue(), completed return # same as in McOpt @@ -185,6 +330,8 @@ def store(self, filename: Path, path: Optional[PurePosixPath] = None) -> None: # same as in McOpt, except for the repetition (in McOpt) def load(self, filename: Path, path: Optional[PurePosixPath] = None) -> None: + """Load orchestrator settings from the result HDF5 file.""" + if path is None: path = self.resultIndex.nxsEntryPoint / "optimization" for key, value in loadKVPairs(filename, path, self.loadKeys): diff --git a/src/mcsas3/mc_hdf.py b/src/mcsas3/mc_hdf.py index 19db19a..24e5b65 100644 --- a/src/mcsas3/mc_hdf.py +++ b/src/mcsas3/mc_hdf.py @@ -1,4 +1,5 @@ import inspect +import logging from collections.abc import Iterable from pathlib import Path, PurePosixPath @@ -9,6 +10,14 @@ import pint from attrs import validators +from .data_model import BaseData, DataBundle, ProcessingData + +PROCESSING_DATA_GROUP = "processingData" +PROCESSING_DATA_SCHEMA = "mcsas3.processing_data" +PROCESSING_DATA_SCHEMA_VERSION = 1 + +logger = logging.getLogger(__name__) + @attrs.define class ResultIndex(object): @@ -24,8 +33,8 @@ class ResultIndex(object): ], ) - def __attrs_post_init__(self, resultIndex: int = 1): - self.resultIndex = resultIndex + def __attrs_post_init__(self): + self.resultIndex = int(self.resultIndex) @property def nxsEntryPoint(self): @@ -34,8 +43,10 @@ def nxsEntryPoint(self): def loadKVPairs(filename: Path, path: PurePosixPath, keys: Iterable) -> Iterable: """Load key-value pairs from HDF5 file""" - assert filename is not None - assert path is not None + if filename is None: + raise ValueError("filename cannot be empty") + if path is None: + raise ValueError("HDF5 path cannot be empty") with h5py.File(filename, "r") as h5f: for key in keys: yield key, h5f[str(path / key)][()] @@ -45,7 +56,9 @@ def loadKV(filename: Path, path: PurePosixPath, datatype=None, default=None, dbg """Load a single key-value pair from HDF5 file""" path = str(path) if dbg: - print(f"loadKV({path})") + logger.debug("loadKV(%s)", path) + if not Path(filename).is_file(): + return default with h5py.File(filename, "r") as h5f: if path not in h5f: return default @@ -84,27 +97,30 @@ def loadKV(filename: Path, path: PurePosixPath, datatype=None, default=None, dbg ) value = pandas.DataFrame(data=vals, columns=cols, index=idx) value.columns = [ - (colname.decode("utf8") if isinstance(colname, bytes) else colname) - for colname in value.columns + (colname.decode("utf8") if isinstance(colname, bytes) else colname) for colname in value.columns ] return value def storeKVPairs(filename: Path, path: PurePosixPath, pairs: Iterable) -> None: - assert filename is not None - assert path is not None + if filename is None: + raise ValueError("filename cannot be empty") + if path is None: + raise ValueError("HDF5 path cannot be empty") try: for key, value in pairs: storeKV(filename=filename, path=path / key, value=value) except Exception: - print(f"Error for path {key} and value '{value}' of type {type(value)}.") + logger.exception("Error storing HDF5 value for %s of type %s.", path / key, type(value)) raise def storeKV(filename: Path, path: PurePosixPath, value=None) -> None: - assert filename is not None, "filename (output filename) cannot be empty" - assert path is not None, "HDF5 path cannot be empty" + if filename is None: + raise ValueError("filename (output filename) cannot be empty") + if path is None: + raise ValueError("HDF5 path cannot be empty") if isinstance(value, (dict, pandas.DataFrame)): storeKVPairs(filename, path, value.items()) @@ -141,3 +157,124 @@ def storeKV(filename: Path, path: PurePosixPath, value=None) -> None: if unit is not None: dset.attrs["unit"] = str(unit) + + +def _decode_hdf_value(value): + if isinstance(value, (bytes, bytearray, np.bytes_)): + return value.decode() + if isinstance(value, np.ndarray) and value.shape == (): + return _decode_hdf_value(value[()]) + return value + + +def _require_clean_group(h5f: h5py.File, path: PurePosixPath | str) -> h5py.Group: + path_str = str(path) + if path_str in h5f: + del h5f[path_str] + return h5f.require_group(path_str) + + +def _store_basedata_group(group: h5py.Group, basedata: BaseData) -> None: + group.attrs["units"] = str(basedata.units) + group.attrs["rank_of_data"] = int(basedata.rank_of_data) + group.create_dataset("signal", data=np.array(basedata.signal, copy=True)) + group.create_dataset("weights", data=np.array(basedata.weights, copy=True)) + uncertainties_group = group.create_group("uncertainties") + for key, uncertainty in basedata.uncertainties.items(): + uncertainties_group.create_dataset(key, data=np.array(uncertainty, copy=True)) + + +def _load_basedata_group(group: h5py.Group) -> BaseData: + uncertainties = {} + if "uncertainties" in group: + uncertainties = {key: np.array(dataset[()], copy=True) for key, dataset in group["uncertainties"].items()} + + return BaseData( + signal=np.array(group["signal"][()], copy=True), + units=str(_decode_hdf_value(group.attrs.get("units", "dimensionless"))), + uncertainties=uncertainties, + weights=np.array(group["weights"][()], copy=True) if "weights" in group else np.array(1.0), + rank_of_data=int(group.attrs.get("rank_of_data", 0)), + ) + + +def storeDataBundle(filename: Path, path: PurePosixPath, bundle: DataBundle) -> None: + with h5py.File(filename, "a") as h5f: + group = _require_clean_group(h5f, path) + if getattr(bundle, "default_plot", None) is not None: + group.attrs["default_plot"] = str(bundle.default_plot) + if getattr(bundle, "description", None) is not None: + group.attrs["description"] = str(bundle.description) + for key, basedata in bundle.items(): + basedata_group = group.create_group(key) + _store_basedata_group(basedata_group, basedata) + + +def loadDataBundle(filename: Path, path: PurePosixPath, default=None): + if not Path(filename).is_file(): + return default + + with h5py.File(filename, "r") as h5f: + path_str = str(path) + if path_str not in h5f: + return default + + group = h5f[path_str] + bundle = DataBundle() + if "default_plot" in group.attrs: + bundle.default_plot = str(_decode_hdf_value(group.attrs["default_plot"])) + if "description" in group.attrs: + bundle.description = str(_decode_hdf_value(group.attrs["description"])) + for key, value in group.items(): + if isinstance(value, h5py.Group): + bundle[key] = _load_basedata_group(value) + return bundle + + +def storeProcessingData(filename: Path, path: PurePosixPath, processing: ProcessingData) -> None: + with h5py.File(filename, "a") as h5f: + group = _require_clean_group(h5f, path) + group.attrs["schema"] = PROCESSING_DATA_SCHEMA + group.attrs["schema_version"] = PROCESSING_DATA_SCHEMA_VERSION + analysis_stage = getattr(processing, "analysis_stage", None) + if analysis_stage is not None: + group.attrs["analysis_stage"] = str(analysis_stage) + for stage_name, bundle in processing.items(): + stage_group = group.create_group(stage_name) + if getattr(bundle, "default_plot", None) is not None: + stage_group.attrs["default_plot"] = str(bundle.default_plot) + if getattr(bundle, "description", None) is not None: + stage_group.attrs["description"] = str(bundle.description) + for key, basedata in bundle.items(): + basedata_group = stage_group.create_group(key) + _store_basedata_group(basedata_group, basedata) + + +def loadProcessingData(filename: Path, path: PurePosixPath, default=None): + if not Path(filename).is_file(): + return default + + with h5py.File(filename, "r") as h5f: + path_str = str(path) + if path_str not in h5f: + return default + + group = h5f[path_str] + processing = ProcessingData() + if "analysis_stage" in group.attrs: + setattr(processing, "analysis_stage", str(_decode_hdf_value(group.attrs["analysis_stage"]))) + + for stage_name, stage_group in group.items(): + if not isinstance(stage_group, h5py.Group): + continue + bundle = DataBundle() + if "default_plot" in stage_group.attrs: + bundle.default_plot = str(_decode_hdf_value(stage_group.attrs["default_plot"])) + if "description" in stage_group.attrs: + bundle.description = str(_decode_hdf_value(stage_group.attrs["description"])) + for key, basedata_group in stage_group.items(): + if isinstance(basedata_group, h5py.Group): + bundle[key] = _load_basedata_group(basedata_group) + processing[stage_name] = bundle + + return processing diff --git a/src/mcsas3/mc_model.py b/src/mcsas3/mc_model.py index db806c2..c0ad63f 100644 --- a/src/mcsas3/mc_model.py +++ b/src/mcsas3/mc_model.py @@ -1,4 +1,6 @@ +import logging from pathlib import Path +from types import SimpleNamespace from typing import List, Optional, Tuple import numpy as np @@ -10,60 +12,67 @@ from mcsas3.mc_hdf import ResultIndex, loadKV, storeKV, storeKVPairs - -# TODO: perhaps better defined as a dataclass with attrs -class sphereParameters(object): - # micro-class to mimick the nested structure of SasModels in simulation model: - defaults = { - "scale": 1.0, - "background": 0.0, - "sld": 1.0e-6, - "sld_solvent": 0, - "radius": 1, - } - - def __init__(self) -> None: - pass - - -# ibid. -class sphereInfo(object): - # micro-class to mimick the nested structure of SasModels in simulation model: - parameters = sphereParameters() - - def __init__(self) -> None: - pass +logger = logging.getLogger(__name__) + +SPHERE_MODEL_DEFAULTS = { + "scale": 1.0, + "background": 0.0, + "sld": 1.0e-6, + "sld_solvent": 0, + "radius": 1, +} +SIM_MODEL_DEFAULTS = { + "extrapY0": 0, + "extrapScaling": 1, + "simDataQ0": np.array([0, 0]), + "simDataQ1": None, + "simDataI": np.array([1, 1]), + "simDataISigma": np.array([0.01, 0.01]), +} +CUSTOM_MODEL_LOADERS = { + "sim": "_load_sim_model", + "mcsas_sphere": "_load_mcsas_sphere_model", +} + + +def _copy_default_value(value): + if isinstance(value, np.ndarray): + return np.array(value, copy=True) + return value + + +def _pseudo_model_info(defaults: dict[str, object]) -> SimpleNamespace: + return SimpleNamespace( + parameters=SimpleNamespace(defaults={key: _copy_default_value(value) for key, value in defaults.items()}) + ) + + +def _require_valid_settable_keys(kwargs: dict, allowed_keys: list[str]) -> None: + for key in kwargs: + if key not in allowed_keys: + raise ValueError( + "Key '{}' is not a valid settable option. Valid options are: \n {}".format(key, allowed_keys) + ) -class mcsasSphereModel(object): +class mcsasSphereModel: """pretends to be a sasmodel, but just for a sphere - in case sasmodels give gcc errors""" - sld = None - sld_solvent = None - radius = None - # scale = None - # background = None - settables = ["sld", "sld_solvent", "radius", "scale", "background"] - measQ = None # needs to be set later when initializing - info = sphereInfo() + settables = ("sld", "sld_solvent", "radius", "scale", "background") def __init__(self, **kwargs: dict) -> None: # reset values to make sure we're not inheriting anything from another instance: - self.sld = 1 # input SLD in units of 1e-6 1/A^2. - self.sld_solvent = 0 - self.radius = [] # first element of two-eleemnt Q list + self.sld = SPHERE_MODEL_DEFAULTS["sld"] # input SLD in units of 1e-6 1/A^2. + self.sld_solvent = SPHERE_MODEL_DEFAULTS["sld_solvent"] + self.radius = SPHERE_MODEL_DEFAULTS["radius"] # self.scale = None # second element of two-element Q list # self.background = [] # intensity of simulated data self.measQ = None # needs to be set later when initializing - self.info = sphereInfo() + self.info = _pseudo_model_info(SPHERE_MODEL_DEFAULTS) # overwrites settings loaded from file if specified. + _require_valid_settable_keys(kwargs, self.settables) for key, value in kwargs.items(): - assert ( - key in self.settables - ), "Key '{}' is not a valid settable option. Valid options are: \n {}".format( - key, self.settables - ) setattr(self, key, value) def make_kernel(self, measQ: np.ndarray = None): # not sure of the output type... sasmodel? @@ -85,89 +94,44 @@ def kernelfunc(self, **parDict: dict) -> Tuple[np.ndarray, np.ndarray]: return Int, V -# ibid. -class simParameters(object): - # micro-class to mimick the nested structure of SasModels in simulation model: - defaults = { - "extrapY0": 0, - "extrapScaling": 1, - "simDataQ0": np.array([0, 0]), - "simDataQ1": None, - "simDataI": np.array([1, 1]), - "simDataISigma": np.array([0.01, 0.01]), - } - - def __init__(self): - pass - - -# ibid. -class simInfo(object): - # micro-class to mimick the nested structure of SasModels in simulation model: - parameters = simParameters() - - def __init__(self): - pass - - -# ibid. -class McSimPseudoModel(object): +class McSimPseudoModel: """pretends to be a sasmodel""" - extrapY0 = None - extrapScaling = None - # simDataDict = {} # this can't be passed on in multiprocessing arguments, - # so need to pass on individual bits: - simDataQ0 = [] # first element of two-eleemnt Q list - simDataQ1 = None # second element of two-element Q list - simDataI = [] # intensity of simulated data - simDataISigma = [] # uncertainty on intensity of simulated data - settables = [ + settables = ( "extrapY0", "extrapScaling", "simDataQ0", "simDataQ1", "simDataI", "simDataISigma", - ] - Ipolator = None # interp1D instance for interpolating intensity - ISpolator = None # interp1D instance for interpolating uncertainty on intensity - measQ = None # needs to be set later when initializing - info = simInfo() + ) def __init__(self, **kwargs: dict) -> None: # reset values to make sure we're not inheriting anything from another instance: - self.extrapY0 = None - self.extrapScaling = None + self.extrapY0 = SIM_MODEL_DEFAULTS["extrapY0"] + self.extrapScaling = SIM_MODEL_DEFAULTS["extrapScaling"] # simDataDict = {} # this can't be passed on in multiprocessing arguments, # so need to pass on individual bits: - self.simDataQ0 = [] # first element of two-eleemnt Q list - self.simDataQ1 = None # second element of two-element Q list - self.simDataI = [] # intensity of simulated data - self.simDataISigma = [] # uncertainty on intensity of simulated data + self.simDataQ0 = np.array([], dtype=float) # first element of two-eleemnt Q list + self.simDataQ1 = SIM_MODEL_DEFAULTS["simDataQ1"] # second element of two-element Q list + self.simDataI = np.array([], dtype=float) # intensity of simulated data + self.simDataISigma = np.array([], dtype=float) # uncertainty on intensity of simulated data self.Ipolator = None # interp1D instance for interpolating intensity self.ISpolator = None # interp1D instance for interpolating uncertainty on intensity self.measQ = None # needs to be set later when initializing - self.info = simInfo() + self.info = _pseudo_model_info(SIM_MODEL_DEFAULTS) # overwrites settings loaded from file if specified. + _require_valid_settable_keys(kwargs, self.settables) for key, value in kwargs.items(): - assert ( - key in self.settables - ), "Key '{}' is not a valid settable option. Valid options are: \n {}".format( - key, self.settables - ) setattr(self, key, value) - # if not 'simDataDict' in kwargs.keys(): - assert all( - [ - key in kwargs.keys() - for key in ["simDataQ0", "simDataQ1", "simDataI", "simDataISigma"] - ] - ), ( - "The following input arguments must be provided to describe the simulation data:" - " simDataQ0, simDataQ1, simDataI, simDataISigma" - ) + required_sim_keys = ["simDataQ0", "simDataQ1", "simDataI", "simDataISigma"] + missing_sim_keys = [key for key in required_sim_keys if key not in kwargs] + if missing_sim_keys: + raise ValueError( + "The following input arguments must be provided to describe the simulation data: " + "simDataQ0, simDataQ1, simDataI, simDataISigma. Missing: " + ", ".join(missing_sim_keys) + ) # self.simDataDict = { # 'Q': (self.simDataQ0, self.simDataQ1), # 'I': self.simDataI, @@ -253,29 +217,7 @@ class McModel: """ - func = None # SasModels model instance - modelName = "sphere" # SasModels model name - modelDType = "fast" # model data type, choose 'fast' for single precision - kernel = object # SasModels kernel pointer - parameterSet = None # pandas dataFrame of length nContrib, with column names of parameters - staticParameters = None # dictionary of static parameter-value pairs during MC optimization - pickParameters = None # dict of values with new random picks, named by parameter names - pickIndex = None # int showing the running number of the current contribution being tested - # dict of value pairs (tuples) *for fit parameters only* with lower, upper limits for the - # random function generator, named by parameter names - fitParameterLimits = None - randomGenerators = None # dict with random value generators - # BETA: dict with boolean values, whether to apply a logarithmic - # transformation on the random generators - logRandoms = None - # BETA: whether to apply a logarithmic transformation on the random - # generators. This will change in the future to a cleaner, per-parameter config - logRandom = False - volumes = None # array of volumes for each model contribution, calculated during execution - seed = 12345 # random generator seed, should vary for parallel execution - nContrib = 300 # number of contributions that make up the entire model - - settables = [ + settables = ( "nContrib", # these are the allowed input arguments, can also be used later for storage "fitParameterLimits", "staticParameters", @@ -283,18 +225,14 @@ class McModel: "modelDType", "seed", "logRandom", - ] + ) - def fitKeys(self) -> List[str]: + def fit_keys(self) -> List[str]: return [key for key in self.fitParameterLimits.keys()] - # make a transformation for the default uniform generator to log-uniform, useful in wide ranges: - def log_transform_generator( - self, rng: np.random.Generator, low: float, high: float, size: int | None = None - ) -> np.ndarray: + def _log_uniform(self, rng: np.random.Generator, low: float, high: float, size: int | None = None) -> np.ndarray: if low <= 0 or high <= 0: raise ValueError("low and high must be positive, nonzero values.") - # swap low and high if low is greater than high if low > high: low, high = high, low return 10 ** (rng(low=np.log10(low), high=np.log10(high), size=size)) @@ -306,77 +244,79 @@ def __init__( resultIndex: int = 1, **kwargs: dict, ) -> None: - # reset everything so we're sure not to inherit anything from another instance: + self._reset_state() + + # make sure we store and read from the right place. + self.resultIndex = ResultIndex(resultIndex) # defines the HDF5 root path + + if loadFromFile is not None: + # nContrib is reset with the length of the tables: + self.load(loadFromFile, loadFromRepetition) + + self._apply_configuration(kwargs) + self._initialize_random_generators() + self._initialize_parameter_set() + self._load_model_function() + self.checkSettings() + + def _reset_state(self) -> None: + """Reset instance state so a fresh model never inherits previous run state.""" self.func = None # SasModels model instance self.modelName = "sphere" # SasModels model name self.modelDType = "fast" # model data type, choose 'fast' for single precision self.kernel = object # SasModels kernel pointer - self.parameterSet = ( - None # pandas dataFrame of length nContrib, with column names of parameters - ) - self.staticParameters = ( - None # dictionary of static parameter-value pairs during MC optimization - ) + self.parameterSet = None # pandas dataFrame of length nContrib, with column names of parameters + self.staticParameters = None # dictionary of static parameter-value pairs during MC optimization self.pickParameters = None # dict of values with new random picks, # named by parameter names - self.pickIndex = ( - None # int showing the running number of the current contribution being tested - ) + self.pickIndex = None # int showing the running number of the current contribution being tested self.fitParameterLimits = None # dict of value pairs (tuples) *for fit parameters only* # with lower, upper limits for the random function # generator, named by parameter names self.randomGenerators = None # dict with random value generators - self.volumes = ( - None # array of volumes for each model contribution, calculated during execution - ) + self.volumes = None # array of volumes for each model contribution, calculated during execution self.seed = 12345 # random generator seed, should vary for parallel execution self.nContrib = 300 # number of contributions that make up the entire model + self.logRandoms = None + self.logRandom = False - # make sure we store and read from the right place. - self.resultIndex = ResultIndex(resultIndex) # defines the HDF5 root path - - if loadFromFile is not None: - # nContrib is reset with the length of the tables: - self.load(loadFromFile, loadFromRepetition) - + def _apply_configuration(self, kwargs: dict) -> None: # overwrites settings loaded from file if specified. + _require_valid_settable_keys(kwargs, self.settables) for key, value in kwargs.items(): - assert ( - key in self.settables - ), "Key '{}' is not a valid settable option. Valid options are: \n {}".format( - key, self.settables - ) setattr(self, key, value) + def _initialize_random_generators(self) -> None: if self.randomGenerators is None: - self.randomGenerators = dict.fromkeys( - [key for key in self.fitKeys()], - np.random.default_rng(self.seed).uniform, - ) - self.logRandoms = dict.fromkeys([key for key in self.fitKeys()], self.logRandom) + uniform = np.random.default_rng(self.seed).uniform + self.randomGenerators = {key: uniform for key in self.fit_keys()} + if self.logRandoms is None: + self.logRandoms = {key: self.logRandom for key in self.fit_keys()} + def _initialize_parameter_set(self) -> None: if self.parameterSet is None: - self.parameterSet = pandas.DataFrame(index=range(self.nContrib), columns=self.fitKeys()) + self.parameterSet = pandas.DataFrame(index=range(self.nContrib), columns=self.fit_keys()) self.resetParameterSet() - if self.modelName.lower() == "sim": - self.loadSimModel() - elif self.modelName.lower() == "mcsas_sphere": - self.loadMcsasSphereModel() - else: - self.loadModel() - - self.checkSettings() + def _load_model_function(self) -> None: + custom_loader_name = CUSTOM_MODEL_LOADERS.get(self.modelName.lower()) + if custom_loader_name is None: + self._load_sasmodels_model() + return + getattr(self, custom_loader_name)() def checkSettings(self) -> None: for key in self.settables: if key in ("seed",): continue val = getattr(self, key, None) - assert val is not None, "required McModel setting {} has not been defined..".format(key) + if val is None: + raise ValueError("Required McModel setting {} has not been defined.".format(key)) - assert self.func is not None, "SasModels function has not been loaded" - assert self.parameterSet is not None, "parameterSet has not been initialized" + if self.func is None: + raise RuntimeError("SasModels function has not been loaded.") + if self.parameterSet is None: + raise RuntimeError("parameterSet has not been initialized.") def calcModelIV(self, parameters: dict) -> Tuple[np.ndarray, np.ndarray]: # moved from McCore @@ -386,27 +326,22 @@ def calcModelIV(self, parameters: dict) -> Tuple[np.ndarray, np.ndarray]: # as in this equation (http://www.sasview.org/docs/user/models/sphere.html). # So needs to be divided by the volume. if isinstance(self.kernel, sasmodels.mixture.MixtureKernel): - print( + logger.warning( "for Mixture kernels (e.g. a+b+...), element a must be a volumetric object " "for McSAS optimizations, the rest must be static!" ) - if isinstance( - self.kernel, (sasmodels.product.ProductKernel, sasmodels.mixture.MixtureKernel) - ): + if isinstance(self.kernel, (sasmodels.product.ProductKernel, sasmodels.mixture.MixtureKernel)): # call_Fq not available Fsq = sasmodels.direct_model.call_kernel(self.kernel, kernelParams) try: V_shell = self.kernel.results()["volume"] except KeyError: - print("This model does not have a volume! Cannot calculate without volume!!") - raise NotImplementedError + raise NotImplementedError("This model does not have a volume and cannot be used in McSAS3.") # this needs to be done for productKernel: Fsq = Fsq * V_shell else: - F, Fsq, R_eff, V_shell, V_ratio = sasmodels.direct_model.call_Fq( - self.kernel, kernelParams - ) + F, Fsq, R_eff, V_shell, V_ratio = sasmodels.direct_model.call_Fq(self.kernel, kernelParams) else: Fsq, V_shell = self.kernel(**kernelParams) # modelIntensity = Fsq/V_shell @@ -420,31 +355,22 @@ def calcModelIV(self, parameters: dict) -> Tuple[np.ndarray, np.ndarray]: def pick(self) -> None: """pick new random model parameter""" - self.pickParameters = self.generateRandomParameterValues() - - def generateRandomParameterValues(self) -> None: - """to be depreciated as soon as models can generate their own...""" - # initialize dict with parameter-value pairs defaulting to None - returnDict = dict.fromkeys([key for key in self.fitParameterLimits]) - # fill: - for parName in self.fitParameterLimits.keys(): - # can be replaced by a loop over iteritems: - (lower, upper) = self.fitParameterLimits[parName] + self.pickParameters = self.generate_random_parameter_values() + + def generate_random_parameter_values(self) -> dict[str, float]: + """Generate one new random parameter set within the configured fit limits.""" + return_dict = dict.fromkeys(self.fitParameterLimits) + for parName, (lower, upper) in self.fitParameterLimits.items(): if self.logRandoms[parName]: - # use log-uniform distribution - returnDict[parName] = self.log_transform_generator( - self.randomGenerators[parName], lower, upper - ) + return_dict[parName] = self._log_uniform(self.randomGenerators[parName], lower, upper) else: - # use uniform distribution - returnDict[parName] = self.randomGenerators[parName](low=lower, high=upper) - return returnDict + return_dict[parName] = self.randomGenerators[parName](low=lower, high=upper) + return return_dict def resetParameterSet(self) -> None: """fills the model parameter values with random values""" for contribi in range(self.nContrib): - # can be improved with a list comprehension, but this only executes once.. - self.parameterSet.loc[contribi] = self.generateRandomParameterValues() + self.parameterSet.loc[contribi] = self.generate_random_parameter_values() # Loading and Storing functions: @@ -453,12 +379,10 @@ def load(self, loadFromFile: Path, loadFromRepetition: int) -> None: loads a preset set of contributions from a previous optimization, stored in HDF5 nContrib is reset to the length of the previous optimization. """ - assert ( - loadFromFile is not None - ), "Input filename cannot be empty. Also specify a repetition number to load." - assert ( - loadFromRepetition is not None - ), "Repetition number must be given when loading model parameters from a file" + if loadFromFile is None: + raise ValueError("Input filename cannot be empty. Also specify a repetition number to load.") + if loadFromRepetition is None: + raise ValueError("Repetition number must be given when loading model parameters from a file") path = self.resultIndex.nxsEntryPoint / "model" @@ -467,19 +391,16 @@ def load(self, loadFromFile: Path, loadFromRepetition: int) -> None: self.modelName = loadKV(loadFromFile, path / "modelName", datatype="str") # .decode('utf8') path /= f"repetition{loadFromRepetition}" self.parameterSet = loadKV(loadFromFile, path / "parameterSet", datatype="dictToPandas") - self.parameterSet.columns = [ - colname for colname in self.parameterSet.columns - ] # what does this do, a no-op? self.volumes = loadKV(loadFromFile, path / "volumes") self.seed = loadKV(loadFromFile, path / "seed") self.modelDType = loadKV(loadFromFile, path / "modelDType", datatype="str") self.nContrib = self.parameterSet.shape[0] def store(self, filename: Path, repetition: int) -> None: - assert ( - repetition is not None - ), "Repetition number must be given when storing model parameters into a paramFile" - assert filename is not None + if repetition is None: + raise ValueError("Repetition number must be given when storing model parameters into a paramFile") + if filename is None: + raise ValueError("filename cannot be empty") path = self.resultIndex.nxsEntryPoint / "model" storeKVPairs(filename, path / "fitParameterLimits", self.fitParameterLimits.items()) @@ -494,64 +415,31 @@ def store(self, filename: Path, repetition: int) -> None: [("seed", self.seed), ("volumes", self.volumes), ("modelDType", self.modelDType)], ) - # SasView SasModel helper functions: - - def availableModels(self) -> None: - # show me all the available models, 1D and 1D+2D - print("\n \n 1D-only SasModel Models:\n") - + def available_models(self) -> dict[str, list[str]]: + """Return available SasModels grouped by dimensionality support.""" + available = {"one_dimensional": [], "one_and_two_dimensional": []} for model in sasmodels.core.list_models(): - modelInfo = sasmodels.core.load_model_info(model) - if not modelInfo.parameters.has_2d: - print("{} is available only in 1D".format(modelInfo.id)) + model_info = sasmodels.core.load_model_info(model) + if model_info.parameters.has_2d: + available["one_and_two_dimensional"].append(model_info.id) + else: + available["one_dimensional"].append(model_info.id) + return available - print("\n \n 2D- and 1D- SasModel Models:\n") - for model in sasmodels.core.list_models(): - modelInfo = sasmodels.core.load_model_info(model) - if modelInfo.parameters.has_2d: - print("{} is available in 1D and 2D".format(modelInfo.id)) - - def modelExists(self) -> bool: - return True - # todo: this doesn't work anymore when combining models, e.g. sphere@hardsphere - # # checks whether the given model name exists, throw exception if not - # assert ( - # self.modelName in sasmodels.core.list_models() - # ), "Model with name: {} does not exist in the list of available models: \n {}".format( - # self.modelName, sasmodels.core.list_models() - # ) - # return True - - def loadModel(self) -> None: - # loads sasView model and puts the handle in the right place: - self.modelExists() # check if model exists + def _load_sasmodels_model(self) -> None: self.func = sasmodels.core.load_model(self.modelName, dtype=self.modelDType) - def loadMcsasSphereModel(self) -> None: - self.func = mcsasSphereModel( - **self.staticParameters - # no arguments here... probably - ) + def _load_mcsas_sphere_model(self) -> None: + self.func = mcsasSphereModel(**self.staticParameters) - def loadSimModel(self) -> None: - if "simDataQ1" not in self.staticParameters.keys(): - # if it was None when written, it might not exist when loading - self.staticParameters.update({"simDataQ1": None}) - - self.func = McSimPseudoModel( - extrapY0=self.staticParameters["extrapY0"], - extrapScaling=self.staticParameters["extrapScaling"], - simDataQ0=self.staticParameters["simDataQ0"], - simDataQ1=self.staticParameters["simDataQ1"], - simDataI=self.staticParameters["simDataI"], - simDataISigma=self.staticParameters["simDataISigma"], - ) - # simDataDict= self.staticParameters['simDataDict']) - - def showModelParameters(self) -> dict: - # find out what the parameters are for the set model, e.g.: - # mc.showModelParameters() - assert ( - self.func is not None - ), "Model must be loaded already before this function can be used, using self.loadModel()" + def _load_sim_model(self) -> None: + static_parameters = dict(self.staticParameters) + static_parameters.setdefault("simDataQ1", None) + self.staticParameters = static_parameters + self.func = McSimPseudoModel(**{key: static_parameters[key] for key in McSimPseudoModel.settables}) + + def model_parameters(self) -> dict: + """Return the default parameter set for the currently loaded model.""" + if self.func is None: + raise RuntimeError("Model must be loaded before model parameters can be queried.") return self.func.info.parameters.defaults diff --git a/src/mcsas3/mc_model_histogrammer.py b/src/mcsas3/mc_model_histogrammer.py index d2ff95b..8b84c02 100644 --- a/src/mcsas3/mc_model_histogrammer.py +++ b/src/mcsas3/mc_model_histogrammer.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pathlib import Path import matplotlib.pyplot as plt @@ -10,127 +12,92 @@ from .mc_model import McModel from .mc_opt import McOpt +MODE_COLUMNS = ("totalValue", "mean", "variance", "skew", "kurtosis") + + +def _empty_modes_frame() -> pandas.DataFrame: + return pandas.DataFrame(columns=MODE_COLUMNS) + class McModelHistogrammer: """ - This class takes care of the analysis of an optimized model instance parameters. - That means it histograms the result based on the histogram range settings, and - calculates the five population modes. These are not weighted by the scaling factor, - and exist therefore in non-absolute units. Contribution weighting is assumed to be 0.5, - i.e. volume-weighted. - - McModelHistogrammer is calculated for every repetition individually. - - Besides this class, there will be an McAnalysis class that combines the results from - McModelHistogrammer (and calculates the resulting mean and standard deviations), - its optimization-instance parameters, and calculates the observability limits. - - McModelHistogrammer is expected to be called from other routines, and so only minimal - input-checking is done. - - histRanges argument should contain the following keys: - - parameter: name of the parameter to histogram - must be a fitparameter (is checked) - - autoRange: Boolean which will use fitparameterlimits to define histogram width, - overrides presetRange - - presetRangeMin: a min value that defines the histogram lower limit - - presetRangeMax: a max value that defines the histogram upper limit - - nBins: the number of bins to divide into - - binScale: "linear" or "log" - - binWeighting: "vol" implemented only. Future options: "num", "volsqr", "surf" - - The histogrammer and McAnalysis classes can be run independent from the optimization - procedure, to allow re-histogramming using different settings without needing re- - optimisation (like our original McSAS did). + Histogram and summarize a single optimized model repetition. + + The histograms are scaled to absolute volume fractions using the repetition scaling + factor and the SasModels-to-McSAS3 correction factor. """ - _model = None # instance of model to work with - _opt = None # instance of optimization parameters - _histRanges = ( - pandas.DataFrame() - ) # pandas dataframe with one row per range, and the parameters as developed in McSAS - _binEdges = ( - dict() - ) # dict of binEdge arrays: _binEdges[0] matches parameters in _histRanges.loc[0]. - _histDict = ( - dict() - ) # histograms, one per range, i.e. _hist[0] matches parameters in _histRanges.loc[0] - _modes = pandas.DataFrame( - columns=["totalValue", "mean", "variance", "skew", "kurtosis"] - ) # modes of the populations: total, mean, variance, skew, kurtosis - _correctionFactor = 1e-5 # scaling factor to switch from SasModel units used in the model - # instance (1/(cm sr) for dimensions in Angstrom) to absolute units - # in 1/(m sr) for dimensions in nm - - def __init__( - self, coreInstance: McCore, histRanges: pandas.DataFrame, resultIndex: int = 1 - ) -> None: - # reset variables, make sure we don't inherit anything from another instance: - self._model = None # instance of model to work with - self._histRanges = ( - pandas.DataFrame() - ) # pandas dataframe with one row per range, and the parameters as developed in McSAS - self._binEdges = ( - dict() - ) # dict of binEdge arrays: _binEdges[0] matches parameters in _histRanges.loc[0]. - self._histDict = ( - dict() - ) # histograms, one per range, i.e. _hist[0] matches parameters in _histRanges.loc[0] - self._modes = pandas.DataFrame( - columns=["totalValue", "mean", "variance", "skew", "kurtosis"] - ) # modes of the populations: total, mean, variance, skew, kurtosis - - self.resultIndex = ResultIndex(resultIndex) # defines the HDF5 root path - - assert isinstance( - coreInstance, McCore - ), "A core instance (containing model + opt) must be provided!" - assert isinstance( - histRanges, pandas.DataFrame - ), "A pandas dataframe with histogram ranges must be provided" - assert isinstance(coreInstance._model, McModel), "the core does not have a valid model set" - assert isinstance( - coreInstance._opt, McOpt - ), "the core does not have a valid optimization instance set" + _correctionFactor = 1e-5 + + def __init__(self, coreInstance: McCore, histRanges: pandas.DataFrame, resultIndex: int = 1) -> None: + self._model: McModel | None = None + self._opt: McOpt | None = None + self._histRanges = pandas.DataFrame() + self._binEdges: dict[int, np.ndarray] = {} + self._histDict: dict[int, np.ndarray] = {} + self._modes = _empty_modes_frame() + + self.resultIndex = ResultIndex(resultIndex) + + self._validate_inputs(coreInstance, histRanges) self._model = coreInstance._model - self._opt = coreInstance._opt # we need this for the scaling factor. - self._histRanges = histRanges - - for histIndex, histRange in histRanges.iterrows(): - # does the model have that parameter? - assert ( - histRange.parameter in self._model.parameterSet.keys() - ), "histogram parameter must be present in model fitparameters" - assert histRange.binScale in [ - "linear", - "log", - "auto", - ], "binning scale must be either 'linear' or 'log'" # , or 'auto' (Doana)" - assert ( - histRange.binWeighting == "vol" - ), "only volume-weighted binning implemented for now" - assert isinstance(histRange.autoRange, bool), "autoRange must be a boolean" - assert isinstance(histRange.nBin, int) and ( - histRange.nBin > 0 - ), "nBin must be an integer > 0" - - if histRange.autoRange: - histRange["rangeMin"] = self._model.fitParameterLimits[histRange.parameter][0] - histRange["rangeMax"] = self._model.fitParameterLimits[histRange.parameter][1] - else: - histRange["rangeMin"] = histRange.presetRangeMin - histRange["rangeMax"] = histRange.presetRangeMax - self._histRanges.loc[histIndex, "rangeMin"] = histRange["rangeMin"] - self._histRanges.loc[histIndex, "rangeMax"] = histRange["rangeMax"] - - self._binEdges[histIndex] = self.genX( - histRange, self._model.parameterSet, self._model.volumes - ) + self._opt = coreInstance._opt + self._histRanges = histRanges.copy(deep=True) + + for histIndex in self._histRanges.index: + histRange = self._resolved_hist_range(histIndex) + self._binEdges[histIndex] = self.genX(histRange, self._model.parameterSet) self.histogram(histRange, histIndex) self.modes(histRange, histIndex) + def _validate_inputs(self, coreInstance: McCore, histRanges: pandas.DataFrame) -> None: + if not isinstance(coreInstance, McCore): + raise TypeError("A core instance (containing model + opt) must be provided.") + if not isinstance(histRanges, pandas.DataFrame): + raise TypeError("A pandas dataframe with histogram ranges must be provided.") + if not isinstance(coreInstance._model, McModel): + raise TypeError("The provided McCore instance does not have a valid model set.") + if not isinstance(coreInstance._opt, McOpt): + raise TypeError("The provided McCore instance does not have a valid optimization instance set.") + + def _resolved_hist_range(self, histIndex: int) -> pandas.Series: + histRange = self._histRanges.loc[histIndex].copy() + if histRange.parameter not in self._model.parameterSet.keys(): + raise ValueError("Histogram parameter must be present in model fit parameters.") + if histRange.binScale not in ["linear", "log", "auto"]: + raise ValueError("Binning scale must be one of 'linear', 'log', or 'auto'.") + if histRange.binWeighting != "vol": + raise ValueError("Only volume-weighted histogramming is implemented.") + if not isinstance(histRange.autoRange, (bool, np.bool_)): + raise TypeError("autoRange must be a boolean.") + if not isinstance(histRange.nBin, (int, np.integer)) or histRange.nBin <= 0: + raise ValueError("nBin must be an integer > 0.") + + if histRange.autoRange: + range_min, range_max = self._model.fitParameterLimits[histRange.parameter] + else: + range_min, range_max = histRange.presetRangeMin, histRange.presetRangeMax + + self._histRanges.loc[histIndex, "rangeMin"] = range_min + self._histRanges.loc[histIndex, "rangeMax"] = range_max + histRange["rangeMin"] = range_min + histRange["rangeMax"] = range_max + return histRange + + @staticmethod + def _calc_modes(values: np.ndarray, weights: np.ndarray) -> tuple[float, float, float, float, float]: + total = np.sum(weights) + if total == 0: + return total, np.nan, np.nan, np.nan, np.nan + mean = np.sum(values * weights) / total + variance = np.sum((values - mean) ** 2 * weights) / total + sigma = np.sqrt(abs(variance)) + skew = np.sum((values - mean) ** 3 * weights) / (total * sigma**3) + kurtosis = np.sum((values - mean) ** 4 * weights) / (total * sigma**4) + return total, mean, variance, skew, kurtosis + def debugPlot(self, histIndex: int) -> None: - """Plots a single histogram, for debugging purposes only, - can only be done after histogramming is complete.""" + """Plot a single histogram for debugging.""" plt.bar( self._binEdges[histIndex][:-1], self._histDict[histIndex], @@ -140,110 +107,72 @@ def debugPlot(self, histIndex: int) -> None: if self._histRanges.loc[histIndex].binScale == "log": plt.xscale("log") - def histogram(self, histRange: pandas.DataFrame, histIndex: int) -> None: - """histograms the data into an individual range""" - - n, _ = np.histogram( + def histogram(self, histRange: pandas.Series, histIndex: int) -> None: + """Histogram the data into an individual range.""" + counts, _ = np.histogram( self._model.parameterSet[histRange.parameter], bins=self._binEdges[histIndex], density=False, - # already volume-weighted. If done so again, we get a vol-sqr-weighted plot - # with the larger sizes overemphasized - # weights = self._model.volumes # correctness needs to be checked !!! ) - # correct for SasView units - McSAS Units difference (correctionFactor), - # and scale to absolute units by multiplying with the overall curve scaling factor.. - self._histDict[histIndex] = n.astype(np.float64) * self._opt.x0[0] * self._correctionFactor - - def modes(self, histRange: pandas.DataFrame, histIndex: int) -> None: - def calcModes(rset, frac): - # function taken from the old McSAS code: - val = sum(frac) - if val == 0: - return val, np.nan, np.nan, np.nan, np.nan - else: - mu = sum(rset * frac) / sum(frac) - var = sum((rset - mu) ** 2 * frac) / sum(frac) - sigma = np.sqrt(abs(var)) - skw = sum((rset - mu) ** 3 * frac) / (sum(frac) * sigma**3) - krt = sum((rset - mu) ** 4 * frac) / (sum(frac) * sigma**4) - return val, mu, var, skw, krt - - # clip the data to the min/max specified in the range: - workData = self._model.parameterSet[histRange.parameter] - workVolumes = self._model.volumes - clippedDataValues = workData[ - workData.between(histRange.rangeMin, histRange.rangeMax) - ].values - clippedDataVolumes = workVolumes[workData.between(histRange.rangeMin, histRange.rangeMax)] - - if clippedDataVolumes.size == 0: - val, mu, var, skw, krt = np.nan, np.nan, np.nan, np.nan, np.nan + self._histDict[histIndex] = counts.astype(np.float64) * self._opt.x0[0] * self._correctionFactor + + def modes(self, histRange: pandas.Series, histIndex: int) -> None: + parameter_values = self._model.parameterSet[histRange.parameter] + in_range = parameter_values.between(histRange.rangeMin, histRange.rangeMax) + clipped_values = parameter_values[in_range].values + clipped_volumes = self._model.volumes[in_range] + + if clipped_volumes.size == 0: + total, mean, variance, skew, kurtosis = np.nan, np.nan, np.nan, np.nan, np.nan else: - # needs a rethink... - val, mu, var, skw, krt = calcModes( - clippedDataValues, np.ones(clippedDataVolumes.shape) - ) # /workVolumes.sum() + total, mean, variance, skew, kurtosis = self._calc_modes( + clipped_values, + np.ones(clipped_volumes.shape), + ) self._modes.loc[histIndex] = pandas.Series( { - "totalValue": val * self._correctionFactor * self._opt.x0[0], - "mean": mu, - "variance": var, - "skew": skw, - "kurtosis": krt, + "totalValue": total * self._correctionFactor * self._opt.x0[0], + "mean": mean, + "variance": variance, + "skew": skew, + "kurtosis": kurtosis, } ) - def genX( - self, histRange: pandas.DataFrame, parameterSet: pandas.DataFrame, volumes: np.ndarray - ) -> np.ndarray: - """Generates bin edges""" + def genX(self, histRange: pandas.Series, parameterSet: pandas.DataFrame) -> np.ndarray: + """Generate histogram bin edges.""" if histRange.binScale == "linear": - binEdges = np.linspace(histRange.rangeMin, histRange.rangeMax, histRange.nBin + 1) - elif histRange.binScale == "log": - binEdges = np.logspace( + return np.linspace(histRange.rangeMin, histRange.rangeMax, histRange.nBin + 1) + if histRange.binScale == "log": + return np.logspace( np.log10(histRange.rangeMin), np.log10(histRange.rangeMax), histRange.nBin + 1, ) - elif histRange.binScale == "auto": - assert isinstance( - parameterSet, pandas.DataFrame - ), "a parameterSet must be provided for automatic bin determination" - binEdges = np.histogram_bin_edges( - parameterSet[histRange.parameter], - bins="auto", - range=[histRange.rangeMin, histRange.rangeMax], - # weights = volumes , # can't be used by "auto" yet, - # but may be in the future according to the docs... - ) - return binEdges + if not isinstance(parameterSet, pandas.DataFrame): + raise TypeError("A parameterSet must be provided for automatic bin determination.") + return np.histogram_bin_edges( + parameterSet[histRange.parameter], + bins="auto", + range=[histRange.rangeMin, histRange.rangeMax], + ) def store(self, filename: Path, repetition: int) -> None: - # TODO: CHECK USE OF KEYS IN STORE PATH: - assert ( - repetition is not None - ), "Repetition number must be given when storing histograms into a paramFile" + if repetition is None: + raise ValueError("Repetition number must be given when storing histograms into a paramFile") path = self.resultIndex.nxsEntryPoint / "histograms" - # store histogram ranges and settings, for archival purposes only, - # these settings are not planned to be reused.: - oDict = self._histRanges.copy().to_dict(orient="index") - for key in oDict.keys(): - # print("histRanges: storing key: {}, value: {}".format(key, oDict[key])) - pairs = [(dKey, dValue) for dKey, dValue in oDict[key].items()] - # TODO: keys might be wrong here: + hist_range_dict = self._histRanges.copy().to_dict(orient="index") + for key, values in hist_range_dict.items(): + pairs = list(values.items()) storeKVPairs(filename, path / f"histRange{key}", pairs) - # store modes, for archival purposes only, these settings are not planned to be reused: - oDict = self._modes.copy().to_dict(orient="index") - for key in oDict.keys(): - # print("modes: storing key: {}, value: {}".format(key, oDict[key])) - pairs = [(dKey, dValue) for dKey, dValue in oDict[key].items()] - # TODO: keys might be wrong here: + mode_dict = self._modes.copy().to_dict(orient="index") + for key, values in mode_dict.items(): + pairs = list(values.items()) storeKVPairs(filename, path / f"histRange{key}" / f"repetition{repetition}", pairs) - for histIndex, histRange in self._histRanges.iterrows(): + for histIndex in self._histRanges.index: storeKVPairs( filename, path / f"histRange{histIndex}" / f"repetition{repetition}", diff --git a/src/mcsas3/mc_opt.py b/src/mcsas3/mc_opt.py index 31c3cdd..cd07b94 100644 --- a/src/mcsas3/mc_opt.py +++ b/src/mcsas3/mc_opt.py @@ -1,34 +1,25 @@ +from __future__ import annotations + from pathlib import Path, PurePosixPath -from typing import Optional +from typing import Any, ClassVar +import attrs import numpy as np from mcsas3.mc_hdf import ResultIndex, loadKVPairs, storeKVPairs -# TODO: refactor this using attrs @define for clearer handling. +def _coerce_result_index(value: ResultIndex | int) -> ResultIndex: + if isinstance(value, ResultIndex): + return value + return ResultIndex(value) -class McOpt: - """Class to store optimization settings and keep track of running variables""" - accepted = None # number of accepted picks - convCrit = 1 # reduced chi-square before valid return - gof = None # continually updated gof value - maxIter = 100000 # maximum steps before fail - maxAccept = np.inf # maximum accepted before valid return - modelI = None # internal, will be filled later - repetition = None # Optimization instance repetition number (defines storage location) - step = None # number of iteration steps, should be renamed "iteration" - testX0 = None # X0 if test is accepted. - testModelI = None # internal, updated intensity after replacing with pick - testModelV = None # volume of test object, optionally used for weighted histogramming later on. - weighting = 0.5 # NOT USED, set to default = volume-weighted. - # volume-weighting / compensation factor for the contributions - x0 = None # continually updated new guess for total scaling, background values. - acceptedSteps = [] # for each accepted pick, write the iteration step number here - acceptedGofs = [] # for each accepted pick, write the reached GOF here. +@attrs.define(slots=False) +class McOpt: + """Optimization settings and per-repetition optimizer state.""" - storeKeys = [ # keys to store in an output file + storeKeys: ClassVar[list[str]] = [ "accepted", "convCrit", "gof", @@ -42,7 +33,7 @@ class McOpt: "acceptedSteps", "acceptedGofs", ] - loadKeys = [ # load (and replace) these settings from a previous run into the current settings + loadKeys: ClassVar[list[str]] = [ "accepted", "convCrit", "gof", @@ -55,57 +46,39 @@ class McOpt: "acceptedGofs", ] - # Multiple types (e.g. Path|None ) only supported from Python 3.10 - def __init__( - self, loadFromFile: Optional[Path] = None, resultIndex: int = 1, **kwargs: dict - ) -> None: - """Initializes the options to the MC algorithm, *or* loads them from a previous run. - Note: If the parameters are loaded from a previous file, - any additional key-value pairs are updated.""" - - # Cleaning the parameters, making sure we do not inherit anything: - self.accepted = None # number of accepted picks - self.convCrit = 1 # reduced chi-square before valid return - self.gof = None # continually updated gof value - self.maxIter = 100000 # maximum steps before fail - self.maxAccept = np.inf # maximum accepted before valid return - self.modelI = None # internal, will be filled later - self.repetition = None # Optimization instance repetition number (defines storage location) - self.step = None # number of iteration steps, should be renamed "iteration" - self.testX0 = None # X0 if test is accepted. - self.testModelI = None # internal, updated intensity after replacing with pick - self.testModelV = ( - None # volume of test object, optionally used for weighted histogramming later on. - ) - self.weighting = 0.5 # NOT USED, set to default = volume-weighted. - # volume-weighting / compensation factor for the contributions - self.x0 = None # continually updated new guess for total scaling, background values. - self.acceptedSteps = [] # for each accepted pick, write the iteration step number here - self.acceptedGofs = [] # for each accepted pick, write the reached GOF here. + accepted: int | None = None + convCrit: float = 1.0 + gof: float | None = None + maxIter: int = 100000 + maxAccept: float = np.inf + modelI: np.ndarray | None = None + repetition: int | None = None + step: int | None = None + testX0: np.ndarray | None = None + testModelI: np.ndarray | None = None + testModelV: Any = None + weighting: float = 0.5 + x0: np.ndarray | None = None + acceptedSteps: list[int] = attrs.field(factory=list) + acceptedGofs: list[float] = attrs.field(factory=list) + resultIndex: ResultIndex = attrs.field(default=1, converter=_coerce_result_index, kw_only=True) + loadFromFile: Path | None = attrs.field(default=None, kw_only=True) + loadFromRepetition: int = attrs.field(default=0, kw_only=True) - self.resultIndex = ResultIndex(resultIndex) # defines the HDF5 root path - self.repetition = kwargs.pop("loadFromRepetition", 0) - - if loadFromFile is not None: - self.load(loadFromFile) - - for key, value in kwargs.items(): - assert key in self.storeKeys, "Key {} is not a valid option".format(key) - setattr(self, key, value) + def __attrs_post_init__(self) -> None: + if self.repetition is None: + self.repetition = self.loadFromRepetition + if self.loadFromFile is not None: + self.load(self.loadFromFile, repetition=self.loadFromRepetition) - # Multiple types (e.g. Path|None ) only supported from Python 3.10 - def store(self, filename: Path, path: Optional[PurePosixPath] = None) -> None: - """stores the settings in an output file (HDF5)""" + def store(self, filename: Path, path: PurePosixPath | None = None) -> None: + """Store the optimizer settings in the result HDF5 file.""" if path is None: path = self.resultIndex.nxsEntryPoint / "optimization" - storeKVPairs( - filename, path, [(key, getattr(self, key, None)) for key in self.storeKeys] - ) + storeKVPairs(filename, path, [(key, getattr(self, key, None)) for key in self.storeKeys]) - # Multiple types (e.g. Path|None ) only supported from Python 3.10 - def load( - self, filename: Path, path: Optional[PurePosixPath] = None, repetition: Optional[int] = None - ) -> None: + def load(self, filename: Path, path: PurePosixPath | None = None, repetition: int | None = None) -> None: + """Load optimizer settings from the result HDF5 file.""" if repetition is None: repetition = self.repetition if path is None: diff --git a/src/mcsas3/mc_plot.py b/src/mcsas3/mc_plot.py index 329df55..21f4798 100644 --- a/src/mcsas3/mc_plot.py +++ b/src/mcsas3/mc_plot.py @@ -7,6 +7,8 @@ import matplotlib.font_manager as fm import matplotlib.pyplot as plt +from .optimizer_input import optimizer_input_from_bundle + class McPlot: """ @@ -19,7 +21,7 @@ class McPlot: """ _analysis = None # instance of McAnalysis - _inputData = None # instance of McData + _inputData = None # optional input-data carrier retained for plotting context _figureHandle = None # handle to figure _axesHandles = None # subplots-style array of axes handles. _monoFont = None # font for the result card text @@ -41,9 +43,7 @@ def __init__(self, **kwargs): for font in fm.fontManager.ttflist if "Mono" in font.name or "Courier" in font.name or "Consolas" in font.name ) - monoFont = tuple( - desiredFont for desiredFont in monoFontPrefs if desiredFont in monospaceFonts - ) + monoFont = tuple(desiredFont for desiredFont in monoFontPrefs if desiredFont in monospaceFonts) if not len(monoFont): # no preferred font is available, use the first available one monoFont = monospaceFonts self._monoFont = monoFont[0] @@ -119,10 +119,15 @@ def resultCard(self, mcres, saveHistFile: Optional[Path] = None) -> None: # plot data and fit: plt.sca(ahs[1, 0]) + if mcres.analysisBundle is not None: + plot_input = optimizer_input_from_bundle(mcres.analysisBundle) + else: + plot_input = mcres._optimizerInput + q_primary = plot_input.primary_q plt.errorbar( - mcres._measData["Q"][0], - mcres._measData["I"], - yerr=mcres._measData["ISigma"], + q_primary, + plot_input.i, + yerr=plot_input.isigma, label="Measured data", zorder=1, ) @@ -131,7 +136,7 @@ def resultCard(self, mcres, saveHistFile: Optional[Path] = None) -> None: plt.xlabel("Q (1/nm)") plt.ylabel("I (1/(m sr))") plt.plot( - mcres._measData["Q"][0], + q_primary, mcres.modelIAvg.modelIMean.values, zorder=2, label="McSAS3 fit", diff --git a/src/mcsas3/mcsas3_cli_histogrammer.py b/src/mcsas3/mcsas3_cli_histogrammer.py index ee57097..32e4a48 100755 --- a/src/mcsas3/mcsas3_cli_histogrammer.py +++ b/src/mcsas3/mcsas3_cli_histogrammer.py @@ -7,7 +7,8 @@ from pathlib import Path from sys import platform -from mcsas3.cli_tools import McSAS3_cli_histogram +from mcsas3.cli_histogram import McSAS3_cli_histogram +from mcsas3.runtime_paths import example_configuration_path def isMac(): @@ -40,7 +41,7 @@ def main(): "-r", "--resultFile", type=lambda p: Path(p).absolute(), - default=Path(__file__).absolute().parent / "test.nxs", + default=Path.cwd() / "test.nxs", help="Path to the file with the McSAS3 optimization result", required=True, ) @@ -48,7 +49,7 @@ def main(): "-H", "--histConfigFile", type=lambda p: Path(p).absolute(), - default=Path("./example_configurations/hist_config_dual.yaml"), + default=example_configuration_path("hist_config_dual.yaml").absolute(), help="Path to the filename with the histogramming configuration", # required=True, ) diff --git a/src/mcsas3/mcsas3_cli_runner.py b/src/mcsas3/mcsas3_cli_runner.py index a87e27d..b0c71cb 100755 --- a/src/mcsas3/mcsas3_cli_runner.py +++ b/src/mcsas3/mcsas3_cli_runner.py @@ -8,8 +8,8 @@ from pathlib import Path from sys import platform -from mcsas3 import __version__ as version # ignore unused import # noqa: F401 -from mcsas3.cli_tools import McSAS3_cli_optimize +from mcsas3.cli_optimize import McSAS3_cli_optimize +from mcsas3.runtime_paths import example_configuration_path, quickstart_testdata_path # from mcsas3.mcmodelhistogrammer import McModelHistogrammer # from mcsas3.mcanalysis import McAnalysis @@ -46,7 +46,7 @@ def main(): "-f", "--dataFile", type=lambda p: Path(p).absolute(), - default=Path(__file__).absolute().parent / "testdata" / "quickstartdemo1.csv", + default=quickstart_testdata_path("quickstartdemo1.csv").absolute(), help="Path to the filename with the SAXS data", # required=True, ) @@ -54,9 +54,7 @@ def main(): "-F", "--readConfigFile", type=lambda p: Path(p).absolute(), - default=Path(__file__).absolute().parent - / "example_configurations" - / "read_config_csv.yaml", + default=example_configuration_path("read_config_csv.yaml").absolute(), help="Path to the config file how to read the input data", # required=True, ) @@ -64,7 +62,7 @@ def main(): "-r", "--resultFile", type=lambda p: Path(p).absolute(), - default=Path(__file__).absolute().parent / "test.nxs", + default=Path.cwd() / "test.nxs", help="Path to the file to create and store the McSAS3 result in", # required=True, ) @@ -72,9 +70,7 @@ def main(): "-R", "--runConfigFile", type=lambda p: Path(p).absolute(), - default=Path(__file__).absolute().parent - / "example_configurations" - / "run_config_spheres_auto.yaml", + default=example_configuration_path("run_config_spheres_auto.yaml").absolute(), help="Path to the configuration file containing the model parameters", # required=True, ) diff --git a/src/mcsas3/optimizer_input.py b/src/mcsas3/optimizer_input.py new file mode 100644 index 0000000..434c02b --- /dev/null +++ b/src/mcsas3/optimizer_input.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any + +import numpy as np + +from .data_adapters import fit_arrays_from_bundle + + +@dataclass(frozen=True) +class OptimizerInput: + """Normalized optimizer-facing arrays for 1D or 2D scattering data.""" + + q: tuple[np.ndarray, ...] + i: np.ndarray + isigma: np.ndarray + + def __post_init__(self) -> None: + q_arrays = tuple(np.asarray(q_component, dtype=float).reshape(-1) for q_component in self.q) + i_array = np.asarray(self.i, dtype=float).reshape(-1) + isigma_array = np.asarray(self.isigma, dtype=float).reshape(-1) + + if len(q_arrays) not in (1, 2): + raise ValueError("OptimizerInput expects one Q array for 1D data or two Q arrays for 2D data.") + if i_array.size == 0: + raise ValueError("OptimizerInput intensity array must not be empty.") + if isigma_array.shape != i_array.shape: + raise ValueError("OptimizerInput intensity and uncertainty arrays must have matching shapes.") + if any(q_component.shape != i_array.shape for q_component in q_arrays): + raise ValueError("All OptimizerInput Q arrays must match the intensity array shape.") + + object.__setattr__(self, "q", q_arrays) + object.__setattr__(self, "i", i_array) + object.__setattr__(self, "isigma", isigma_array) + + @property + def ndim(self) -> int: + """Return the dimensionality of the optimizer input.""" + + return len(self.q) + + @property + def q_for_model(self) -> list[np.ndarray]: + """Return copied Q arrays ready for kernel construction.""" + + return [q_component.copy() for q_component in self.q] + + @property + def primary_q(self) -> np.ndarray: + """Return the primary Q axis used for 1D support calculations.""" + + return self.q[0] + + @property + def q_support(self) -> np.ndarray: + """Return absolute Q support for limit auto-scaling.""" + + if self.ndim == 1: + return np.abs(self.primary_q) + return np.sqrt(np.sum(np.stack([q_component**2 for q_component in self.q], axis=0), axis=0)) + + +def optimizer_input_from_bundle(bundle: Mapping[str, Any]) -> OptimizerInput: + """Build normalized optimizer arrays from a canonical bundle.""" + + q_arrays, intensity, sigma = fit_arrays_from_bundle(bundle) + return OptimizerInput(q=q_arrays, i=intensity, isigma=sigma) + + +def as_optimizer_input(data: Any) -> OptimizerInput: + """Coerce supported analysis inputs into an `OptimizerInput` instance.""" + + if isinstance(data, OptimizerInput): + return data + + if isinstance(data, Mapping): + if "signal" in data: + return optimizer_input_from_bundle(data) + + raise TypeError("Optimizer input must be an OptimizerInput or a canonical DataBundle.") + + +__all__ = [ + "OptimizerInput", + "as_optimizer_input", + "optimizer_input_from_bundle", +] diff --git a/src/mcsas3/osb.py b/src/mcsas3/osb.py index 84afa8c..cef22b3 100644 --- a/src/mcsas3/osb.py +++ b/src/mcsas3/osb.py @@ -1,102 +1,105 @@ +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any + import numpy as np -import scipy import scipy.optimize +from .data_adapters import as_analysis_bundle, fit_arrays_from_bundle +from .optimizer_input import as_optimizer_input + -class optimizeScalingAndBackground(object): - """small class derived from the McSAS mcsas/backgroundscalingfit.py class, - quickly provides an optimized scaling and background value for two datasets. +def _coerce_measurement_arrays( + measurement_input: Any, + measurement_sigma: np.ndarray | Sequence[float] | None, +) -> tuple[np.ndarray, np.ndarray]: + if measurement_sigma is None and not isinstance(measurement_input, (np.ndarray, list, tuple)): + try: + analysis_bundle = as_analysis_bundle(measurement_input) + except TypeError: + optimizer_input = as_optimizer_input(measurement_input) + measurement_input = optimizer_input.i + measurement_sigma = optimizer_input.isigma + else: + _q_arrays, measurement_input, measurement_sigma = fit_arrays_from_bundle(analysis_bundle) - **TODO (maybe)**: include a porod background contribution? If so, Q should be - available to this class. + return np.asarray(measurement_input, dtype=float), np.asarray(measurement_sigma, dtype=float) - Parameters - ---------- - measDataI: - numpy array of measured intensities - measDataISigma: - associated uncertainties - modelDataI: - array of model intensities. - x0: - optional, two-element tuple with initial guess for scaling and background - xBounds: - optional, constraints to the optimization, - speeds up when appropriate constraints are given - Returns - ------- - x: - length 2 ndarray with optimized scaling parameter and background parameter - cs: - final reduced chi-squared +def _default_x_bounds(measured_intensity: np.ndarray) -> list[list[float | None]]: + finite_values = measured_intensity[np.isfinite(measured_intensity)] + mean_value = float(finite_values.mean()) + return [[0, None], [-mean_value, mean_value]] - Usage example: +class optimizeScalingAndBackground: + """Optimize curve scaling and background against measured intensities.""" - o = optimizeScalingAndBackground(measDataI, measDataISigma) - xOpt, rcs = o.match(modelDataI) - """ + @classmethod + def from_input( + cls, + measurement_input: Any, + measurement_sigma: np.ndarray | Sequence[float] | None = None, + xBounds=None, + ) -> "optimizeScalingAndBackground": + """Construct an optimizer from canonical, optimizer-input, or raw array measurements.""" - measDataI = None - measDataISigma = None - xBounds = None + measured_intensity, measured_sigma = _coerce_measurement_arrays(measurement_input, measurement_sigma) + return cls(measured_intensity, measured_sigma, xBounds=xBounds) def __init__(self, measDataI=None, measDataISigma=None, xBounds=None): - self.measDataI = measDataI - self.measDataISigma = measDataISigma + """Initialize scaling/background optimization against flattened intensity data.""" + + measured_intensity, measured_sigma = _coerce_measurement_arrays(measDataI, measDataISigma) + self.measDataI = measured_intensity + self.measDataISigma = measured_sigma self.validate() - if xBounds is None: - self.xBounds = [ - [0, None], - [ - -self.measDataI[np.isfinite(self.measDataI)].mean(), - self.measDataI[np.isfinite(self.measDataI)].mean(), - ], - ] - # [self.measDataI[np.isfinite(self.measDataI)].min(), - # self.measDataI[np.isfinite(self.measDataI)].max()]] + self.xBounds = _default_x_bounds(self.measDataI) if xBounds is None else xBounds def initialGuess(self, optI): - # new guess: + """Return a robust initial guess for scale and background.""" + sc = np.median(self.measDataI / optI) bgnd = self.measDataI[-int(np.floor(4 * len(self.measDataI) / 5)) :].mean() - # bgnd = self.measDataI[np.isfinite(self.measDataI)].min() - # sc = ((self.measDataI - bgnd) / optI).mean() if sc <= 0: - sc = 1.0 # auto-determination failed, but we need to stay within bounds - # x0 = np.array([self.measDataI.mean() / optI.mean(), self.measDataI.min()]) - # sc = ((self.measDataI) / optI).mean() + sc = 1.0 bgnd = np.clip(bgnd, self.xBounds[1][0], self.xBounds[1][1]) return np.array([sc, bgnd]) def validate(self): - # checks input - assert not any(np.isnan(self.measDataI)) - assert not any(np.isinf(self.measDataI)) - assert not any(np.isnan(self.measDataISigma)) - assert not any(np.isinf(self.measDataISigma)) - assert any(np.isfinite(self.measDataISigma)) - assert any(np.isfinite(self.measDataISigma)) - assert self.measDataI.size != 0 - assert self.measDataI.shape == self.measDataISigma.shape - assert self.measDataI.ndim == 1 + """Validate the measured intensity arrays before optimization.""" + + if np.any(np.isnan(self.measDataI)): + raise ValueError("Measured intensities cannot contain NaN values.") + if np.any(np.isinf(self.measDataI)): + raise ValueError("Measured intensities cannot contain infinite values.") + if np.any(np.isnan(self.measDataISigma)): + raise ValueError("Intensity uncertainties cannot contain NaN values.") + if np.any(np.isinf(self.measDataISigma)): + raise ValueError("Intensity uncertainties cannot contain infinite values.") + if not np.any(np.isfinite(self.measDataISigma)): + raise ValueError("At least one finite intensity uncertainty is required.") + if self.measDataI.size == 0: + raise ValueError("Measured intensities cannot be empty.") + if self.measDataI.shape != self.measDataISigma.shape: + raise ValueError("Measured intensities and uncertainties must have matching shapes.") + if self.measDataI.ndim != 1: + raise ValueError("Measured intensities must be one-dimensional.") @staticmethod def optFunc(sc, measDataI, measDataISigma, modelDataI): - # reduced chi-square; normalized by uncertainty. - cs = ( - sum(((measDataI - (modelDataI * sc[0] + sc[1])) / measDataISigma) ** 2) / measDataI.size - ) + """Return the reduced chi-square for scale/background-adjusted model intensities.""" + + cs = np.sum(((measDataI - (modelDataI * sc[0] + sc[1])) / measDataISigma) ** 2) / measDataI.size return cs def match(self, modelDataI, x0=None): - if x0 is None: # optional argument with starting guess.. - # some initial guess + """Optimize scale and background against a model intensity vector.""" + + if x0 is None: x0 = self.initialGuess(modelDataI) - # adapt bounds to modelData: - # self._xBounds[0][1] /= modelDataI.mean() opt = scipy.optimize.minimize( self.optFunc, x0, diff --git a/src/mcsas3/preprocessing.py b/src/mcsas3/preprocessing.py new file mode 100644 index 0000000..3ac26d9 --- /dev/null +++ b/src/mcsas3/preprocessing.py @@ -0,0 +1,455 @@ +import logging +from collections.abc import Mapping, Sequence +from typing import Any, TypeAlias + +import attrs +import numpy as np +import pandas + +from .data_adapters import ( + _combine_uncertainties, + bundle_dimension, + bundle_from_1d_dataframe, + bundle_from_2d_arrays, + frame_from_bundle, + raw_2d_stage_from_bundle, +) +from .data_model import BaseData, DataBundle + +logger = logging.getLogger(__name__) +RangeLike: TypeAlias = Sequence[float] +RangeListLike: TypeAlias = Sequence[Sequence[float]] | None + + +@attrs.frozen +class Prepared1DStage: + """Canonical 1D stage plus its compatibility table view.""" + + bundle: DataBundle = attrs.field(validator=attrs.validators.instance_of(DataBundle)) + frame: pandas.DataFrame = attrs.field(validator=attrs.validators.instance_of(pandas.DataFrame)) + + +@attrs.frozen +class Prepared1DResult: + """Prepared clipped and binned 1D stages.""" + + clipped: Prepared1DStage = attrs.field(validator=attrs.validators.instance_of(Prepared1DStage)) + binned: Prepared1DStage = attrs.field(validator=attrs.validators.instance_of(Prepared1DStage)) + + +@attrs.frozen +class Prepared2DResult: + """Prepared clipped and binned 2D stages.""" + + clipped: DataBundle = attrs.field(validator=attrs.validators.instance_of(DataBundle)) + binned: DataBundle = attrs.field(validator=attrs.validators.instance_of(DataBundle)) + + +def _copy_bundle_metadata(source: Mapping[str, BaseData], target: DataBundle) -> DataBundle: + if getattr(source, "default_plot", None) is not None: + target.default_plot = source.default_plot + if getattr(source, "description", None) is not None: + target.description = source.description + return target + + +def copy_bundle(bundle: Mapping[str, BaseData]) -> DataBundle: + """Deep-copy a canonical bundle, preserving bundle-level metadata.""" + + copied = DataBundle() + for key, basedata in bundle.items(): + copied[key] = basedata.copy() + return _copy_bundle_metadata(bundle, copied) + + +def _bundle_units(bundle: Mapping[str, BaseData]) -> tuple[Any, Any]: + if "Q" in bundle: + q_units = bundle["Q"].units + elif "Qx" in bundle: + q_units = bundle["Qx"].units + else: + raise KeyError("Bundle must contain 'Q' for 1D data or 'Qx'/'Qy' for 2D data.") + return q_units, bundle["signal"].units + + +def _canonical_1d_frame(frame: pandas.DataFrame) -> pandas.DataFrame: + columns = ["Q", "I", "ISigma"] + for column in ("QSigma", "mask"): + if column in frame.columns: + columns.append(column) + return frame.loc[:, columns].copy() + + +def _frame_for_1d_stage( + bundle: Mapping[str, BaseData], source_frame: pandas.DataFrame | None = None +) -> pandas.DataFrame: + if source_frame is None: + return frame_from_bundle(bundle) + return source_frame.copy() + + +def _prepared_1d_stage(reference_bundle: Mapping[str, BaseData], frame: pandas.DataFrame) -> Prepared1DStage: + q_units, intensity_units = _bundle_units(reference_bundle) + bundle = bundle_from_1d_dataframe( + _canonical_1d_frame(frame), + q_units=q_units, + intensity_units=intensity_units, + ) + _copy_bundle_metadata(reference_bundle, bundle) + return Prepared1DStage(bundle=bundle, frame=frame.copy()) + + +def _propagated_mean_sigma(sigmas: pandas.Series) -> float: + sigma_values = sigmas.to_numpy(dtype=float) + return float(np.sqrt(np.square(sigma_values).sum()) / len(sigma_values)) + + +def _sample_sem(values: pandas.Series) -> float: + if len(values) <= 1: + return np.nan + return float(values.sem(ddof=1, skipna=True)) + + +def _bounded_sigma(*, propagated_sigma: float, sem_sigma: float, value: float, relative_floor: float) -> float: + floor_sigma = abs(float(value)) * relative_floor + candidates = [propagated_sigma, floor_sigma] + if np.isfinite(sem_sigma): + candidates.append(sem_sigma) + return float(np.max(candidates)) + + +def _validated_range(name: str, values: RangeLike) -> tuple[float, float]: + if len(values) != 2: + raise ValueError(f"{name} must contain exactly two values.") + try: + lower = float(values[0]) + upper = float(values[1]) + except (TypeError, ValueError) as exc: + raise TypeError(f"{name} must contain numeric lower and upper bounds.") from exc + if np.isnan(lower) or np.isnan(upper): + raise ValueError(f"{name} cannot contain NaN bounds.") + if lower > upper: + raise ValueError(f"{name} lower bound must be <= upper bound.") + return lower, upper + + +def _validated_range_list(name: str, ranges: RangeListLike) -> list[tuple[float, float]]: + if ranges is None: + return [] + if not isinstance(ranges, Sequence): + raise TypeError(f"{name} must be a sequence of [lower, upper] pairs.") + return [_validated_range(f"{name}[{index}]", value_range) for index, value_range in enumerate(ranges)] + + +def _validated_relative_floor(name: str, value: float) -> float: + try: + normalized = float(value) + except (TypeError, ValueError) as exc: + raise TypeError(f"{name} must be a numeric value.") from exc + if np.isnan(normalized) or normalized < 0: + raise ValueError(f"{name} must be non-negative.") + return normalized + + +def _validated_nbins(nbins: int) -> int: + if nbins < 0: + raise ValueError("nbins must be zero or positive.") + return int(nbins) + + +def copy_1d_stage(bundle: Mapping[str, BaseData], *, source_frame: pandas.DataFrame | None = None) -> Prepared1DStage: + """Copy a canonical 1D stage and its compatibility dataframe view.""" + + return Prepared1DStage( + bundle=copy_bundle(bundle), + frame=_frame_for_1d_stage(bundle, source_frame=source_frame), + ) + + +def clip_1d_bundle( + bundle: Mapping[str, BaseData], + *, + data_range: Sequence[float], + source_frame: pandas.DataFrame | None = None, +) -> Prepared1DStage: + """Clip a canonical 1D bundle to the requested Q range.""" + + lower, upper = _validated_range("data_range", data_range) + frame = _frame_for_1d_stage(bundle, source_frame=source_frame) + clipped = frame.query(f"{lower} <= Q < {upper}").dropna().copy() + if len(clipped) == 0: + raise ValueError("Data clipping range too small, no datapoints found.") + return _prepared_1d_stage(bundle, clipped) + + +def omit_1d_bundle( + bundle: Mapping[str, BaseData], + *, + omit_q_ranges: Sequence[Sequence[float]] | None, + source_frame: pandas.DataFrame | None = None, +) -> Prepared1DStage: + """Drop configured Q intervals from a canonical 1D bundle.""" + + if omit_q_ranges is None: + return copy_1d_stage(bundle, source_frame=source_frame) + + frame = _frame_for_1d_stage(bundle, source_frame=source_frame) + for q_min, q_max in _validated_range_list("omit_q_ranges", omit_q_ranges): + frame.drop(frame.query(f"{q_min} <= Q < {q_max}").index, inplace=True) + return _prepared_1d_stage(bundle, frame) + + +def rebin_1d_bundle( + bundle: Mapping[str, BaseData], + *, + nbins: int, + iemin: float, + qemin: float = 0.01, + source_frame: pandas.DataFrame | None = None, +) -> Prepared1DStage: + """Logarithmically rebin a canonical 1D bundle with uncertainty floors.""" + + nbins = _validated_nbins(nbins) + iemin = _validated_relative_floor("iemin", iemin) + qemin = _validated_relative_floor("qemin", qemin) + if nbins <= 0: + raise ValueError("nbins must be positive for 1D rebinning.") + + clipped_data = _frame_for_1d_stage(bundle, source_frame=source_frame) + q_min = clipped_data.Q.dropna().min() + q_max = clipped_data.Q.dropna().max() + if q_min <= 0: + raise ValueError("Logarithmic 1D rebinning requires strictly positive Q values.") + + if np.isclose(q_min, q_max): + rebinnable = clipped_data.loc[:, ["Q", "I", "ISigma"]].copy() + rebinnable["_bin"] = 0 + else: + bin_edges = np.logspace(np.log10(q_min), np.log10(q_max), num=nbins + 1) + bin_edges[-1] = bin_edges[-1] + 1e-3 * (bin_edges[-1] - bin_edges[-2]) + bin_numbers = np.searchsorted(bin_edges, clipped_data["Q"].to_numpy(dtype=float), side="right") - 1 + valid = (bin_numbers >= 0) & (bin_numbers < nbins) + if not np.any(valid): + raise ValueError("1D rebinning produced no populated bins.") + rebinnable = clipped_data.loc[valid, ["Q", "I", "ISigma"]].copy() + rebinnable["_bin"] = bin_numbers[valid] + if "QSigma" in clipped_data.columns: + rebinnable["QSigma"] = clipped_data.loc[valid, "QSigma"].to_numpy(dtype=float) + + rows: list[dict[str, float]] = [] + for _bin_number, df_range in rebinnable.groupby("_bin", sort=True): + mean_q = float(df_range["Q"].mean(skipna=True)) + mean_i = float(df_range["I"].mean(skipna=True)) + + q_error = mean_q * qemin + if "QSigma" in df_range.columns: + q_error = _propagated_mean_sigma(df_range["QSigma"]) + + rows.append( + { + "Q": mean_q, + "I": mean_i, + "ISigma": _bounded_sigma( + propagated_sigma=_propagated_mean_sigma(df_range["ISigma"]), + sem_sigma=_sample_sem(df_range["I"]), + value=mean_i, + relative_floor=iemin, + ), + "QSigma": _bounded_sigma( + propagated_sigma=q_error, + sem_sigma=_sample_sem(df_range["Q"]), + value=mean_q, + relative_floor=qemin, + ), + } + ) + + return _prepared_1d_stage(bundle, pandas.DataFrame(rows, columns=["Q", "I", "ISigma", "QSigma"])) + + +def prepare_1d_bundle( + raw_bundle: Mapping[str, BaseData], + *, + data_range: Sequence[float], + omit_q_ranges: Sequence[Sequence[float]] | None = None, + nbins: int = 0, + iemin: float = 0.01, + qemin: float = 0.01, + source_frame: pandas.DataFrame | None = None, +) -> Prepared1DResult: + """Run the full 1D preprocessing chain on a canonical raw bundle.""" + + _validated_range("data_range", data_range) + _validated_range_list("omit_q_ranges", omit_q_ranges) + nbins = _validated_nbins(nbins) + iemin = _validated_relative_floor("iemin", iemin) + qemin = _validated_relative_floor("qemin", qemin) + clipped = clip_1d_bundle(raw_bundle, data_range=data_range, source_frame=source_frame) + clipped = omit_1d_bundle(clipped.bundle, omit_q_ranges=omit_q_ranges, source_frame=clipped.frame) + if nbins != 0: + binned = rebin_1d_bundle(clipped.bundle, nbins=nbins, iemin=iemin, qemin=qemin, source_frame=clipped.frame) + else: + binned = copy_1d_stage(clipped.bundle, source_frame=clipped.frame) + return Prepared1DResult(clipped=clipped, binned=binned) + + +def _stage_for_2d_bundle( + bundle: Mapping[str, BaseData], source_stage: Mapping[str, Any] | None = None +) -> dict[str, np.ndarray]: + if source_stage is None: + return raw_2d_stage_from_bundle(bundle) + return {key: np.array(value, copy=True) for key, value in source_stage.items()} + + +def clip_2d_bundle( + bundle: Mapping[str, BaseData], + *, + data_range: Sequence[float], + ortho_q0_range: Sequence[float], + ortho_q1_range: Sequence[float], + source_stage: Mapping[str, Any] | None = None, +) -> DataBundle: + """Crop a canonical 2D bundle to the requested radial and orthogonal Q limits.""" + + data_range = _validated_range("data_range", data_range) + ortho_q0_range = _validated_range("ortho_q0_range", ortho_q0_range) + ortho_q1_range = _validated_range("ortho_q1_range", ortho_q1_range) + raw_stage = _stage_for_2d_bundle(bundle, source_stage=source_stage) + intensity = raw_stage["I"] + intensity_sigma = raw_stage["ISigma"] + q1 = raw_stage["Qx"] + q0 = raw_stage["Qy"] + mask = raw_stage.get("mask", np.zeros(intensity.shape, dtype=bool)).astype(bool) + + within_limits = ( + (np.abs(q1) > ortho_q1_range[0]) + & (np.abs(q1) < ortho_q1_range[1]) + & (np.abs(q0) > ortho_q0_range[0]) + & (np.abs(q0) < ortho_q0_range[1]) + & (np.sqrt(q1**2 + q0**2) > data_range[0]) + & (np.sqrt(q1**2 + q0**2) < data_range[1]) + ).astype(bool) & np.invert(mask) + + q0_hits = np.argwhere(within_limits.sum(axis=1) > 0).flatten() + q1_hits = np.argwhere(within_limits.sum(axis=0) > 0).flatten() + if len(q0_hits) == 0: + raise ValueError("Could not determine valid crop limits for axis 0 (y).") + if len(q1_hits) == 0: + raise ValueError("Could not determine valid crop limits for axis 1 (x).") + + q0_limits = (q0_hits.min(), q0_hits.max() + 1) + q1_limits = (q1_hits.min(), q1_hits.max() + 1) + q_units, intensity_units = _bundle_units(bundle) + clipped = bundle_from_2d_arrays( + intensity=intensity[q0_limits[0] : q0_limits[1], q1_limits[0] : q1_limits[1]], + intensity_sigma=intensity_sigma[q0_limits[0] : q0_limits[1], q1_limits[0] : q1_limits[1]], + qx=q1[q0_limits[0] : q0_limits[1], q1_limits[0] : q1_limits[1]], + qy=q0[q0_limits[0] : q0_limits[1], q1_limits[0] : q1_limits[1]], + mask=mask[q0_limits[0] : q0_limits[1], q1_limits[0] : q1_limits[1]], + q_units=q_units, + intensity_units=intensity_units, + ) + return _copy_bundle_metadata(bundle, clipped) + + +def omit_2d_bundle( + bundle: Mapping[str, BaseData], *, omit_q_ranges: Sequence[Sequence[float]] | None = None +) -> DataBundle: + """Return the 2D bundle unchanged while omission remains unsupported.""" + + if omit_q_ranges is not None: + logger.warning("2D omit ranges are not implemented; returning the clipped bundle unchanged.") + return copy_bundle(bundle) + + +def rebin_2d_bundle( + bundle: Mapping[str, BaseData], *, nbins: int, iemin: float = 0.01, qemin: float = 0.01 +) -> DataBundle: + """Return the 2D bundle unchanged while rebinning remains unsupported.""" + + nbins = _validated_nbins(nbins) + iemin = _validated_relative_floor("iemin", iemin) + qemin = _validated_relative_floor("qemin", qemin) + logger.warning( + "2D rebinning is not implemented yet; returning the clipped bundle unchanged (nbins=%s, iemin=%s, qemin=%s).", + nbins, + iemin, + qemin, + ) + return copy_bundle(bundle) + + +def reconstruct_2d_from_clipped_bundle(bundle: Mapping[str, BaseData], model_i_1d: np.ndarray) -> np.ndarray: + """Map flattened model intensities back into the clipped 2D image geometry.""" + + if bundle_dimension(bundle) != 2: + raise ValueError("reconstruct_2d_from_clipped_bundle requires a canonical 2D scattering bundle.") + + intensity = np.asarray(bundle["signal"].signal, dtype=float) + intensity_sigma = _combine_uncertainties(bundle["signal"]) + mask = np.zeros_like(intensity, dtype=bool) + if "mask" in bundle: + mask = np.asarray(bundle["mask"].signal, dtype=bool) + + valid = np.isfinite(intensity) & np.isfinite(intensity_sigma) & (intensity_sigma != 0) & np.invert(mask) + model_i_1d = np.asarray(model_i_1d, dtype=float).reshape(-1) + if valid.sum() != model_i_1d.size: + raise ValueError("Model intensity length does not match the number of valid pixels in the clipped 2D bundle.") + + reconstructed = np.full(intensity.shape, np.nan) + reconstructed[valid] = model_i_1d + return reconstructed + + +def prepare_2d_bundle( + raw_bundle: Mapping[str, BaseData], + *, + data_range: Sequence[float], + ortho_q0_range: Sequence[float], + ortho_q1_range: Sequence[float], + omit_q_ranges: Sequence[Sequence[float]] | None = None, + nbins: int = 0, + iemin: float = 0.01, + qemin: float = 0.01, + source_stage: Mapping[str, Any] | None = None, +) -> Prepared2DResult: + """Run the current 2D preprocessing chain on a canonical raw bundle.""" + + _validated_range("data_range", data_range) + _validated_range("ortho_q0_range", ortho_q0_range) + _validated_range("ortho_q1_range", ortho_q1_range) + _validated_range_list("omit_q_ranges", omit_q_ranges) + nbins = _validated_nbins(nbins) + iemin = _validated_relative_floor("iemin", iemin) + qemin = _validated_relative_floor("qemin", qemin) + clipped = clip_2d_bundle( + raw_bundle, + data_range=data_range, + ortho_q0_range=ortho_q0_range, + ortho_q1_range=ortho_q1_range, + source_stage=source_stage, + ) + clipped = omit_2d_bundle(clipped, omit_q_ranges=omit_q_ranges) + if nbins != 0: + binned = rebin_2d_bundle(clipped, nbins=nbins, iemin=iemin, qemin=qemin) + else: + binned = copy_bundle(clipped) + return Prepared2DResult(clipped=clipped, binned=binned) + + +__all__ = [ + "Prepared1DResult", + "Prepared1DStage", + "Prepared2DResult", + "clip_1d_bundle", + "clip_2d_bundle", + "copy_bundle", + "copy_1d_stage", + "omit_1d_bundle", + "omit_2d_bundle", + "prepare_1d_bundle", + "prepare_2d_bundle", + "rebin_1d_bundle", + "rebin_2d_bundle", + "reconstruct_2d_from_clipped_bundle", +] diff --git a/src/mcsas3/runtime_paths.py b/src/mcsas3/runtime_paths.py new file mode 100644 index 0000000..1241d13 --- /dev/null +++ b/src/mcsas3/runtime_paths.py @@ -0,0 +1,30 @@ +"""Runtime path helpers for source and frozen standalone executions.""" + +from __future__ import annotations + +import sys +from pathlib import Path + + +def _source_checkout_root() -> Path: + return Path(__file__).resolve().parents[2] + + +def runtime_resource_root() -> Path: + """Return the directory that should contain bundled example and test resources.""" + + if getattr(sys, "frozen", False): + return Path(sys.executable).resolve().parent + return _source_checkout_root() + + +def example_configuration_path(filename: str) -> Path: + """Return a path inside the example configuration directory.""" + + return runtime_resource_root() / "example_configurations" / filename + + +def quickstart_testdata_path(filename: str) -> Path: + """Return a path inside the bundled or checkout test-data directory.""" + + return runtime_resource_root() / "testdata" / filename diff --git a/src/mcsas3/workflows.py b/src/mcsas3/workflows.py new file mode 100644 index 0000000..6366a2b --- /dev/null +++ b/src/mcsas3/workflows.py @@ -0,0 +1,343 @@ +from pathlib import Path +from typing import Any, TypeAlias + +import pandas + +from .data_adapters import ( + DEFAULT_ANALYSIS_STAGE, + STAGE_BINNED, + STAGE_CLIPPED, + STAGE_RAW, + bundle_from_1d_dataframe, + bundle_from_2d_stage, + frame_from_bundle, + selected_bundle_from_processing, + set_processing_analysis_stage, +) +from .data_model import BaseData, DataBundle, ProcessingData +from .ingestion import load_1d_dataframe_from_file, load_2d_stage_from_file +from .mc_hat import McHat +from .mc_hdf import PROCESSING_DATA_GROUP, ResultIndex, loadProcessingData, storeKVPairs, storeProcessingData +from .preprocessing import copy_bundle, prepare_1d_bundle, prepare_2d_bundle + +CanonicalBundleLike: TypeAlias = DataBundle | dict[str, BaseData] +Legacy2DStageLike: TypeAlias = dict[str, Any] + + +def _empty_processing() -> ProcessingData: + """Create an empty canonical processing carrier.""" + + return ProcessingData() + + +def _result_mcdata_path(result_index: int) -> Path: + """Return the canonical `mcdata` group path for a result index.""" + + return ResultIndex(result_index).nxsEntryPoint / "mcdata" + + +def _normalized_1d_file_workflow_config(read_config: dict[str, Any]) -> dict[str, Any]: + """Normalize accepted 1D file-ingest keyword aliases into workflow config keys.""" + + aliases = { + "QUnits": "sourceQUnits", + "IUnits": "sourceIntensityUnits", + "Q_units": "sourceQUnits", + "I_units": "sourceIntensityUnits", + "QEMin": "qemin", + } + defaults = { + "loader": None, + "csvargs": None, + "pathDict": None, + "dataRange": [-float("inf"), float("inf")], + "omitQRanges": None, + "nbins": 100, + "IEmin": 0.01, + "qemin": 0.01, + "analysisStage": DEFAULT_ANALYSIS_STAGE, + "sourceQUnits": None, + "sourceIntensityUnits": None, + } + normalized = defaults.copy() + + for key, value in read_config.items(): + normalized_key = aliases.get(key, key) + if normalized_key not in normalized: + raise ValueError(f"Unsupported 1D workflow configuration key '{key}'.") + + if normalized[normalized_key] != defaults[normalized_key] and normalized[normalized_key] != value: + raise ValueError( + f"Conflicting configuration values provided for '{normalized_key}': " + f"{normalized[normalized_key]!r} and {value!r}." + ) + normalized[normalized_key] = value + + return normalized + + +def _normalized_2d_file_workflow_config(read_config: dict[str, Any]) -> dict[str, Any]: + """Normalize accepted 2D file-ingest keyword aliases into workflow config keys.""" + + aliases = { + "QUnits": "sourceQUnits", + "IUnits": "sourceIntensityUnits", + "Q_units": "sourceQUnits", + "I_units": "sourceIntensityUnits", + "QEMin": "qemin", + } + defaults = { + "loader": None, + "pathDict": None, + "dataRange": [0, float("inf")], + "orthoQ0Range": [0, float("inf")], + "orthoQ1Range": [0, float("inf")], + "omitQRanges": None, + "nbins": 100, + "IEmin": 0.01, + "qemin": 0.01, + "analysisStage": DEFAULT_ANALYSIS_STAGE, + "sourceQUnits": None, + "sourceIntensityUnits": None, + } + normalized = defaults.copy() + + for key, value in read_config.items(): + normalized_key = aliases.get(key, key) + if normalized_key not in normalized: + raise ValueError(f"Unsupported 2D workflow configuration key '{key}'.") + + if normalized[normalized_key] != defaults[normalized_key] and normalized[normalized_key] != value: + raise ValueError( + f"Conflicting configuration values provided for '{normalized_key}': " + f"{normalized[normalized_key]!r} and {value!r}." + ) + normalized[normalized_key] = value + + return normalized + + +def prepare_1d_processing_data( + raw_data: pandas.DataFrame | CanonicalBundleLike, + *, + data_range: tuple[float, float] | list[float] | None = None, + omit_q_ranges: list[list[float]] | tuple[tuple[float, float], ...] | None = None, + nbins: int = 0, + iemin: float = 0.01, + qemin: float = 0.01, + analysis_stage: str = DEFAULT_ANALYSIS_STAGE, + source_q_units: Any | None = None, + source_intensity_units: Any | None = None, +) -> ProcessingData: + """Build canonical 1D ProcessingData directly from a raw table or bundle.""" + + if data_range is None: + data_range = [-float("inf"), float("inf")] + + source_frame = None + if isinstance(raw_data, pandas.DataFrame): + raw_bundle = bundle_from_1d_dataframe( + raw_data, + source_q_units=source_q_units, + source_intensity_units=source_intensity_units, + ) + source_frame = frame_from_bundle(raw_bundle) + elif isinstance(raw_data, dict): + raw_bundle = copy_bundle(raw_data) + else: + raw_bundle = copy_bundle(raw_data) + + prepared = prepare_1d_bundle( + raw_bundle, + data_range=data_range, + omit_q_ranges=omit_q_ranges, + nbins=nbins, + iemin=iemin, + qemin=qemin, + source_frame=source_frame, + ) + processing = _empty_processing() + processing[STAGE_RAW] = copy_bundle(raw_bundle) + processing[STAGE_CLIPPED] = prepared.clipped.bundle + processing[STAGE_BINNED] = prepared.binned.bundle + set_processing_analysis_stage(processing, analysis_stage) + return processing + + +def prepare_2d_processing_data( + raw_data: CanonicalBundleLike | Legacy2DStageLike, + *, + data_range: tuple[float, float] | list[float], + ortho_q0_range: tuple[float, float] | list[float], + ortho_q1_range: tuple[float, float] | list[float], + omit_q_ranges: list[list[float]] | tuple[tuple[float, float], ...] | None = None, + nbins: int = 0, + iemin: float = 0.01, + qemin: float = 0.01, + analysis_stage: str = DEFAULT_ANALYSIS_STAGE, + source_q_units: Any | None = None, + source_intensity_units: Any | None = None, +) -> ProcessingData: + """Build canonical 2D ProcessingData from a raw stage dict or canonical 2D bundle.""" + + if isinstance(raw_data, DataBundle): + canonical_raw = copy_bundle(raw_data) + elif isinstance(raw_data, dict) and raw_data and all(isinstance(value, BaseData) for value in raw_data.values()): + canonical_raw = copy_bundle(raw_data) + else: + canonical_raw = bundle_from_2d_stage( + raw_data, + source_q_units=source_q_units, + source_intensity_units=source_intensity_units, + ) + prepared = prepare_2d_bundle( + canonical_raw, + data_range=data_range, + ortho_q0_range=ortho_q0_range, + ortho_q1_range=ortho_q1_range, + omit_q_ranges=omit_q_ranges, + nbins=nbins, + iemin=iemin, + qemin=qemin, + ) + processing = _empty_processing() + processing[STAGE_RAW] = canonical_raw + processing[STAGE_CLIPPED] = prepared.clipped + processing[STAGE_BINNED] = prepared.binned + set_processing_analysis_stage(processing, analysis_stage) + return processing + + +def prepare_2d_processing_data_from_file( + filename: Path, + *, + result_index: int = 1, + **read_config: Any, +) -> ProcessingData: + """File-ingest helper returning canonical 2D ProcessingData from a source file.""" + + _ = result_index + config = _normalized_2d_file_workflow_config(read_config) + loaded = load_2d_stage_from_file( + filename, + loader=config["loader"], + path_dict=config["pathDict"], + ) + source_q_units = config["sourceQUnits"] if config["sourceQUnits"] is not None else loaded.source_q_units + source_intensity_units = ( + config["sourceIntensityUnits"] if config["sourceIntensityUnits"] is not None else loaded.source_intensity_units + ) + raw_bundle = bundle_from_2d_stage( + loaded.stage, + source_q_units=source_q_units, + source_intensity_units=source_intensity_units, + ) + return prepare_2d_processing_data( + raw_bundle, + data_range=config["dataRange"], + ortho_q0_range=config["orthoQ0Range"], + ortho_q1_range=config["orthoQ1Range"], + omit_q_ranges=config["omitQRanges"], + nbins=config["nbins"], + iemin=config["IEmin"], + qemin=config["qemin"], + analysis_stage=config["analysisStage"], + ) + + +def prepare_1d_processing_data_from_file( + filename: Path, + *, + result_index: int = 1, + **read_config: Any, +) -> ProcessingData: + """File-ingest helper returning canonical 1D ProcessingData from a source file.""" + + config = _normalized_1d_file_workflow_config(read_config) + loaded = load_1d_dataframe_from_file( + filename, + loader=config["loader"], + csvargs=config["csvargs"], + path_dict=config["pathDict"], + ) + source_q_units = config["sourceQUnits"] if config["sourceQUnits"] is not None else loaded.source_q_units + source_intensity_units = ( + config["sourceIntensityUnits"] if config["sourceIntensityUnits"] is not None else loaded.source_intensity_units + ) + return prepare_1d_processing_data( + loaded.frame, + data_range=config["dataRange"], + omit_q_ranges=config["omitQRanges"], + nbins=config["nbins"], + iemin=config["IEmin"], + qemin=config["qemin"], + analysis_stage=config["analysisStage"], + source_q_units=source_q_units, + source_intensity_units=source_intensity_units, + ) + + +def load_result_processing_data(filename: Path, *, result_index: int = 1) -> ProcessingData: + """Load canonical ProcessingData from an McSAS3 result file.""" + + path = _result_mcdata_path(result_index) + processing = loadProcessingData(filename, path / PROCESSING_DATA_GROUP, default=None) + if processing is None: + raise ValueError( + f"Result file {filename} does not contain canonical processing data at {path / PROCESSING_DATA_GROUP}." + ) + return processing + + +def store_result_processing_data( + filename: Path, + processing: ProcessingData, + *, + result_index: int = 1, + metadata: dict[str, Any] | None = None, +) -> None: + """Store canonical ProcessingData and optional metadata into an McSAS3 result file.""" + + path = _result_mcdata_path(result_index) + storeProcessingData(filename=filename, path=path / PROCESSING_DATA_GROUP, processing=processing) + if metadata: + storeKVPairs(filename, path, metadata.items()) + + +def optimize_processing_data( + processing: ProcessingData, + result_file: Path, + *, + result_index: int = 1, + store_processing: bool = True, + processing_metadata: dict[str, Any] | None = None, + hat: McHat | None = None, + **hat_kwargs: Any, +) -> McHat: + """Run McSAS optimization from canonical ProcessingData.""" + + if hat is not None and hat_kwargs: + raise ValueError("Provide either an McHat instance or McHat keyword arguments, not both.") + + if store_processing: + store_result_processing_data( + result_file, + processing, + result_index=result_index, + metadata=processing_metadata, + ) + + active_hat = hat if hat is not None else McHat(resultIndex=result_index, **hat_kwargs) + active_hat.run(selected_bundle_from_processing(processing), result_file, resultIndex=result_index) + return active_hat + + +__all__ = [ + "load_result_processing_data", + "optimize_processing_data", + "prepare_1d_processing_data", + "prepare_1d_processing_data_from_file", + "prepare_2d_processing_data", + "prepare_2d_processing_data_from_file", + "store_result_processing_data", +] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e46b3d7 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,56 @@ +import pytest + +INTEGRATION_TEST_FILES = {"test_optimizer_integraltest.py"} + + +def pytest_addoption(parser): + parser.addoption( + "--run-integration", + action="store_true", + default=False, + help="Run integration tests that exercise multiple layers or file-backed workflows.", + ) + parser.addoption( + "--run-slow", + action="store_true", + default=False, + help="Run explicitly slow tests. Implies --run-integration.", + ) + + +def pytest_ignore_collect(collection_path, config): + run_slow = config.getoption("--run-slow") + run_integration = config.getoption("--run-integration") or run_slow + + if not run_integration and collection_path.name in INTEGRATION_TEST_FILES: + return True + + return False + + +def pytest_collection_modifyitems(config, items): + run_slow = config.getoption("--run-slow") + run_integration = config.getoption("--run-integration") or run_slow + + skipped = [] + selected = [] + + for item in items: + is_integration = item.get_closest_marker("integration") is not None + is_slow = item.get_closest_marker("slow") is not None + + if is_slow and not run_slow: + item.add_marker(pytest.mark.skip(reason="need --run-slow option to run")) + skipped.append(item) + continue + + if is_integration and not run_integration: + item.add_marker(pytest.mark.skip(reason="need --run-integration option to run")) + skipped.append(item) + continue + + selected.append(item) + + if skipped: + config.hook.pytest_deselected(items=skipped) + items[:] = selected diff --git a/tests/test_McData1D_unittest.py b/tests/test_McData1D_unittest.py deleted file mode 100644 index 7a430d1..0000000 --- a/tests/test_McData1D_unittest.py +++ /dev/null @@ -1,141 +0,0 @@ -import shutil # for copy - -# these need to be loaded at the beginning to avoid errors related to relative imports -# (ImportWarning in h5py), might be related to the change of import style for Python 3.5+. -# Tested on Python 3.7 at 20200417 -import unittest -from pathlib import Path - -# these packages are failing to import in mchat if they are not loaded here: -import numpy as np -import pandas - -# for reading configuration files -import yaml - -# %matplotlib inline -# import matplotlib.pyplot as plt -from mcsas3 import mc_data_1d - -# import warnings -# warnings.filterwarnings('error') - - -class testMcData1D(unittest.TestCase): - def test_mcdata1d_instantiated(self): - md = mc_data_1d.McData1D() - md.from_pdh(filename=r"testdata/S2870 BSA THF 1 1 d.pdh") - self.assertIsNotNone(md.measData, "measData is not populated") - self.assertTrue("Q" in md.measData.keys()) - - def test_mcdata1d_singleLine(self): - md = mc_data_1d.McData1D(filename=Path(r"testdata/S2870 BSA THF 1 1 d.pdh")) - self.assertIsNotNone(md.measData, "measData is not populated") - self.assertTrue("Q" in md.measData.keys()) - - def test_mcdata1d_singleLineWithOptions(self): - md = mc_data_1d.McData1D( - filename=Path(r"testdata/S2870 BSA THF 1 1 d.pdh"), dataRange=[0.1, 0.6], nbins=50 - ) - self.assertIsNotNone(md.measData, "measData is not populated") - self.assertTrue("Q" in md.measData.keys(), "measData does not contain Q") - self.assertTrue(np.min(md.measData["Q"]) > 0.1, "clipper has not applied minimum") - self.assertTrue(np.max(md.measData["Q"]) < 0.6, "clipper has not applied maximum") - self.assertTrue(len(md.measData["Q"]) < 51, "rebinner has not rebinned to <51 bins") - - def test_mcdata1d_csv(self): - md = mc_data_1d.McData1D( - filename=Path("testdata", "quickstartdemo1.csv"), - nbins=100, - IEmin=0.045, - csvargs={"sep": ";", "header": None, "names": ["Q", "I", "ISigma"]}, - ) - self.assertIsNotNone(md.measData, "measData is not populated") - self.assertTrue("Q" in md.measData.keys(), "measData does not contain Q") - self.assertTrue(len(md.measData["Q"]) < 101, "rebinner has not rebinned to <51 bins") - - def test_mcdata1d_nxs_with_omit_from_yaml(self): - readConfigFile = Path("example_configurations", "read_config_nxs_with_omit.yaml") - filename = Path("testdata", "datamerge.nxs") - with open(readConfigFile, "r") as f: - readDict = yaml.safe_load(f) - # try loading the data - _ = mc_data_1d.McData1D(filename=filename, resultIndex=0, **readDict) - - def test_mcdata1d_csv_norebin(self): - md = mc_data_1d.McData1D( - filename=Path("testdata", "quickstartdemo1.csv"), - nbins=0, - csvargs={"sep": ";", "header": None, "names": ["Q", "I", "ISigma"]}, - ) - self.assertIsNotNone(md.measData, "measData is not populated") - self.assertTrue("Q" in md.measData.keys(), "measData does not contain Q") - self.assertTrue(len(md.measData["I"]) == len(md.rawData), "rebinner has not been bypassed") - - def test_restore_state(self): - if Path("test_state.h5").is_file(): - Path("test_state.h5").unlink() - - mds = mc_data_1d.McData1D( - filename=Path("testdata", "quickstartdemo1.csv"), - nbins=100, - csvargs={"sep": ";", "header": None, "names": ["Q", "I", "ISigma"]}, - ) - mds.store(filename="test_state.h5") - del mds - _ = mc_data_1d.McData1D(loadFromFile=Path("test_state.h5")) - - # def test_restore_state_withIndex(self): - # md = McData1D.McData1D( - # loadFromFile=Path("testdata", "merged_096.nxs"), resultIndex=2 - # ) - - def test_restore_state_fromDataframe(self): - # create state: - if Path("test_state_df.h5").is_file(): - Path("test_state_df.h5").unlink() - - # test dataframe: - Q = np.linspace(0.01, 0.99, 100, dtype=float) - Int = Q**-4 - ISigma = Int * 0.01 - testdf = pandas.DataFrame(data={"Q": Q, "I": Int, "ISigma": ISigma}) - od = mc_data_1d.McData1D(df=testdf) - od.store(filename="test_state_df.h5") - del od - _ = mc_data_1d.McData1D(loadFromFile=Path("test_state_df.h5")) - - def test_from_nxsas(self): - # tests whether McData can load from NXsas - hpath = Path("testdata", "20190725_11_expanded_stacked_processed_190807_161306.nxs") - _ = mc_data_1d.McData1D(filename=hpath) - - def test_restore_state_from_nxsas(self): - # tests whether I can restore state from a nexus file-derived McSAS state file - hpath = Path("testdata", "20190725_11_expanded_stacked_processed_190807_161306.nxs") - od = mc_data_1d.McData1D(filename=hpath) - od.store(filename="test_state_nxsas.h5") - del od - _ = mc_data_1d.McData1D(loadFromFile=Path("test_state_nxsas.h5")) - - def test_nxsas_io(self): - # tests whether I can read and write in the same nexus file - if Path("testdata", "test_nexus_io.nxs").is_file(): - Path("testdata", "test_nexus_io.nxs").unlink() - hpath = Path("testdata", "20190725_11_expanded_stacked_processed_190807_161306.nxs") - tpath = Path("testdata", "test_nexus_io.nxs") - shutil.copy(hpath, tpath) - - od = mc_data_1d.McData1D(filename=tpath) - od.store(filename=tpath) - del od - _ = mc_data_1d.McData1D(loadFromFile=tpath) - - def test_read_datamerge(self): - # tests whether I can read a file written by datamerge v2.5 - hpath = Path("testdata", "datamerge.nxs") - _ = mc_data_1d.McData1D(filename=hpath) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_McData2D_unittest.py b/tests/test_McData2D_unittest.py deleted file mode 100644 index b988431..0000000 --- a/tests/test_McData2D_unittest.py +++ /dev/null @@ -1,24 +0,0 @@ -# these need to be loaded at the beginning to avoid errors related to relative imports -# (ImportWarning in h5py), might be related to the change of import style for Python 3.5+. -# Tested on Python 3.7 at 20200417 -import unittest -from pathlib import Path - -# %matplotlib inline -# import matplotlib.pyplot as plt -from mcsas3 import mc_data_2d - -# import warnings -# warnings.filterwarnings('error') - - -class testMcData2D(unittest.TestCase): - def test_mcdata2d_instantiated(self): - md = mc_data_2d.McData2D() - md.from_nexus(filename=Path(r"testdata/009766_forSasView.h5")) - self.assertIsNotNone(md.measData, "measData is not populated") - self.assertTrue("Q" in md.measData.keys()) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_build_standalone_fast.py b/tests/test_build_standalone_fast.py new file mode 100644 index 0000000..f0ca373 --- /dev/null +++ b/tests/test_build_standalone_fast.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def _load_build_standalone_module(): + module_path = Path(__file__).resolve().parents[1] / "tools" / "build_standalone.py" + spec = importlib.util.spec_from_file_location("build_standalone", module_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_build_standalone_uses_local_sasmodels_hook(tmp_path): + module = _load_build_standalone_module() + args = module._pyinstaller_args("demo", Path("demo.py"), tmp_path, ()) + assert "--additional-hooks-dir" in args + assert str(module.HOOKS_DIR) in args + assert "--collect-all" not in args + + +def test_build_standalone_explicitly_includes_modacor_hidden_imports(tmp_path): + module = _load_build_standalone_module() + args = module._pyinstaller_args("demo", Path("demo.py"), tmp_path, ()) + + assert "modacor" in args + assert "modacor.units" in args + assert "modacor.dataclasses.basedata" in args + assert "modacor.dataclasses.databundle" in args + assert "modacor.dataclasses.processing_data" in args diff --git a/tests/test_cli_tools_fast.py b/tests/test_cli_tools_fast.py new file mode 100644 index 0000000..8bc32b0 --- /dev/null +++ b/tests/test_cli_tools_fast.py @@ -0,0 +1,127 @@ +import pandas +import pytest + +import mcsas3.cli_histogram as cli_histogram +import mcsas3.cli_tools as cli_tools + + +def test_cli_optimize_uses_processing_data_workflow(monkeypatch, tmp_path): + data_file = tmp_path / "input.dat" + data_file.write_text("0.1 1.0 0.1\n") + result_file = tmp_path / "result.h5" + read_config_file = tmp_path / "read.yaml" + read_config_file.write_text("nbins: 5\ndataRange: [1.0, 4.0]\n") + run_config_file = tmp_path / "run.yaml" + run_config_file.write_text("modelName: sphere\nnRep: 3\n") + + processing = object() + calls: dict[str, object] = {} + + def fake_prepare(filename, *, result_index, **read_config): + calls["prepare"] = (filename, result_index, read_config) + return processing + + def fake_optimize(processing_input, result_path, **kwargs): + calls["optimize"] = (processing_input, result_path, kwargs) + + monkeypatch.setattr(cli_tools.workflows, "prepare_1d_processing_data_from_file", fake_prepare) + monkeypatch.setattr(cli_tools.workflows, "optimize_processing_data", fake_optimize) + + cli_tools.McSAS3_cli_optimize( + dataFile=data_file, + resultFile=result_file, + readConfigFile=read_config_file, + runConfigFile=run_config_file, + resultIndex=2, + deleteIfExists=False, + nThreads=4, + ) + + assert calls["prepare"] == (data_file, 2, {"nbins": 5, "dataRange": [1.0, 4.0]}) + optimize_processing, optimize_result, optimize_kwargs = calls["optimize"] + assert optimize_processing is processing + assert optimize_result == result_file + assert optimize_kwargs["result_index"] == 2 + assert optimize_kwargs["seed"] is None + assert optimize_kwargs["nCores"] == 4 + assert optimize_kwargs["modelName"] == "sphere" + assert optimize_kwargs["nRep"] == 3 + assert optimize_kwargs["processing_metadata"]["filename"] == data_file + assert optimize_kwargs["processing_metadata"]["nbins"] == 5 + + +def test_cli_histogram_uses_processing_data_workflow(monkeypatch, tmp_path): + result_file = tmp_path / "result.h5" + result_file.write_text("placeholder") + hist_config_file = tmp_path / "hist.yaml" + hist_config_file.write_text("parameter: radius\nnBin: 20\n") + + processing = object() + analysis_result = object() + calls: dict[str, object] = {} + + def fake_load(filename, *, result_index): + calls["load"] = (filename, result_index) + return processing + + def fake_analysis(input_file, analysis_data, hist_ranges, store=False, resultIndex=1): + calls["analysis"] = (input_file, analysis_data, hist_ranges.copy(), store, resultIndex) + return analysis_result + + class FakePlot: + def resultCard(self, mcres, saveHistFile=None): + calls["plot"] = (mcres, saveHistFile) + + monkeypatch.setattr(cli_tools.workflows, "load_result_processing_data", fake_load) + monkeypatch.setattr(cli_histogram, "McAnalysis", fake_analysis) + monkeypatch.setattr(cli_histogram, "McPlot", FakePlot) + + cli_tools.McSAS3_cli_histogram( + resultFile=result_file, + histConfigFile=hist_config_file, + resultIndex=3, + ) + + assert calls["load"] == (result_file, 3) + analysis_input_file, analysis_processing, hist_ranges, store, result_index = calls["analysis"] + assert analysis_input_file == result_file + assert analysis_processing is processing + assert isinstance(hist_ranges, pandas.DataFrame) + assert hist_ranges.loc[0, "parameter"] == "radius" + assert hist_ranges.loc[0, "nBin"] == 20 + assert store is True + assert result_index == 3 + assert calls["plot"] == (analysis_result, result_file.with_suffix(".pdf")) + + +def test_cli_optimize_requires_yaml_config_files(tmp_path): + data_file = tmp_path / "input.dat" + data_file.write_text("0.1 1.0 0.1\n") + result_file = tmp_path / "result.h5" + read_config_file = tmp_path / "read.txt" + read_config_file.write_text("nbins: 5\n") + run_config_file = tmp_path / "run.yaml" + run_config_file.write_text("modelName: sphere\n") + + with pytest.raises(ValueError, match="readConfigFile file must be a yaml file"): + cli_tools.McSAS3_cli_optimize( + dataFile=data_file, + resultFile=result_file, + readConfigFile=read_config_file, + runConfigFile=run_config_file, + resultIndex=1, + deleteIfExists=False, + nThreads=1, + ) + + +def test_cli_histogram_requires_existing_config_file(tmp_path): + result_file = tmp_path / "result.h5" + result_file.write_text("placeholder") + + with pytest.raises(FileNotFoundError, match="histConfigFile file .* must exist"): + cli_tools.McSAS3_cli_histogram( + resultFile=result_file, + histConfigFile=tmp_path / "missing.yaml", + resultIndex=1, + ) diff --git a/tests/test_data_adapters_fast.py b/tests/test_data_adapters_fast.py new file mode 100644 index 0000000..c55f2a6 --- /dev/null +++ b/tests/test_data_adapters_fast.py @@ -0,0 +1,216 @@ +import numpy as np +import pandas +import pandas.testing as pdt +import pytest + +from mcsas3.data_adapters import ( + DEFAULT_ANALYSIS_STAGE, + DEFAULT_INTENSITY_UNITS, + DEFAULT_Q_UNITS, + STAGE_BINNED, + STAGE_CLIPPED, + STAGE_RAW, + bundle_from_1d_dataframe, + bundle_from_2d_stage, + fit_arrays_from_bundle, + frame_from_bundle, + get_processing_analysis_stage, + selected_bundle_from_processing, + set_processing_analysis_stage, +) +from mcsas3.data_model import ( + MODACOR_IMPORT_MODE, + BaseData, + DataBundle, + ProcessingData, + ureg, +) +from mcsas3.workflows import prepare_1d_processing_data, prepare_2d_processing_data + + +def test_modacor_import_layer_exposes_real_types(): + assert MODACOR_IMPORT_MODE == "installed" + + bundle = DataBundle() + bundle["signal"] = BaseData( + signal=np.array([1.0]), + units=ureg.dimensionless, + uncertainties={"propagate_to_all": np.array([0.1])}, + rank_of_data=1, + ) + + processing = ProcessingData() + processing[STAGE_RAW] = bundle + + assert processing[STAGE_RAW]["signal"].signal.shape == (1,) + + +def test_1d_bundle_adapter_round_trips_dataframe_and_fit_arrays(): + frame = pandas.DataFrame( + { + "Q": np.array([0.5, 1.0, 2.0], dtype=float), + "I": np.array([5.0, 10.0, 20.0], dtype=float), + "ISigma": np.array([0.5, 1.0, 2.0], dtype=float), + "QSigma": np.array([0.05, 0.1, 0.2], dtype=float), + "mask": np.array([False, True, False], dtype=bool), + } + ) + + bundle = bundle_from_1d_dataframe(frame) + + assert bundle["signal"].units == DEFAULT_INTENSITY_UNITS + assert bundle["Q"].units == DEFAULT_Q_UNITS + + q_arrays, intensity, sigma = fit_arrays_from_bundle(bundle) + np.testing.assert_allclose(q_arrays[0], np.array([0.5, 1.0, 2.0])) + np.testing.assert_allclose(intensity, frame["I"].to_numpy()) + np.testing.assert_allclose(sigma, frame["ISigma"].to_numpy()) + + pdt.assert_frame_equal(frame_from_bundle(bundle), frame) + + +def test_2d_bundle_adapter_builds_canonical_bundle_and_filters_fit_arrays(): + stage = { + "I2D": np.array([[5.0, 6.0], [9.0, 10.0]], dtype=float), + "ISigma2D": np.array([[1.0, 0.0], [1.0, 1.0]], dtype=float), + "Q0Crop2D": np.array([[-0.5, -0.5], [0.5, 0.5]], dtype=float), + "Q1Crop2D": np.array([[-0.5, 0.5], [-0.5, 0.5]], dtype=float), + "mask2D": np.array([[True, False], [False, False]], dtype=bool), + } + + bundle = bundle_from_2d_stage(stage) + + assert bundle["signal"].units == DEFAULT_INTENSITY_UNITS + assert bundle["Qx"].units == DEFAULT_Q_UNITS + assert bundle["Qy"].units == DEFAULT_Q_UNITS + + q_arrays, intensity, sigma = fit_arrays_from_bundle(bundle) + np.testing.assert_allclose(q_arrays[0], np.array([0.5, 0.5])) + np.testing.assert_allclose(q_arrays[1], np.array([-0.5, 0.5])) + np.testing.assert_allclose(intensity, np.array([9.0, 10.0])) + np.testing.assert_allclose(sigma, np.array([1.0, 1.0])) + + frame = frame_from_bundle(bundle) + assert list(frame.columns) == ["Qx", "Qy", "I", "ISigma", "mask"] + assert len(frame) == 4 + + +def test_1d_bundle_adapter_normalizes_declared_source_units_to_canonical_defaults(): + frame = pandas.DataFrame( + { + "Q": np.array([0.1, 0.2], dtype=float), + "I": np.array([1.0, 2.0], dtype=float), + "ISigma": np.array([0.1, 0.2], dtype=float), + "QSigma": np.array([0.01, 0.02], dtype=float), + } + ) + + bundle = bundle_from_1d_dataframe( + frame, + source_q_units="1 / angstrom", + source_intensity_units="1 / centimeter / steradian", + ) + + assert bundle["signal"].units == DEFAULT_INTENSITY_UNITS + assert bundle["Q"].units == DEFAULT_Q_UNITS + np.testing.assert_allclose(bundle["signal"].signal, np.array([100.0, 200.0])) + np.testing.assert_allclose(bundle["signal"].uncertainties["propagate_to_all"], np.array([10.0, 20.0])) + np.testing.assert_allclose(bundle["Q"].signal, np.array([1.0, 2.0])) + np.testing.assert_allclose(bundle["Q"].uncertainties["propagate_to_all"], np.array([0.1, 0.2])) + + +def test_2d_bundle_adapter_normalizes_declared_source_units_to_canonical_defaults(): + stage = { + "I2D": np.array([[1.0, 2.0], [3.0, 4.0]], dtype=float), + "ISigma2D": np.array([[0.1, 0.2], [0.3, 0.4]], dtype=float), + "Q0Crop2D": np.array([[-0.05, -0.05], [0.05, 0.05]], dtype=float), + "Q1Crop2D": np.array([[-0.05, 0.05], [-0.05, 0.05]], dtype=float), + "mask2D": np.array([[False, False], [False, True]], dtype=bool), + } + + bundle = bundle_from_2d_stage( + stage, + source_q_units="1/A", + source_intensity_units="1 / centimeter / steradian", + ) + + assert bundle["signal"].units == DEFAULT_INTENSITY_UNITS + assert bundle["Qx"].units == DEFAULT_Q_UNITS + assert bundle["Qy"].units == DEFAULT_Q_UNITS + np.testing.assert_allclose(bundle["signal"].signal, np.array([[100.0, 200.0], [300.0, 400.0]])) + np.testing.assert_allclose(bundle["Qx"].signal, np.array([[-0.5, 0.5], [-0.5, 0.5]])) + np.testing.assert_allclose(bundle["Qy"].signal, np.array([[-0.5, -0.5], [0.5, 0.5]])) + + +def test_2d_bundle_adapter_rejects_mismatched_component_shapes(): + stage = { + "I2D": np.array([[1.0, 2.0], [3.0, 4.0]], dtype=float), + "ISigma2D": np.array([[0.1, 0.2], [0.3, 0.4]], dtype=float), + "Q0Crop2D": np.array([[-0.05, -0.05], [0.05, 0.05]], dtype=float), + "Q1Crop2D": np.array([[-0.05, 0.05]], dtype=float), + } + + with pytest.raises(ValueError, match="q1 shape"): + bundle_from_2d_stage(stage) + + +def test_prepare_1d_processing_data_matches_selected_binned_fit_arrays(): + frame = pandas.DataFrame( + { + "Q": np.array([0.5, 1.0, 2.0, 4.0, 5.0], dtype=float), + "I": np.array([5.0, 10.0, 20.0, 40.0, 50.0], dtype=float), + "ISigma": np.array([0.5, 1.0, 2.0, 4.0, 5.0], dtype=float), + } + ) + processing = prepare_1d_processing_data( + frame, + data_range=[1.0, 5.0], + omit_q_ranges=[[1.5, 3.0]], + nbins=0, + ) + + assert set(processing.keys()) == {STAGE_RAW, STAGE_CLIPPED, STAGE_BINNED} + bridged = fit_arrays_from_bundle(processing[STAGE_BINNED]) + direct = fit_arrays_from_bundle(selected_bundle_from_processing(processing)) + np.testing.assert_allclose(bridged[0][0], direct[0][0]) + np.testing.assert_allclose(bridged[1], direct[1]) + np.testing.assert_allclose(bridged[2], direct[2]) + + +def test_prepare_2d_processing_data_matches_selected_binned_fit_arrays(): + coords = np.array([-1.5, -0.5, 0.5, 1.5], dtype=float) + qx, qy = np.meshgrid(coords, coords) + intensity = np.arange(16, dtype=float).reshape(4, 4) + sigma = np.ones((4, 4), dtype=float) + mask = np.zeros((4, 4), dtype=bool) + mask[1, 1] = True + sigma[1, 2] = 0.0 + processing = prepare_2d_processing_data( + {"Qx": qx, "Qy": qy, "I": intensity, "ISigma": sigma, "mask": mask}, + data_range=[0.0, 1.0], + ortho_q0_range=[0.0, 1.0], + ortho_q1_range=[0.0, 1.0], + nbins=0, + ) + + assert set(processing.keys()) == {STAGE_RAW, STAGE_CLIPPED, STAGE_BINNED} + bridged = fit_arrays_from_bundle(processing[STAGE_BINNED]) + direct = fit_arrays_from_bundle(selected_bundle_from_processing(processing)) + np.testing.assert_allclose(bridged[0][0], direct[0][0]) + np.testing.assert_allclose(bridged[0][1], direct[0][1]) + np.testing.assert_allclose(bridged[1], direct[1]) + np.testing.assert_allclose(bridged[2], direct[2]) + + +def test_processing_data_tracks_selected_analysis_stage(): + processing = ProcessingData() + processing[STAGE_RAW] = DataBundle() + processing[STAGE_CLIPPED] = DataBundle() + processing[STAGE_BINNED] = DataBundle() + + assert get_processing_analysis_stage(processing) == DEFAULT_ANALYSIS_STAGE + + set_processing_analysis_stage(processing, STAGE_CLIPPED) + + assert get_processing_analysis_stage(processing) == STAGE_CLIPPED + assert selected_bundle_from_processing(processing) is processing[STAGE_CLIPPED] diff --git a/tests/test_dependency_diagram_fast.py b/tests/test_dependency_diagram_fast.py new file mode 100644 index 0000000..cbca484 --- /dev/null +++ b/tests/test_dependency_diagram_fast.py @@ -0,0 +1,23 @@ +import importlib.util +from pathlib import Path + + +def _load_dependency_diagram_module(): + root = Path(__file__).resolve().parents[1] + module_path = root / "tools" / "generate_dependency_diagram.py" + spec = importlib.util.spec_from_file_location("generate_dependency_diagram", module_path) + if spec is None or spec.loader is None: + raise RuntimeError("Could not load dependency diagram generator module.") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_generated_dependency_diagram_is_current(): + root = Path(__file__).resolve().parents[1] + module = _load_dependency_diagram_module() + + expected = module.generate_markdown() + actual = (root / "design_documentation" / "generated_module_dependencies.md").read_text(encoding="utf-8") + + assert actual == expected diff --git a/tests/test_ingestion_fast.py b/tests/test_ingestion_fast.py new file mode 100644 index 0000000..23aea53 --- /dev/null +++ b/tests/test_ingestion_fast.py @@ -0,0 +1,171 @@ +import h5py +import numpy as np +import pandas +import pandas.testing as pdt +import pytest + +from mcsas3.ingestion import DEFAULT_1D_CSVARGS, load_1d_dataframe_from_file, load_2d_stage_from_file + + +def _write_test_2d_nexus(filename): + qx = np.array([[-0.5, 0.5], [-0.5, 0.5]], dtype=float) + qy = np.array([[-0.5, -0.5], [0.5, 0.5]], dtype=float) + q = np.stack([qy, qx, np.zeros_like(qx)], axis=0) + intensity = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=float) + sigma = np.array([[0.1, 0.2], [0.3, 0.4]], dtype=float) + mask = np.array([[False, True], [False, False]], dtype=bool) + + with h5py.File(filename, "w") as h5f: + h5f.attrs["default"] = "entry" + entry = h5f.create_group("entry") + entry.attrs["default"] = "data" + data = entry.create_group("data") + data.attrs["signal"] = "I" + data.attrs["I_uncertainty"] = "I_unc" + data.attrs["mask"] = "mask" + data.attrs["axes"] = np.array(["q"], dtype="S") + signal = data.create_dataset("I", data=intensity) + signal.attrs["units"] = "1 / centimeter / steradian" + sigma_ds = data.create_dataset("I_unc", data=sigma) + sigma_ds.attrs["units"] = "1 / centimeter / steradian" + q_ds = data.create_dataset("q", data=q) + q_ds.attrs["units"] = "1 / angstrom" + data.create_dataset("mask", data=mask) + + +def _write_test_2d_nexus_with_split_q(filename): + qx = np.array([[-0.5, 0.5], [-0.5, 0.5]], dtype=float) + qy = np.array([[-0.5, -0.5], [0.5, 0.5]], dtype=float) + intensity = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=float) + sigma = np.array([[0.1, 0.2], [0.3, 0.4]], dtype=float) + + with h5py.File(filename, "w") as h5f: + entry = h5f.create_group("entry") + data = entry.create_group("data") + signal = data.create_dataset("I", data=intensity) + signal.attrs["units"] = "1 / centimeter / steradian" + sigma_ds = data.create_dataset("I_unc", data=sigma) + sigma_ds.attrs["units"] = "1 / centimeter / steradian" + qx_ds = data.create_dataset("qx", data=qx) + qx_ds.attrs["units"] = "1 / angstrom" + qy_ds = data.create_dataset("qy", data=qy) + qy_ds.attrs["units"] = "1 / angstrom" + + +def test_load_1d_dataframe_from_csv_uses_default_columns(tmp_path): + filename = tmp_path / "input.dat" + filename.write_text("0.1 1.0 0.1\n0.2 2.0 0.2\n") + + loaded = load_1d_dataframe_from_file(filename, csvargs=DEFAULT_1D_CSVARGS) + + assert loaded.loader == "from_csv" + pdt.assert_frame_equal( + loaded.frame.reset_index(drop=True), + pandas.DataFrame( + { + "Q": np.array([0.1, 0.2], dtype=float), + "I": np.array([1.0, 2.0], dtype=float), + "ISigma": np.array([0.1, 0.2], dtype=float), + } + ), + ) + + +def test_load_1d_dataframe_from_nexus_detects_units_and_follows_default_path(tmp_path): + filename = tmp_path / "input.nxs" + + with h5py.File(filename, "w") as h5f: + h5f.attrs["default"] = "entry" + entry = h5f.create_group("entry") + entry.attrs["default"] = "data" + data = entry.create_group("data") + data.attrs["signal"] = "I" + data.attrs["I_uncertainty"] = "I_unc" + data.attrs["axes"] = np.array(["q"], dtype="S") + signal = data.create_dataset("I", data=np.array([1.0, 2.0], dtype=float)) + signal.attrs["units"] = "1 / centimeter / steradian" + sigma = data.create_dataset("I_unc", data=np.array([0.1, 0.2], dtype=float)) + sigma.attrs["units"] = "1 / centimeter / steradian" + q = data.create_dataset("q", data=np.array([0.1, 0.2], dtype=float)) + q.attrs["units"] = "1 / angstrom" + + loaded = load_1d_dataframe_from_file(filename) + + assert loaded.loader == "from_nexus" + assert loaded.source_q_units == "1 / angstrom" + assert loaded.source_intensity_units == "1 / centimeter / steradian" + np.testing.assert_allclose(loaded.frame["Q"], np.array([0.1, 0.2])) + np.testing.assert_allclose(loaded.frame["I"], np.array([1.0, 2.0])) + + +def test_load_1d_dataframe_from_nexus_rejects_2d_q_data(tmp_path): + filename = tmp_path / "input_2d.nxs" + + with h5py.File(filename, "w") as h5f: + h5f.attrs["default"] = "entry" + entry = h5f.create_group("entry") + entry.attrs["default"] = "data" + data = entry.create_group("data") + data.attrs["signal"] = "I" + data.attrs["I_uncertainty"] = "I_unc" + data.attrs["axes"] = np.array(["q"], dtype="S") + data.create_dataset("I", data=np.ones((2, 2), dtype=float)) + data.create_dataset("I_unc", data=np.ones((2, 2), dtype=float)) + data.create_dataset("q", data=np.ones((2, 2), dtype=float)) + + with pytest.raises(ValueError, match="cannot read 2D NeXus data directly"): + load_1d_dataframe_from_file(filename) + + +def test_load_2d_stage_from_nexus_detects_units_and_resolves_q_components(tmp_path): + filename = tmp_path / "input_2d.nxs" + _write_test_2d_nexus(filename) + + loaded = load_2d_stage_from_file(filename) + + assert loaded.loader == "from_nexus" + assert loaded.source_q_units == "1 / angstrom" + assert loaded.source_intensity_units == "1 / centimeter / steradian" + np.testing.assert_allclose(loaded.stage["Qx"], np.array([[-0.5, 0.5], [-0.5, 0.5]])) + np.testing.assert_allclose(loaded.stage["Qy"], np.array([[-0.5, -0.5], [0.5, 0.5]])) + np.testing.assert_allclose(loaded.stage["I"], np.array([[1.0, 2.0], [3.0, 4.0]])) + np.testing.assert_array_equal(loaded.stage["mask"], np.array([[False, True], [False, False]])) + + +def test_load_2d_stage_from_file_rejects_1d_q_data(tmp_path): + filename = tmp_path / "input_1d.nxs" + + with h5py.File(filename, "w") as h5f: + h5f.attrs["default"] = "entry" + entry = h5f.create_group("entry") + entry.attrs["default"] = "data" + data = entry.create_group("data") + data.attrs["signal"] = "I" + data.attrs["I_uncertainty"] = "I_unc" + data.attrs["axes"] = np.array(["q"], dtype="S") + data.create_dataset("I", data=np.array([1.0, 2.0], dtype=float)) + data.create_dataset("I_unc", data=np.array([0.1, 0.2], dtype=float)) + data.create_dataset("q", data=np.array([0.1, 0.2], dtype=float)) + + with pytest.raises(ValueError, match="require a multidimensional Q dataset"): + load_2d_stage_from_file(filename) + + +def test_load_2d_stage_from_file_accepts_split_q_path_dict(tmp_path): + filename = tmp_path / "input_2d_split_q.nxs" + _write_test_2d_nexus_with_split_q(filename) + + loaded = load_2d_stage_from_file( + filename, + path_dict={ + "I": "/entry/data/I", + "ISigma": "/entry/data/I_unc", + "Qx": "/entry/data/qx", + "Qy": "/entry/data/qy", + }, + ) + + assert loaded.source_q_units == "1 / angstrom" + assert loaded.source_intensity_units == "1 / centimeter / steradian" + np.testing.assert_allclose(loaded.stage["Qx"], np.array([[-0.5, 0.5], [-0.5, 0.5]])) + np.testing.assert_allclose(loaded.stage["Qy"], np.array([[-0.5, -0.5], [0.5, 0.5]])) diff --git a/tests/test_mc_core_fast.py b/tests/test_mc_core_fast.py new file mode 100644 index 0000000..b009c53 --- /dev/null +++ b/tests/test_mc_core_fast.py @@ -0,0 +1,500 @@ +import logging +from pathlib import Path +from types import SimpleNamespace + +import numpy as np +import pandas +import pytest +import sasmodels.core +import sasmodels.direct_model + +from mcsas3.data_adapters import bundle_from_1d_dataframe +from mcsas3.mc_analysis import McAnalysis +from mcsas3.mc_core import McCore +from mcsas3.mc_hat import McHat +from mcsas3.mc_model import McModel +from mcsas3.mc_model_histogrammer import McModelHistogrammer +from mcsas3.mc_opt import McOpt +from mcsas3.osb import optimizeScalingAndBackground + + +def test_mchat_fill_fit_parameter_limits_uses_q_range_for_auto_limits(): + hat = McHat( + modelName="mcsas_sphere", + fitParameterLimits={"radius": "auto"}, + staticParameters={"background": 0.0, "scale": 1.0, "sld": 1.0, "sld_solvent": 0.0}, + nRep=1, + nCores=1, + maxIter=1, + ) + + analysis_bundle = bundle_from_1d_dataframe( + pandas.DataFrame( + { + "Q": np.array([0.1, 1.0], dtype=float), + "I": np.array([1.0, 2.0], dtype=float), + "ISigma": np.array([0.1, 0.2], dtype=float), + } + ) + ) + + hat.fillFitParameterLimits(analysis_bundle) + + np.testing.assert_allclose(hat._modelArgs["fitParameterLimits"]["radius"], [np.pi / 1.0, np.pi / 0.1]) + + +def test_mchat_fill_fit_parameter_limits_rejects_zero_q_for_auto_limits(): + hat = McHat( + modelName="mcsas_sphere", + fitParameterLimits={"radius": "auto"}, + staticParameters={"background": 0.0, "scale": 1.0, "sld": 1.0, "sld_solvent": 0.0}, + nRep=1, + nCores=1, + maxIter=1, + ) + + with pytest.raises(ValueError, match="smallest Q value must be > 0"): + hat.fillFitParameterLimits( + bundle_from_1d_dataframe( + pandas.DataFrame( + { + "Q": np.array([0.0, 1.0], dtype=float), + "I": np.array([1.0, 2.0], dtype=float), + "ISigma": np.array([0.1, 0.2], dtype=float), + } + ) + ) + ) + + +def test_mchat_fill_fit_parameter_limits_rejects_unknown_string_limit_mode(): + hat = McHat( + modelName="mcsas_sphere", + fitParameterLimits={"radius": "invalid"}, + staticParameters={"background": 0.0, "scale": 1.0, "sld": 1.0, "sld_solvent": 0.0}, + nRep=1, + nCores=1, + maxIter=1, + ) + + with pytest.raises(ValueError, match='explicit \\[min, max\\] pairs or the string "auto"'): + hat.fillFitParameterLimits( + bundle_from_1d_dataframe( + pandas.DataFrame( + { + "Q": np.array([0.1, 1.0], dtype=float), + "I": np.array([1.0, 2.0], dtype=float), + "ISigma": np.array([0.1, 0.2], dtype=float), + } + ) + ) + ) + + +def test_mchat_init_rejects_unknown_option_key(): + with pytest.raises(ValueError, match="not a valid option"): + McHat(modelName="mcsas_sphere", invalidOption=True) + + +def test_mcanalysis_requires_existing_project_file(tmp_path): + with pytest.raises(ValueError, match="project filename"): + McAnalysis( + tmp_path / "missing_result.h5", + bundle_from_1d_dataframe( + pandas.DataFrame( + { + "Q": np.array([0.1, 1.0], dtype=float), + "I": np.array([1.0, 2.0], dtype=float), + "ISigma": np.array([0.1, 0.2], dtype=float), + } + ) + ), + pandas.DataFrame(), + ) + + +def test_mcmodelhistogrammer_requires_core_instance_type(): + with pytest.raises(TypeError, match="core instance"): + McModelHistogrammer(object(), pandas.DataFrame()) + + +def test_mcmodelhistogrammer_does_not_mutate_input_hist_ranges(): + analysis_bundle = bundle_from_1d_dataframe( + pandas.DataFrame( + { + "Q": np.array([0.1, 0.2, 0.3], dtype=float), + "I": np.array([1.0, 1.5, 2.0], dtype=float), + "ISigma": np.array([0.1, 0.1, 0.2], dtype=float), + } + ) + ) + model = McModel( + modelName="mcsas_sphere", + nContrib=1, + fitParameterLimits={"radius": (5.0, 10.0)}, + staticParameters={"background": 0.0, "scale": 1.0, "sld": 1.0, "sld_solvent": 0.0}, + seed=123, + ) + opt = McOpt(convCrit=0.0, maxIter=1, repetition=0) + core = McCore(analysis_input=analysis_bundle, model=model, opt=opt) + hist_ranges = pandas.DataFrame( + [ + dict( + parameter="radius", + nBin=4, + binScale="linear", + presetRangeMin=1.0, + presetRangeMax=20.0, + binWeighting="vol", + autoRange=True, + ) + ] + ) + original_hist_ranges = hist_ranges.copy(deep=True) + + with pytest.warns(RuntimeWarning): + histogrammer = McModelHistogrammer(core, hist_ranges) + + assert "rangeMin" not in hist_ranges.columns + assert "rangeMax" not in hist_ranges.columns + pandas.testing.assert_frame_equal(hist_ranges, original_hist_ranges) + assert histogrammer._histRanges.loc[0, "rangeMin"] == 5.0 + assert histogrammer._histRanges.loc[0, "rangeMax"] == 10.0 + + +def test_mcmodel_rejects_unknown_option_key(): + with pytest.raises(ValueError, match="not a valid settable option"): + McModel(invalidOption=True) + + +def test_mcsim_pseudo_model_requires_simulation_arrays(): + from mcsas3.mc_model import McSimPseudoModel + + with pytest.raises(ValueError, match="Missing: simDataQ1, simDataI, simDataISigma"): + McSimPseudoModel(simDataQ0=np.array([0.1, 0.2], dtype=float)) + + +def test_mcopt_instances_do_not_share_accepted_history(): + first = McOpt() + second = McOpt() + + first.acceptedSteps.append(5) + first.acceptedGofs.append(0.25) + + assert second.acceptedSteps == [] + assert second.acceptedGofs == [] + + +def test_mcsas_sphere_model_defaults_remain_available_via_model_info(): + model = McModel( + modelName="mcsas_sphere", + nContrib=1, + fitParameterLimits={"radius": (5.0, 10.0)}, + staticParameters={"background": 0.0, "scale": 1.0, "sld": 1.0, "sld_solvent": 0.0}, + seed=123, + ) + + defaults = model.model_parameters() + + assert defaults["radius"] == 1 + assert defaults["scale"] == 1.0 + assert defaults["background"] == 0.0 + + +def test_mcmodel_available_models_returns_grouped_mapping(): + model = McModel( + modelName="mcsas_sphere", + nContrib=1, + fitParameterLimits={"radius": (5.0, 10.0)}, + staticParameters={"background": 0.0, "scale": 1.0, "sld": 1.0, "sld_solvent": 0.0}, + seed=123, + ) + + available = model.available_models() + + assert "one_dimensional" in available + assert "one_and_two_dimensional" in available + assert "sphere" in available["one_dimensional"] + available["one_and_two_dimensional"] + + +def test_mcmodel_model_parameters_requires_loaded_model(): + model = McModel.__new__(McModel) + model.func = None + + with pytest.raises(RuntimeError, match="loaded before model parameters can be queried"): + model.model_parameters() + + +def test_optimize_scaling_and_background_rejects_nan_measurement_data(): + with pytest.raises(ValueError, match="cannot contain NaN"): + optimizeScalingAndBackground( + measDataI=np.array([1.0, np.nan], dtype=float), + measDataISigma=np.array([0.1, 0.1], dtype=float), + ) + + +def test_optimize_scaling_and_background_preserves_custom_bounds(): + custom_bounds = [[0.0, 10.0], [-2.0, 2.0]] + + optimizer = optimizeScalingAndBackground( + measDataI=np.array([1.0, 2.0, 3.0], dtype=float), + measDataISigma=np.array([0.1, 0.1, 0.1], dtype=float), + xBounds=custom_bounds, + ) + + assert optimizer.xBounds == custom_bounds + + +def test_optimize_scaling_and_background_accepts_canonical_bundle_input(): + analysis_bundle = bundle_from_1d_dataframe( + pandas.DataFrame( + { + "Q": np.array([0.1, 0.2, 0.3], dtype=float), + "I": np.array([1.0, 1.5, 2.0], dtype=float), + "ISigma": np.array([0.1, 0.1, 0.2], dtype=float), + } + ) + ) + + optimizer = optimizeScalingAndBackground(analysis_bundle) + + np.testing.assert_allclose(optimizer.measDataI, np.array([1.0, 1.5, 2.0])) + np.testing.assert_allclose(optimizer.measDataISigma, np.array([0.1, 0.1, 0.2])) + + +def test_mccore_optimize_returns_false_when_stop_requested(): + core = McCore.__new__(McCore) + core._stopRequested = lambda: core._opt.step >= 3 + core._opt = SimpleNamespace( + repetition=2, + gof=10.0, + accepted=0, + step=0, + maxAccept=100, + maxIter=100, + convCrit=0.0, + ) + core.iterate = lambda: setattr(core._opt, "step", core._opt.step + 1) + + completed = core.optimize() + + assert completed is False + assert core._opt.step == 3 + + +def test_mccore_optimize_logs_progress_when_stopped(caplog): + core = McCore.__new__(McCore) + core._stopRequested = lambda: core._opt.step >= 1 + core._opt = SimpleNamespace( + repetition=5, + gof=10.0, + accepted=0, + step=0, + maxAccept=100, + maxIter=100, + convCrit=0.0, + ) + core.iterate = lambda: setattr(core._opt, "step", core._opt.step + 1) + + with caplog.at_level(logging.INFO, logger="mcsas3.mc_core"): + completed = core.optimize() + + assert completed is False + assert "Optimization of repetition 5 started." in caplog.text + assert "Optimization of repetition 5 interrupted." in caplog.text + + +def test_mchat_request_stop_prevents_later_single_core_repetitions(monkeypatch, tmp_path): + hat = McHat( + modelName="mcsas_sphere", + fitParameterLimits={"radius": "auto"}, + staticParameters={"background": 0.0, "scale": 1.0, "sld": 1.0, "sld_solvent": 0.0}, + nRep=3, + nCores=1, + maxIter=1, + ) + started_repetitions = [] + + monkeypatch.setattr(hat, "fillFitParameterLimits", lambda analysis_input: None) + + def fake_run_once(analysis_input, filename, repetition=0, bufferStdIO=False, resultIndex=1): + started_repetitions.append(repetition) + hat.request_stop() + return None + + monkeypatch.setattr(hat, "runOnce", fake_run_once) + + hat.run( + bundle_from_1d_dataframe( + pandas.DataFrame( + { + "Q": np.array([0.1, 1.0], dtype=float), + "I": np.array([1.0, 2.0], dtype=float), + "ISigma": np.array([0.1, 0.2], dtype=float), + } + ) + ), + tmp_path / "unused.h5", + ) + + assert started_repetitions == [0] + assert hat.lastRunStopped is True + assert hat.isRunning is False + + +def test_mchat_run_once_buffers_logging_output(monkeypatch, tmp_path): + hat = McHat( + modelName="mcsas_sphere", + fitParameterLimits={"radius": "auto"}, + staticParameters={"background": 0.0, "scale": 1.0, "sld": 1.0, "sld_solvent": 0.0}, + nRep=1, + nCores=1, + maxIter=1, + ) + hat._opt = SimpleNamespace(repetition=0, gof=1.5, accepted=2) + hat._model = SimpleNamespace( + resetParameterSet=lambda: None, + kernel=SimpleNamespace(release=lambda: None), + ) + + class FakeMcCore: + def __init__(self, analysis_input, model, opt, resultIndex, stop_requested): + self._opt = opt + + def optimize(self): + logging.getLogger("mcsas3.mc_core").info("buffered worker progress") + return False + + monkeypatch.setattr("mcsas3.mc_hat.McCore", FakeMcCore) + + repetition, output, completed = hat.runOnce( + bundle_from_1d_dataframe( + pandas.DataFrame( + { + "Q": np.array([0.1, 1.0], dtype=float), + "I": np.array([1.0, 2.0], dtype=float), + "ISigma": np.array([0.1, 0.2], dtype=float), + } + ) + ), + tmp_path / "unused.h5", + repetition=0, + bufferStdIO=True, + ) + + assert repetition == 0 + assert completed is False + assert "buffered worker progress" in output + assert "Optimization of repetition 0 stopped before completion." in output + + +def test_mcanalysis_average_histogram_rejects_mismatched_bin_edges(): + analysis = McAnalysis.__new__(McAnalysis) + analysis._repetitionList = [0, 1] + analysis._concatHistograms = {0: {0: np.array([1.0]), 1: np.array([2.0])}} + analysis._concatBinEdges = {0: {0: np.array([0.0, 1.0]), 1: np.array([0.0, 2.0])}} + + with pytest.raises(ValueError, match="identical histogram bin edges"): + analysis.averageHistogram(0) + + +def test_mccore_accept_updates_parameter_set_and_optimizer_state(): + core = McCore.__new__(McCore) + core._model = SimpleNamespace( + nContrib=2, + parameterSet=pandas.DataFrame(data={"radius": [1.0, 2.0]}), + pickParameters={"radius": 9.0}, + volumes=np.array([10.0, 20.0], dtype=float), + ) + core._opt = SimpleNamespace( + step=3, + modelI=np.array([1.0, 2.0], dtype=float), + testModelI=np.array([3.0, 4.0], dtype=float), + testModelV=99.0, + x0=np.array([1.0, 0.0], dtype=float), + testX0=np.array([2.0, 0.5], dtype=float), + acceptedSteps=[0], + acceptedGofs=[1.5], + accepted=1, + gof=0.5, + ) + + core.accept() + + assert core._model.parameterSet.loc[1, "radius"] == 9.0 + np.testing.assert_allclose(core._opt.modelI, np.array([3.0, 4.0])) + assert core._model.volumes[1] == 99.0 + np.testing.assert_allclose(core._opt.x0, np.array([2.0, 0.5])) + assert core._opt.accepted == 2 + assert core._opt.acceptedSteps == [0, 3] + assert core._opt.acceptedGofs == [1.5, 0.5] + + +def test_sasmodels_sphere_unit_bridge_recovers_expected_volume_fraction(monkeypatch): + sasmodels_cache = Path(".pytest_sasmodels_cache", "compiled_models") + sasmodels_cache.mkdir(parents=True, exist_ok=True) + monkeypatch.setenv("SAS_OPENCL", "none") + monkeypatch.setenv("SAS_DLL_PATH", str(sasmodels_cache.resolve())) + + q_nm = np.geomspace(0.03, 0.3, 48) + radius_nm = 35.0 + sld = 6.0 + sld_solvent = 1.0 + expected_volume_fraction = 0.037 + + sas_model = sasmodels.core.load_model("sphere", dtype="default") + sas_kernel = sas_model.make_kernel([q_nm / 10.0]) + _f, fsq, _r_eff, v_shell, _v_ratio = sasmodels.direct_model.call_Fq( + sas_kernel, + {"radius": radius_nm * 10.0, "sld": sld, "sld_solvent": sld_solvent}, + ) + reference_intensity = expected_volume_fraction * 100.0 * (fsq / v_shell) + + analysis_bundle = bundle_from_1d_dataframe( + pandas.DataFrame( + { + "Q": q_nm, + "I": reference_intensity, + "ISigma": np.maximum(reference_intensity * 0.01, 1e-12), + } + ) + ) + + model = McModel( + modelName="sphere", + modelDType="default", + nContrib=1, + fitParameterLimits={"radius": (radius_nm, radius_nm)}, + staticParameters={"background": 0.0, "scale": 1.0, "sld": sld, "sld_solvent": sld_solvent}, + seed=123, + ) + model.parameterSet.loc[0, "radius"] = radius_nm + model.kernel = model.func.make_kernel([q_nm]) + bridged_intensity, _volume = model.calcModelIV({"radius": radius_nm}) + + expected_optimizer_scale = expected_volume_fraction / McModelHistogrammer._correctionFactor + np.testing.assert_allclose(reference_intensity, expected_optimizer_scale * bridged_intensity, rtol=1e-10) + + opt = McOpt(convCrit=0.0, maxIter=1, repetition=0) + core = McCore(analysis_input=analysis_bundle, model=model, opt=opt) + + np.testing.assert_allclose(core._opt.x0[0], expected_optimizer_scale, rtol=5e-5) + + hist_ranges = pandas.DataFrame( + [ + dict( + parameter="radius", + nBin=1, + binScale="linear", + presetRangeMin=radius_nm * 0.9, + presetRangeMax=radius_nm * 1.1, + binWeighting="vol", + autoRange=False, + ) + ] + ) + with pytest.warns(RuntimeWarning): + histogrammer = McModelHistogrammer(core, hist_ranges) + + np.testing.assert_allclose(histogrammer._histDict[0][0], expected_volume_fraction, rtol=5e-5) + np.testing.assert_allclose(histogrammer._modes.loc[0, "totalValue"], expected_volume_fraction, rtol=5e-5) diff --git a/tests/test_mc_hdf_fast.py b/tests/test_mc_hdf_fast.py new file mode 100644 index 0000000..fb58e41 --- /dev/null +++ b/tests/test_mc_hdf_fast.py @@ -0,0 +1,109 @@ +from pathlib import Path, PurePosixPath + +import numpy as np +import pandas +import pandas.testing as pdt +import pytest + +from mcsas3.data_adapters import DEFAULT_INTENSITY_UNITS, STAGE_BINNED, STAGE_RAW, bundle_from_1d_dataframe +from mcsas3.data_model import ProcessingData +from mcsas3.mc_hdf import ResultIndex, loadKV, loadProcessingData, storeKV, storeKVPairs, storeProcessingData + + +def test_result_index_builds_expected_entry_path(): + assert ResultIndex(3).nxsEntryPoint == PurePosixPath("/analyses/MCResult3") + + +def test_loadkv_returns_default_for_missing_path(tmp_path): + filename = tmp_path / "missing.h5" + + assert loadKV(filename, PurePosixPath("/does/not/exist"), default="fallback") == "fallback" + + +def test_storekv_round_trips_path_and_nested_dict_payloads(tmp_path): + filename = tmp_path / "payloads.h5" + source_path = Path("nested/datafile.dat") + payload = { + "labels": np.array(["alpha", "beta"]), + "meta": { + "count": 2, + "scale": 1.5, + }, + } + + storeKV(filename, PurePosixPath("/config/source"), source_path) + storeKVPairs(filename, PurePosixPath("/payload"), payload.items()) + + assert loadKV(filename, PurePosixPath("/config/source"), datatype=Path) == source_path + + loaded_payload = loadKV(filename, PurePosixPath("/payload"), datatype="dict") + assert loaded_payload["labels"].tolist() == ["alpha", "beta"] + assert loaded_payload["meta"] == {"count": 2, "scale": 1.5} + + +@pytest.mark.parametrize( + ("filename", "path", "message"), + [ + (None, PurePosixPath("/config/value"), "filename"), + (Path("payloads.h5"), None, "HDF5 path"), + ], +) +def test_storekv_rejects_missing_filename_or_path(filename, path, message): + with pytest.raises(ValueError, match=message): + storeKV(filename, path, "value") + + +def test_loadkv_dict_to_pandas_reconstructs_split_dataframe(tmp_path): + filename = tmp_path / "frame.h5" + frame = pandas.DataFrame( + data={"radius": [1.0, 2.5], "volume_fraction": [0.1, 0.2]}, + index=[10, 11], + ) + + storeKVPairs(filename, PurePosixPath("/frame"), frame.to_dict(orient="split").items()) + + restored = loadKV(filename, PurePosixPath("/frame"), datatype="dictToPandas") + + pdt.assert_frame_equal(restored, frame) + + +def test_processing_data_round_trips_with_units_uncertainties_and_stage_selection(tmp_path): + filename = tmp_path / "processing_data.h5" + raw_bundle = bundle_from_1d_dataframe( + pandas.DataFrame( + { + "Q": np.array([1.0, 2.0], dtype=float), + "I": np.array([10.0, 20.0], dtype=float), + "ISigma": np.array([1.0, 2.0], dtype=float), + } + ) + ) + binned_bundle = bundle_from_1d_dataframe( + pandas.DataFrame( + { + "Q": np.array([1.5], dtype=float), + "I": np.array([15.0], dtype=float), + "ISigma": np.array([1.5], dtype=float), + } + ) + ) + raw_bundle.description = "input data" + raw_bundle.default_plot = "signal" + processing = ProcessingData() + processing[STAGE_RAW] = raw_bundle + processing[STAGE_BINNED] = binned_bundle + setattr(processing, "analysis_stage", STAGE_BINNED) + + storeProcessingData(filename, PurePosixPath("/mcdata/processingData"), processing) + restored = loadProcessingData(filename, PurePosixPath("/mcdata/processingData")) + + assert getattr(restored, "analysis_stage") == STAGE_BINNED + assert restored[STAGE_RAW].default_plot == "signal" + assert restored[STAGE_RAW].description == "input data" + np.testing.assert_allclose(restored[STAGE_RAW]["Q"].signal, np.array([1.0, 2.0])) + np.testing.assert_allclose(restored[STAGE_RAW]["signal"].signal, np.array([10.0, 20.0])) + np.testing.assert_allclose( + restored[STAGE_RAW]["signal"].uncertainties["propagate_to_all"], + np.array([1.0, 2.0]), + ) + assert restored[STAGE_RAW]["signal"].units == DEFAULT_INTENSITY_UNITS diff --git a/tests/test_optimizer_input_fast.py b/tests/test_optimizer_input_fast.py new file mode 100644 index 0000000..a7cbb34 --- /dev/null +++ b/tests/test_optimizer_input_fast.py @@ -0,0 +1,120 @@ +import numpy as np +import pandas +import pytest + +from mcsas3.data_adapters import fit_arrays_from_bundle +from mcsas3.mc_hat import McHat +from mcsas3.optimizer_input import OptimizerInput, as_optimizer_input, optimizer_input_from_bundle +from mcsas3.workflows import prepare_1d_processing_data, prepare_2d_processing_data + + +def _make_test_processing_2d(**kwargs): + coords = np.array([-1.5, -0.5, 0.5, 1.5], dtype=float) + qx, qy = np.meshgrid(coords, coords) + intensity = np.arange(16, dtype=float).reshape(4, 4) + sigma = np.ones((4, 4), dtype=float) + mask = np.zeros((4, 4), dtype=bool) + mask[1, 1] = True + sigma[1, 2] = 0.0 + + return prepare_2d_processing_data( + { + "Qx": qx, + "Qy": qy, + "I": intensity, + "ISigma": sigma, + "mask": mask, + }, + data_range=[0.0, 1.0], + ortho_q0_range=[0.0, 1.0], + ortho_q1_range=[0.0, 1.0], + **kwargs, + ) + + +def test_optimizer_input_from_1d_bundle_matches_fit_arrays(): + frame = pandas.DataFrame( + { + "Q": np.array([0.5, 1.0, 2.0, 4.0, 5.0], dtype=float), + "I": np.array([5.0, 10.0, 20.0, 40.0, 50.0], dtype=float), + "ISigma": np.array([0.5, 1.0, 2.0, 4.0, 5.0], dtype=float), + } + ) + processing = prepare_1d_processing_data( + frame, + data_range=[1.0, 5.0], + omit_q_ranges=[[1.5, 3.0]], + nbins=0, + ) + bundle = processing["sample_binned"] + + optimizer_input = optimizer_input_from_bundle(bundle) + q_arrays, intensity, sigma = fit_arrays_from_bundle(bundle) + + np.testing.assert_allclose(optimizer_input.q[0], q_arrays[0]) + np.testing.assert_allclose(optimizer_input.i, intensity) + np.testing.assert_allclose(optimizer_input.isigma, sigma) + + +def test_optimizer_input_from_2d_bundle_matches_fit_arrays(): + processing = _make_test_processing_2d(nbins=0) + bundle = processing["sample_binned"] + + optimizer_input = optimizer_input_from_bundle(bundle) + q_arrays, intensity, sigma = fit_arrays_from_bundle(bundle) + + np.testing.assert_allclose(optimizer_input.q[0], q_arrays[0]) + np.testing.assert_allclose(optimizer_input.q[1], q_arrays[1]) + np.testing.assert_allclose(optimizer_input.i, intensity) + np.testing.assert_allclose(optimizer_input.isigma, sigma) + + +def test_optimizer_input_from_bundle_matches_direct_bundle_coercion_for_1d_bundle(): + frame = pandas.DataFrame( + { + "Q": np.array([0.5, 1.0, 2.0], dtype=float), + "I": np.array([5.0, 10.0, 20.0], dtype=float), + "ISigma": np.array([0.5, 1.0, 2.0], dtype=float), + } + ) + processing = prepare_1d_processing_data(frame, nbins=0) + bundle = processing["sample_binned"] + + from_bundle = optimizer_input_from_bundle(bundle) + from_bundle_coercion = as_optimizer_input(bundle) + + assert isinstance(from_bundle, OptimizerInput) + np.testing.assert_allclose(from_bundle.q[0], from_bundle_coercion.q[0]) + np.testing.assert_allclose(from_bundle.i, from_bundle_coercion.i) + np.testing.assert_allclose(from_bundle.isigma, from_bundle_coercion.isigma) + + +def test_mchat_fill_fit_parameter_limits_accepts_optimizer_input(): + hat = McHat( + modelName="mcsas_sphere", + fitParameterLimits={"radius": "auto"}, + staticParameters={"background": 0.0, "scale": 1.0, "sld": 1.0, "sld_solvent": 0.0}, + nRep=1, + nCores=1, + maxIter=1, + ) + optimizer_input = OptimizerInput( + q=(np.array([0.1, 1.0], dtype=float),), + i=np.array([1.0, 2.0], dtype=float), + isigma=np.array([0.1, 0.2], dtype=float), + ) + + hat.fillFitParameterLimits(optimizer_input) + + np.testing.assert_allclose(hat._modelArgs["fitParameterLimits"]["radius"], [np.pi / 1.0, np.pi / 0.1]) + + +def test_as_optimizer_input_rejects_flat_analysis_data_dict(): + with pytest.raises(TypeError, match="Optimizer input must be an OptimizerInput or a canonical DataBundle."): + as_optimizer_input( + { + "Q": [np.array([1.0], dtype=float)], + "I": np.array([1.0], dtype=float), + "ISigma": np.array([0.1], dtype=float), + } + ) diff --git a/tests/test_optimizer_integraltest.py b/tests/test_optimizer_integraltest.py index 880cb59..e3702e4 100644 --- a/tests/test_optimizer_integraltest.py +++ b/tests/test_optimizer_integraltest.py @@ -1,21 +1,148 @@ +# ruff: noqa: E402 + import os import shutil # for file copy - -# these need to be loaded at the beginning to avoid errors related to relative imports -# (ImportWarning in h5py), might be related to the change of import style for Python 3.5+. -# Tested on Python 3.11 at 20241127 -import sys +import tempfile import unittest import warnings from pathlib import Path import numpy as np import pandas +import pytest + +SASMODELS_CACHE = Path(".pytest_sasmodels_cache", "compiled_models") +SASMODELS_CACHE.mkdir(parents=True, exist_ok=True) +os.environ.setdefault("MPLBACKEND", "Agg") +os.environ.setdefault("SAS_OPENCL", "none") +os.environ.setdefault("SAS_DLL_PATH", str(SASMODELS_CACHE.resolve())) -from mcsas3 import mc_data_1d, mc_data_2d, mc_hat, mc_plot +from mcsas3 import mc_hat, mc_plot, workflows +from mcsas3.data_adapters import selected_bundle_from_processing from mcsas3.mc_analysis import McAnalysis +from mcsas3.optimizer_input import optimizer_input_from_bundle +# Keep imports at module scope; moving them into helpers has triggered relative-import issues before. warnings.filterwarnings("error") +pytestmark = pytest.mark.integration + +FAST_N_CONTRIB = 96 +FAST_MAX_ITER = 1500 +FAST_N_REP = 1 +FAST_SEED = 12345 + + +def build_hat( + *, + model_name: str, + fit_parameter_limits: dict, + static_parameters: dict, + conv_crit: float, + result_index: int = 1, + n_cores: int = 1, + n_contrib: int = FAST_N_CONTRIB, + max_iter: int = FAST_MAX_ITER, + n_rep: int = FAST_N_REP, + seed: int | None = FAST_SEED, + **kwargs: dict, +) -> mc_hat.McHat: + if n_cores > 1: + os.environ["SAS_OPENCL"] = "none" + + return mc_hat.McHat( + modelName=model_name, + nContrib=n_contrib, + modelDType="default", + fitParameterLimits=fit_parameter_limits, + staticParameters=static_parameters, + maxIter=max_iter, + convCrit=conv_crit, + nRep=n_rep, + nCores=n_cores, + seed=seed, + resultIndex=result_index, + **kwargs, + ) + + +def build_simulation_inputs(): + measurement_data = workflows.prepare_1d_processing_data_from_file( + filename=Path("testdata", "nPSize4.dat"), + nbins=0, + csvargs={ + "sep": ";", + "header": None, + "names": ["Q", "I", "ISigma"], + "usecols": [0, 3, 4], + }, + dataRange=[0.04, 1], + ) + simulation_data = workflows.prepare_1d_processing_data_from_file( + filename=Path("testdata", "fancyCubePD0p01.nxs"), + pathDict={ + "Q": "/sasentry1/sasdata1/Q", + "I": "/sasentry1/sasdata1/I", + "ISigma": "/sasentry1/sasdata1/Idev", + }, + dataRange=[0, 38], + ) + return measurement_data, simulation_data + + +def factor_hist_ranges() -> pandas.DataFrame: + return pandas.DataFrame( + [ + dict( + parameter="factor", + nBin=50, + binScale="log", + presetRangeMin=0.1, + presetRangeMax=3, + binWeighting="vol", + autoRange=True, + ), + dict( + parameter="factor", + nBin=50, + binScale="linear", + presetRangeMin=0.1, + presetRangeMax=3, + binWeighting="vol", + autoRange=False, + ), + ] + ) + + +def run_simulation_fit(res_path: Path, *, n_cores: int, rebuild: bool = True) -> dict: + measurement_processing, simulation_processing = build_simulation_inputs() + simulation_input = optimizer_input_from_bundle(selected_bundle_from_processing(simulation_processing)) + + if rebuild and res_path.is_file(): + res_path.unlink() + + if rebuild or not res_path.is_file(): + workflows.optimize_processing_data( + measurement_processing, + res_path, + hat=build_hat( + model_name="sim", + fit_parameter_limits={"factor": (20, 40)}, + static_parameters={ + "extrapY0": 2.21e-09, + "extrapScaling": 9.61e01, + "simDataQ0": simulation_input.q[0], + "simDataQ1": None, + "simDataI": simulation_input.i, + "simDataISigma": simulation_input.isigma, + }, + conv_crit=14, + n_cores=n_cores, + n_rep=2 if n_cores > 1 else 1, + ), + ) + + return measurement_processing class testOptimizer(unittest.TestCase): @@ -24,36 +151,33 @@ def test_optimizer_2D_cylinder(self): if resPath.is_file(): resPath.unlink() - # md = McData2D.McData2D() - # md.from_nexus(filename=r"testdata/009766_forSasView.h5") - mds = mc_data_2d.McData2D( + analysis_input = workflows.prepare_2d_processing_data_from_file( filename=Path("testdata", "009766_forSasView.h5"), + dataRange=[0, np.inf], + orthoQ0Range=[0, np.inf], + orthoQ1Range=[0, np.inf], + nbins=0, ) - mh = mc_hat.McHat( - modelName="cylinder", - nContrib=600, - modelDType="default", - fitParameterLimits={ + mh = build_hat( + model_name="cylinder", + n_contrib=128, + fit_parameter_limits={ "radius": (5, 500), "length": (600, 1200), "phi": (90 - 90, 90 + 90), }, - staticParameters={ + static_parameters={ "background": 0, "scale": 1, "sld": 6.3, # e-6, "sld_solvent": 1, # e-6, # D2O "theta": 90, }, - maxIter=1e5, - convCrit=1e5, - nRep=4, - nCores=0, - seed=None, + max_iter=500, + conv_crit=1e5, ) - md = mds.measData.copy() - mh.run(md, resPath) + workflows.optimize_processing_data(analysis_input, resPath, hat=mh) histRanges = pandas.DataFrame( [ @@ -86,7 +210,7 @@ def test_optimizer_2D_cylinder(self): ), ] ) - _ = McAnalysis(resPath, md, histRanges, store=True) + _ = McAnalysis(resPath, analysis_input, histRanges, store=True) def test_optimizer_1D_mcsas_sphere_and_rehistogrammer(self): # uses an internal sphere function for the case the sasmodels don't want to work. @@ -95,35 +219,27 @@ def test_optimizer_1D_mcsas_sphere_and_rehistogrammer(self): if resPath.is_file(): resPath.unlink() - mds = mc_data_1d.McData1D( + analysis_input = workflows.prepare_1d_processing_data_from_file( filename=Path("testdata", "quickstartdemo1.csv"), nbins=100, csvargs={"sep": ";", "header": None, "names": ["Q", "I", "ISigma"]}, - resultIndex=2, + result_index=2, ) - mds.store(resPath) # run the Monte Carlo method - mh = mc_hat.McHat( - modelName="mcsas_sphere", - nContrib=300, - modelDType="default", - fitParameterLimits={"radius": (3.14, 314)}, - staticParameters={ + mh = build_hat( + model_name="mcsas_sphere", + fit_parameter_limits={"radius": (3.14, 314)}, + static_parameters={ "background": 0, "scale": 1, "sld": 3.35e-5, "sld_solvent": 0, }, - maxIter=1e5, - convCrit=1, - nRep=4, - nCores=1, - seed=None, - resultIndex=2, + result_index=2, + conv_crit=1, ) - md = mds.measData.copy() - mh.run(md, resPath, resultIndex=2) + workflows.optimize_processing_data(analysis_input, resPath, result_index=2, hat=mh) histRanges = pandas.DataFrame( [ @@ -147,7 +263,7 @@ def test_optimizer_1D_mcsas_sphere_and_rehistogrammer(self): ), ] ) - _ = McAnalysis(resPath, md, histRanges, store=True, resultIndex=2) + _ = McAnalysis(resPath, analysis_input, histRanges, store=True, resultIndex=2) # -- -- -- # def test_reHistogrammer(self): @@ -156,11 +272,11 @@ def test_optimizer_1D_mcsas_sphere_and_rehistogrammer(self): # resPath = Path("test_resultssphere.h5") # clear prior results: - del mds, mh, histRanges + del mh, histRanges # load the data - mds = mc_data_1d.McData1D(loadFromFile=resPath, resultIndex=2) + analysis_input = workflows.load_result_processing_data(resPath, result_index=2) histRanges = pandas.DataFrame( [ @@ -185,8 +301,7 @@ def test_optimizer_1D_mcsas_sphere_and_rehistogrammer(self): ] ) # run the Monte Carlo method - md = mds.measData.copy() - mcres = McAnalysis(resPath, md, histRanges, store=True, resultIndex=2) + mcres = McAnalysis(resPath, analysis_input, histRanges, store=True, resultIndex=2) # plotting: # plot the histogram result @@ -203,7 +318,7 @@ def test_optimizer_1D_sphere_poor_inital_guess(self): if resPath.is_file(): resPath.unlink() - mds = mc_data_1d.McData1D( + analysis_input = workflows.prepare_1d_processing_data_from_file( filename=Path("testdata", "S2870 BSA THF 1 1 d.pdh"), nbins=100, csvargs={ @@ -219,21 +334,14 @@ def test_optimizer_1D_sphere_poor_inital_guess(self): ) # run the Monte Carlo method - mh = mc_hat.McHat( - modelName="sphere", - nContrib=300, - modelDType="default", - fitParameterLimits={"radius": (3.14, 314)}, - staticParameters={"background": 0, "scale": 0.1e6, "sld": 33, "sld_solvent": 0}, - maxIter=1e5, + mh = build_hat( + model_name="sphere", + fit_parameter_limits={"radius": (3.14, 314)}, + static_parameters={"background": 0, "scale": 0.1e6, "sld": 33, "sld_solvent": 0}, maxAccept=1e3, - convCrit=1, - nRep=4, - nCores=0, - seed=None, + conv_crit=1, ) - md = mds.measData.copy() - mh.run(md, resPath) + workflows.optimize_processing_data(analysis_input, resPath, hat=mh) histRanges = pandas.DataFrame( [ @@ -257,59 +365,7 @@ def test_optimizer_1D_sphere_poor_inital_guess(self): ), ] ) - _ = McAnalysis(resPath, md, histRanges, store=True) - - def test_optimizer_1D_sphere(self): - # remove any prior results file: - resPath = Path("test_resultssphere_1D.h5") - if resPath.is_file(): - resPath.unlink() - - mds = mc_data_1d.McData1D( - filename=Path("testdata", "quickstartdemo1.csv"), - nbins=100, - csvargs={"sep": ";", "header": None, "names": ["Q", "I", "ISigma"]}, - ) - - # run the Monte Carlo method - mh = mc_hat.McHat( - modelName="sphere", - nContrib=300, - modelDType="default", - fitParameterLimits={"radius": (3.14, 314)}, - staticParameters={"background": 0, "scale": 0.1e6}, - maxIter=1e5, - convCrit=1, - nRep=4, - nCores=0, - seed=None, - ) - md = mds.measData.copy() - mh.run(md, resPath) - - histRanges = pandas.DataFrame( - [ - dict( - parameter="radius", - nBin=50, - binScale="log", - presetRangeMin=1, - presetRangeMax=314, - binWeighting="vol", - autoRange=True, - ), - dict( - parameter="radius", - nBin=50, - binScale="linear", - presetRangeMin=10, - presetRangeMax=100, - binWeighting="vol", - autoRange=False, - ), - ] - ) - _ = McAnalysis(resPath, md, histRanges, store=True) + _ = McAnalysis(resPath, analysis_input, histRanges, store=True) def test_optimizer_1D_sphere_with_hardspherestructure(self): # remove any prior results file: @@ -317,33 +373,26 @@ def test_optimizer_1D_sphere_with_hardspherestructure(self): if resPath.is_file(): resPath.unlink() - mds = mc_data_1d.McData1D( + analysis_input = workflows.prepare_1d_processing_data_from_file( filename=Path("testdata", "quickstartdemo1.csv"), nbins=100, csvargs={"sep": ";", "header": None, "names": ["Q", "I", "ISigma"]}, ) # run the Monte Carlo method - mh = mc_hat.McHat( - modelName="sphere@hardsphere", - nContrib=300, - modelDType="default", - fitParameterLimits={"radius": (3.14, 314)}, - staticParameters={ + mh = build_hat( + model_name="sphere@hardsphere", + fit_parameter_limits={"radius": (3.14, 314)}, + static_parameters={ "background": 0, "scale": 1, "radius_effective_mode": 1, # effective radius follows radius "structure_factor_mode": 1, # with beta approximation "volfraction": 0.01, }, - maxIter=1e5, - convCrit=1, - nRep=4, - nCores=0, - seed=None, + conv_crit=1, ) - md = mds.measData.copy() - mh.run(md, resPath) + workflows.optimize_processing_data(analysis_input, resPath, hat=mh) histRanges = pandas.DataFrame( [ @@ -367,295 +416,19 @@ def test_optimizer_1D_sphere_with_hardspherestructure(self): ), ] ) - _ = McAnalysis(resPath, md, histRanges, store=True) + _ = McAnalysis(resPath, analysis_input, histRanges, store=True) def test_optimizer_1D_sim0_singlecore(self): - # use a simulation for fitting. - # remove any prior results file: resPath = Path("test_resultssim_1D_singlecore.h5") - if resPath.is_file(): - resPath.unlink() - - # measurement data: - mds = mc_data_1d.McData1D( - filename=Path("testdata", "nPSize4.dat"), - nbins=0, # no rebinning - csvargs={ - "sep": ";", - "header": None, - "names": ["Q", "I", "ISigma"], - "usecols": [0, 3, 4], - }, - dataRange=[0.04, 1], - ) - # simulation data: - simd = mc_data_1d.McData1D( - filename=Path("testdata", "fancyCubePD0p01.nxs"), - pathDict={ - "Q": "/sasentry1/sasdata1/Q", - "I": "/sasentry1/sasdata1/I", - "ISigma": "/sasentry1/sasdata1/Idev", - }, - dataRange=[0, 38], # clip last datapoint for neatness - ) - - # run the Monte Carlo method - mh = mc_hat.McHat( - modelName="sim", - nContrib=300, - modelDType="default", - fitParameterLimits={"factor": (20, 40)}, - staticParameters={ - "extrapY0": 2.21e-09, - "extrapScaling": 9.61e01, - "simDataQ0": simd.measData["Q"][0], - "simDataQ1": None, - "simDataI": simd.measData["I"], - "simDataISigma": simd.measData["ISigma"], - }, - # staticParameters={"extrapY0": 2.21e-09, "extrapScaling": 9.61e+01, - # "simDataDict": simd.measData}, - maxIter=1e5, - convCrit=14, - nRep=4, - nCores=1, - seed=None, - ) - mds.store(resPath) - md = mds.measData.copy() - mh.run(md, resPath) - - histRanges = pandas.DataFrame( - [ - dict( - parameter="factor", - nBin=50, - binScale="log", - presetRangeMin=0.1, - presetRangeMax=3, - binWeighting="vol", - autoRange=True, - ), - dict( - parameter="factor", - nBin=50, - binScale="linear", - presetRangeMin=0.1, - presetRangeMax=3, - binWeighting="vol", - autoRange=False, - ), - ] - ) - _ = McAnalysis(resPath, md, histRanges, store=True) + analysis_input = run_simulation_fit(resPath, n_cores=1) + histRanges = factor_hist_ranges() + _ = McAnalysis(resPath, analysis_input, histRanges, store=True) def test_optimizer_1D_sim1_multicore(self): - # use a simulation for fitting. - # remove any prior results file: resPath = Path("test_resultssim_1D_multicore.h5") - if resPath.is_file(): - resPath.unlink() - - # measurement data: - mds = mc_data_1d.McData1D( - filename=Path("testdata", "nPSize4.dat"), - nbins=0, # no rebinning - csvargs={ - "sep": ";", - "header": None, - "names": ["Q", "I", "ISigma"], - "usecols": [0, 3, 4], - }, - dataRange=[0.04, 1], - ) - # simulation data: - simd = mc_data_1d.McData1D( - filename=Path("testdata", "fancyCubePD0p01.nxs"), - pathDict={ - "Q": "/sasentry1/sasdata1/Q", - "I": "/sasentry1/sasdata1/I", - "ISigma": "/sasentry1/sasdata1/Idev", - }, - dataRange=[0, 38], # clip last datapoint for neatness - ) - - # run the Monte Carlo method - mh = mc_hat.McHat( - modelName="sim", - nContrib=300, - modelDType="default", - fitParameterLimits={"factor": (20, 40)}, - staticParameters={ - "extrapY0": 2.21e-09, - "extrapScaling": 9.61e01, - "simDataQ0": simd.measData["Q"][0], - "simDataQ1": None, - "simDataI": simd.measData["I"], - "simDataISigma": simd.measData["ISigma"], - }, - maxIter=1e5, - convCrit=14, - nRep=4, - nCores=2, - seed=None, - ) - mds.store(resPath) - md = mds.measData.copy() - mh.run(md, resPath) - - histRanges = pandas.DataFrame( - [ - dict( - parameter="factor", - nBin=50, - binScale="log", - presetRangeMin=0.1, - presetRangeMax=3, - binWeighting="vol", - autoRange=True, - ), - dict( - parameter="factor", - nBin=50, - binScale="linear", - presetRangeMin=0.1, - presetRangeMax=3, - binWeighting="vol", - autoRange=False, - ), - ] - ) - _ = McAnalysis(resPath, md, histRanges, store=True) - - def test_optimizer_1D_sim2_histogram(self): - # can only be run after the test_optimizer_1D_sim has been run - resPath = Path("test_resultssim_1D_multicore.h5") - assert resPath.exists(), "MC optimization not done yet, run the sim test first" - - # measurement data: - mds = mc_data_1d.McData1D( - filename=Path("testdata", "nPSize4.dat"), - nbins=0, # no rebinning - csvargs={ - "sep": ";", - "header": None, - "names": ["Q", "I", "ISigma"], - "usecols": [0, 3, 4], - }, - dataRange=[0.04, 1], - ) - md = mds.measData.copy() - - histRanges = pandas.DataFrame( - [ - dict( - parameter="factor", - nBin=50, - binScale="log", - presetRangeMin=0.1, - presetRangeMax=3, - binWeighting="vol", - autoRange=True, - ), - dict( - parameter="factor", - nBin=50, - binScale="linear", - presetRangeMin=0.1, - presetRangeMax=3, - binWeighting="vol", - autoRange=False, - ), - ] - ) - _ = McAnalysis(resPath, md, histRanges, store=True) - - def test_optimizer_1D_sphere_rehistogram(self): - # same as above, but include a test of the re-histogramming functionality: - # remove any prior results file: - resPath = Path("test_resultssphere_rehist.h5") - if resPath.is_file(): - resPath.unlink() - - mds = mc_data_1d.McData1D( - filename=Path("testdata", "quickstartdemo1.csv"), - nbins=100, - csvargs={"sep": ";", "header": None, "names": ["Q", "I", "ISigma"]}, - ) - # load required modules - homedir = os.path.expanduser("~") - # disable OpenCL for multiprocessing on CPU - os.environ["SAS_OPENCL"] = "none" - # set location where the SasView/sasmodels are installed - # sasviewPath = os.path.join(homedir, "AppData", "Local", "SasView") - sasviewPath = os.path.join(homedir, "Code", "sasmodels") # BRP-specific - if sasviewPath not in sys.path: - sys.path.append(sasviewPath) - - # run the Monte Carlo method - mh = mc_hat.McHat( - modelName="sphere", - nContrib=300, - modelDType="default", - fitParameterLimits={"radius": (1, 314)}, - staticParameters={"background": 0, "scale": 0.1e6}, - maxIter=1e5, - convCrit=1, - nRep=4, - nCores=0, - seed=None, - ) - md = mds.measData.copy() - mh.run(md, resPath) - # histogram the determined size contributions - histRanges = pandas.DataFrame( - [ - dict( - parameter="radius", - nBin=50, - binScale="log", - presetRangeMin=1, - presetRangeMax=314, - binWeighting="vol", - autoRange=True, - ), - dict( - parameter="radius", - nBin=50, - binScale="linear", - presetRangeMin=10, - presetRangeMax=100, - binWeighting="vol", - autoRange=False, - ), - ] - ) - _ = McAnalysis(resPath, md, histRanges, store=True) - - # now change the histograms and re-run: - histRanges = pandas.DataFrame( - [ - dict( - parameter="radius", - nBin=20, - binScale="linear", - presetRangeMin=10, - presetRangeMax=34, - binWeighting="vol", - autoRange=True, - ), - dict( - parameter="radius", - nBin=60, - binScale="log", - presetRangeMin=1, - presetRangeMax=200, - binWeighting="vol", - autoRange=False, - ), - ] - ) - _ = McAnalysis(resPath, md, histRanges, store=True) + analysis_input = run_simulation_fit(resPath, n_cores=2) + histRanges = factor_hist_ranges() + _ = McAnalysis(resPath, analysis_input, histRanges, store=True) def test_optimizer_1D_sphere_state(self): # (re-)creates a state for the restore-state test. @@ -663,28 +436,20 @@ def test_optimizer_1D_sphere_state(self): if resPath.is_file(): resPath.unlink() - mds = mc_data_1d.McData1D( + analysis_input = workflows.prepare_1d_processing_data_from_file( filename=Path("testdata", "quickstartdemo1.csv"), nbins=100, csvargs={"sep": ";", "header": None, "names": ["Q", "I", "ISigma"]}, ) - mds.store(filename=resPath) # run the Monte Carlo method - mh = mc_hat.McHat( - modelName="sphere", - nContrib=300, - modelDType="default", - fitParameterLimits={"radius": (1, 314)}, - staticParameters={"background": 0, "scale": 0.1e6}, - maxIter=1e5, - convCrit=1, - nRep=4, - nCores=0, - seed=None, + mh = build_hat( + model_name="sphere", + fit_parameter_limits={"radius": (1, 314)}, + static_parameters={"background": 0, "scale": 0.1e6}, + conv_crit=1, ) - md = mds.measData.copy() - mh.run(md, resPath) + workflows.optimize_processing_data(analysis_input, resPath, hat=mh) # histogram the determined size contributions histRanges = pandas.DataFrame( [ @@ -699,56 +464,12 @@ def test_optimizer_1D_sphere_state(self): ), ] ) - _ = McAnalysis(resPath, md, histRanges, store=True) + _ = McAnalysis(resPath, analysis_input, histRanges, store=True) # state created - # def test_optimizer_1D_sphere_restorestate(self): - # can we recover a state as stored in the HDF5 file?: - del mds, mh, md, histRanges + del mh, analysis_input, histRanges - mds = mc_data_1d.McData1D(loadFromFile=resPath) - # load required modules - # run the Monte Carlo method - mh = mc_hat.McHat( - modelName="sphere", - nContrib=300, - modelDType="default", - fitParameterLimits={"radius": (1, 314)}, - staticParameters={"background": 0, "scale": 0.1e6}, - maxIter=1e5, - convCrit=1, - nRep=4, - nCores=0, - seed=None, - ) - md = mds.measData.copy() - mh.run(md, resPath) - # histogram the determined size contributions - histRanges = pandas.DataFrame( - [ - dict( - parameter="radius", - nBin=50, - binScale="log", - presetRangeMin=1, - presetRangeMax=314, - binWeighting="vol", - autoRange=True, - ), - dict( - parameter="radius", - nBin=50, - binScale="linear", - presetRangeMin=10, - presetRangeMax=100, - binWeighting="vol", - autoRange=False, - ), - ] - ) - _ = McAnalysis(resPath, md, histRanges, store=True) - - # now change the histograms and re-run: + analysis_input = workflows.load_result_processing_data(resPath) histRanges = pandas.DataFrame( [ dict( @@ -771,41 +492,39 @@ def test_optimizer_1D_sphere_state(self): ), ] ) - _ = McAnalysis(resPath, md, histRanges, store=True) + _ = McAnalysis(resPath, analysis_input, histRanges, store=True) + @pytest.mark.slow def test_optimizer_1D_sphere_accuratestate(self): # (re-)creates an accurate state for histogramming tests. resPath = Path("test_accuratestate.h5") if resPath.is_file(): resPath.unlink() - mds = mc_data_1d.McData1D( + analysis_input = workflows.prepare_1d_processing_data_from_file( filename=Path("testdata", "quickstartdemo1.csv"), nbins=100, csvargs={"sep": ";", "header": None, "names": ["Q", "I", "ISigma"]}, ) - mds.store(filename=resPath) # run the Monte Carlo method - mh = mc_hat.McHat( - modelName="sphere", - nContrib=300, - modelDType="default", - fitParameterLimits={"radius": (3.14, 314)}, - staticParameters={ + mh = build_hat( + model_name="sphere", + n_contrib=300, + fit_parameter_limits={"radius": (3.14, 314)}, + static_parameters={ "background": 0, "scale": 1, "sld": 77.93, "sld_solvent": 9.45, }, - maxIter=1e5, - convCrit=1, - nRep=50, - nCores=2, + max_iter=100000, + conv_crit=1, + n_rep=50, + n_cores=2, seed=None, ) - md = mds.measData.copy() - mh.run(md, resPath) + workflows.optimize_processing_data(analysis_input, resPath, hat=mh) # histogram the determined size contributions histRanges = pandas.DataFrame( [ @@ -847,15 +566,14 @@ def test_optimizer_1D_sphere_accuratestate(self): ), ] ) - _ = McAnalysis(resPath, md, histRanges, store=True) + _ = McAnalysis(resPath, analysis_input, histRanges, store=True) # state created # def test_optimizer_1D_sphere_rehistogram_accuratestate(self): # for troubleshooting the histogramming function : - del mds, md, histRanges, mh + del analysis_input, histRanges, mh - mds = mc_data_1d.McData1D(loadFromFile=resPath) - md = mds.measData.copy() + analysis_input = workflows.load_result_processing_data(resPath) # histogram the determined size contributions histRanges = pandas.DataFrame( [ @@ -897,19 +615,13 @@ def test_optimizer_1D_sphere_accuratestate(self): ), ] ) - mcres = McAnalysis(resPath, md, histRanges, store=True) + mcres = McAnalysis(resPath, analysis_input, histRanges, store=True) # test whether the volume fraction of the first population is within expectation: - np.testing.assert_allclose( - mcres._averagedModes.loc[1, "totalValue"]["valMean"], 0.027, atol=0.001 - ) + np.testing.assert_allclose(mcres._averagedModes.loc[1, "totalValue"]["valMean"], 0.027, atol=0.001) # test whether the volume fraction of the second population is within expectation: - np.testing.assert_allclose( - mcres._averagedModes.loc[2, "totalValue"]["valMean"], 9.01e-02, atol=0.001 - ) + np.testing.assert_allclose(mcres._averagedModes.loc[2, "totalValue"]["valMean"], 9.01e-02, atol=0.001) # test whether the volume fraction of the third population is within expectation: - np.testing.assert_allclose( - mcres._averagedModes.loc[3, "totalValue"]["valMean"], 9.57e-02, atol=0.001 - ) + np.testing.assert_allclose(mcres._averagedModes.loc[3, "totalValue"]["valMean"], 9.57e-02, atol=0.001) # test whether the mean dimension of the first population is within expectation: np.testing.assert_allclose(mcres._averagedModes.loc[1, "mean"]["valMean"], 1.11e01, atol=1) # test whether the mean dimension of the first population is within expectation: @@ -923,25 +635,20 @@ def test_optimizer_1D_gaussianchain(self): if resPath.is_file(): resPath.unlink() - md = mc_data_1d.McData1D( - filename=Path(r"testdata/S2870 BSA THF 1 1 d.pdh"), dataRange=[0.1, 4], nbins=50 + analysis_input = workflows.prepare_1d_processing_data_from_file( + Path(r"testdata/S2870 BSA THF 1 1 d.pdh"), + dataRange=[0.1, 4], + nbins=50, ) - md.store(resPath) # run the Monte Carlo method - mh = mc_hat.McHat( - modelName="mono_gauss_coil", - nContrib=300, - modelDType="default", - fitParameterLimits={"rg": (1, 20)}, - staticParameters={"background": 0, "i_zero": 0.00319}, - maxIter=1e5, - convCrit=2, - nRep=5, - nCores=0, - seed=None, + mh = build_hat( + model_name="mono_gauss_coil", + fit_parameter_limits={"rg": (1, 20)}, + static_parameters={"background": 0, "i_zero": 0.00319}, + conv_crit=2, ) # test step seems to be broken? Maybe same issue with multicore processing with sasview - mh.run(md.measData, resPath) + workflows.optimize_processing_data(analysis_input, resPath, hat=mh) histRanges = pandas.DataFrame( [ dict( @@ -955,7 +662,7 @@ def test_optimizer_1D_gaussianchain(self): ), ] ) - _ = McAnalysis(resPath, md.measData, histRanges, store=True) + _ = McAnalysis(resPath, analysis_input, histRanges, store=True) def broken_test_optimizer_1D_sphere_plus_fractal(self): """Thsi does not work as fractal model does not have a volume.""" @@ -964,25 +671,21 @@ def broken_test_optimizer_1D_sphere_plus_fractal(self): if resPath.is_file(): resPath.unlink() - md = mc_data_1d.McData1D( - filename=Path(r"testdata/S2870 BSA THF 1 1 d.pdh"), dataRange=[0.1, 4], nbins=50 + analysis_input = workflows.prepare_1d_processing_data_from_file( + Path(r"testdata/S2870 BSA THF 1 1 d.pdh"), + dataRange=[0.1, 4], + nbins=50, ) - md.store(resPath) # run the Monte Carlo method - mh = mc_hat.McHat( - modelName="sphere+fractal", - nContrib=300, - modelDType="default", - fitParameterLimits={"A_radius": (1, 20)}, - staticParameters={"background": 0, "i_zero": 0.00319}, - maxIter=1e3, - convCrit=1, - nRep=5, - nCores=0, - seed=None, + mh = build_hat( + model_name="sphere+fractal", + fit_parameter_limits={"A_radius": (1, 20)}, + static_parameters={"background": 0, "i_zero": 0.00319}, + max_iter=1000, + conv_crit=1, ) # test step seems to be broken? Maybe same issue with multicore processing with sasview - mh.run(md.measData, resPath) + workflows.optimize_processing_data(analysis_input, resPath, hat=mh) histRanges = pandas.DataFrame( [ dict( @@ -996,58 +699,49 @@ def broken_test_optimizer_1D_sphere_plus_fractal(self): ), ] ) - _ = McAnalysis(resPath, md.measData, histRanges, store=True) + _ = McAnalysis(resPath, analysis_input, histRanges, store=True) def test_optimizer_nxsas_io(self): - tpath = Path("testdata", "test_nexus_io.nxs") - # tests whether I can read and write in the same nexus file - if tpath.is_file(): - tpath.unlink() - hpath = Path("testdata", "20190725_11_expanded_stacked_processed_190807_161306.nxs") - - shutil.copy(hpath, tpath) - - od = mc_data_1d.McData1D(filename=tpath) - od.store(filename=tpath) - - mh = mc_hat.McHat( - modelName="sphere", - nContrib=300, - modelDType="default", - fitParameterLimits={"radius": (0.2, 160)}, - staticParameters={"background": 0, "scale": 1e3}, - maxIter=1e5, - convCrit=4000, - nRep=4, - nCores=0, - seed=None, - ) - - mh.run(od.measData.copy(), tpath) - # histogram the determined size contributions - histRanges = pandas.DataFrame( - [ - dict( - parameter="radius", - nBin=50, - binScale="log", - presetRangeMin=1, - presetRangeMax=314, - binWeighting="vol", - autoRange=True, - ), - dict( - parameter="radius", - nBin=50, - binScale="linear", - presetRangeMin=1, - presetRangeMax=10, - binWeighting="vol", - autoRange=False, - ), - ] - ) - _ = McAnalysis(tpath, od.measData.copy(), histRanges, store=True) + with tempfile.TemporaryDirectory() as tmpdir: + tpath = Path(tmpdir, "test_nexus_io.nxs") + hpath = Path("testdata", "20190725_11_expanded_stacked_processed_190807_161306.nxs") + + shutil.copy(hpath, tpath) + + analysis_input = workflows.prepare_1d_processing_data_from_file(tpath) + + mh = build_hat( + model_name="sphere", + fit_parameter_limits={"radius": (0.2, 160)}, + static_parameters={"background": 0, "scale": 1e3}, + max_iter=500, + conv_crit=4000, + ) + + workflows.optimize_processing_data(analysis_input, tpath, hat=mh) + histRanges = pandas.DataFrame( + [ + dict( + parameter="radius", + nBin=50, + binScale="log", + presetRangeMin=1, + presetRangeMax=314, + binWeighting="vol", + autoRange=True, + ), + dict( + parameter="radius", + nBin=50, + binScale="linear", + presetRangeMin=1, + presetRangeMax=10, + binWeighting="vol", + autoRange=False, + ), + ] + ) + _ = McAnalysis(tpath, analysis_input, histRanges, store=True) if __name__ == "__main__": diff --git a/tests/test_preprocessing_fast.py b/tests/test_preprocessing_fast.py new file mode 100644 index 0000000..707206f --- /dev/null +++ b/tests/test_preprocessing_fast.py @@ -0,0 +1,167 @@ +import numpy as np +import pandas +import pandas.testing as pdt +import pytest + +from mcsas3.data_adapters import bundle_from_1d_dataframe, bundle_from_2d_arrays +from mcsas3.preprocessing import ( + prepare_1d_bundle, + prepare_2d_bundle, + rebin_1d_bundle, + reconstruct_2d_from_clipped_bundle, +) + + +def test_prepare_1d_bundle_preserves_extra_compatibility_columns(): + frame = pandas.DataFrame( + data={ + "Q": np.array([0.5, 1.0, 2.0, 4.0, 5.0], dtype=float), + "I": np.array([5.0, 10.0, 20.0, 40.0, 50.0], dtype=float), + "ISigma": np.array([0.5, 1.0, 2.0, 4.0, 5.0], dtype=float), + "sample_id": np.array([10, 20, 30, 40, 50], dtype=int), + } + ) + raw_bundle = bundle_from_1d_dataframe(frame.loc[:, ["Q", "I", "ISigma"]]) + + prepared = prepare_1d_bundle( + raw_bundle, + data_range=[1.0, 5.0], + omit_q_ranges=[[1.5, 3.0]], + nbins=0, + source_frame=frame, + ) + + expected = frame.iloc[[1, 3]].reset_index(drop=True) + pdt.assert_frame_equal(prepared.clipped.frame.reset_index(drop=True), expected) + pdt.assert_frame_equal(prepared.binned.frame.reset_index(drop=True), expected) + np.testing.assert_allclose(prepared.binned.bundle["Q"].signal, np.array([1.0, 4.0])) + np.testing.assert_allclose(prepared.binned.bundle["signal"].signal, np.array([10.0, 40.0])) + + +def test_rebin_1d_bundle_returns_minimal_statistics_contract(): + frame = pandas.DataFrame( + data={ + "Q": np.array([1.0, 2.0, 20.0], dtype=float), + "I": np.array([10.0, 14.0, 100.0], dtype=float), + "ISigma": np.array([1.0, 1.0, 2.0], dtype=float), + } + ) + clipped_bundle = bundle_from_1d_dataframe(frame) + + prepared = rebin_1d_bundle(clipped_bundle, nbins=2, iemin=0.1, source_frame=frame) + + assert len(prepared.frame) == 2 + assert list(prepared.frame.columns) == ["Q", "I", "ISigma", "QSigma"] + + first_bin = prepared.frame.iloc[0] + second_bin = prepared.frame.iloc[1] + + np.testing.assert_allclose(first_bin["Q"], 1.5) + np.testing.assert_allclose(first_bin["I"], 12.0) + np.testing.assert_allclose(first_bin["ISigma"], 2.0) + np.testing.assert_allclose(first_bin["QSigma"], 0.5) + + np.testing.assert_allclose(second_bin["Q"], 20.0) + np.testing.assert_allclose(second_bin["I"], 100.0) + np.testing.assert_allclose(second_bin["ISigma"], 10.0) + np.testing.assert_allclose(second_bin["QSigma"], 0.2) + + np.testing.assert_allclose(prepared.bundle["Q"].signal, np.array([1.5, 20.0])) + np.testing.assert_allclose(prepared.bundle["signal"].signal, np.array([12.0, 100.0])) + + +def test_rebin_1d_bundle_uses_absolute_intensity_for_uncertainty_floor(): + frame = pandas.DataFrame( + data={ + "Q": np.array([1.0], dtype=float), + "I": np.array([-5.0], dtype=float), + "ISigma": np.array([0.0], dtype=float), + } + ) + clipped_bundle = bundle_from_1d_dataframe(frame) + + prepared = rebin_1d_bundle(clipped_bundle, nbins=1, iemin=0.1, source_frame=frame) + + np.testing.assert_allclose(prepared.frame.loc[0, "ISigma"], 0.5) + + +def test_prepare_1d_bundle_rejects_malformed_data_range(): + frame = pandas.DataFrame( + data={ + "Q": np.array([1.0, 2.0], dtype=float), + "I": np.array([10.0, 14.0], dtype=float), + "ISigma": np.array([1.0, 1.0], dtype=float), + } + ) + raw_bundle = bundle_from_1d_dataframe(frame) + + with pytest.raises(ValueError, match="data_range must contain exactly two values"): + prepare_1d_bundle(raw_bundle, data_range=[1.0], nbins=0) + + +def test_prepare_2d_bundle_rejects_negative_nbins(): + coords = np.array([-1.5, -0.5, 0.5, 1.5], dtype=float) + qx, qy = np.meshgrid(coords, coords) + intensity = np.arange(16, dtype=float).reshape(4, 4) + sigma = np.ones((4, 4), dtype=float) + raw_bundle = bundle_from_2d_arrays(intensity=intensity, intensity_sigma=sigma, qx=qx, qy=qy) + + with pytest.raises(ValueError, match="nbins must be zero or positive"): + prepare_2d_bundle( + raw_bundle, + data_range=[0.0, 1.0], + ortho_q0_range=[0.0, 1.0], + ortho_q1_range=[0.0, 1.0], + nbins=-1, + ) + + +def test_prepare_2d_bundle_clips_canonical_bundle_without_mcd_data(): + coords = np.array([-1.5, -0.5, 0.5, 1.5], dtype=float) + qx, qy = np.meshgrid(coords, coords) + intensity = np.arange(16, dtype=float).reshape(4, 4) + sigma = np.ones((4, 4), dtype=float) + mask = np.zeros((4, 4), dtype=bool) + mask[1, 1] = True + sigma[1, 2] = 0.0 + raw_bundle = bundle_from_2d_arrays(intensity=intensity, intensity_sigma=sigma, qx=qx, qy=qy, mask=mask) + + prepared = prepare_2d_bundle( + raw_bundle, + data_range=[0.0, 1.0], + ortho_q0_range=[0.0, 1.0], + ortho_q1_range=[0.0, 1.0], + nbins=0, + ) + + np.testing.assert_array_equal(prepared.clipped["signal"].signal, np.array([[5.0, 6.0], [9.0, 10.0]])) + np.testing.assert_array_equal(prepared.clipped["Qx"].signal, np.array([[-0.5, 0.5], [-0.5, 0.5]])) + np.testing.assert_array_equal(prepared.clipped["Qy"].signal, np.array([[-0.5, -0.5], [0.5, 0.5]])) + assert prepared.binned is not prepared.clipped + np.testing.assert_array_equal(prepared.binned["signal"].signal, prepared.clipped["signal"].signal) + + +def test_reconstruct_2d_from_clipped_bundle_restores_model_values_into_valid_pixels(): + coords = np.array([-1.5, -0.5, 0.5, 1.5], dtype=float) + qx, qy = np.meshgrid(coords, coords) + intensity = np.arange(16, dtype=float).reshape(4, 4) + sigma = np.ones((4, 4), dtype=float) + mask = np.zeros((4, 4), dtype=bool) + mask[1, 1] = True + sigma[1, 2] = 0.0 + raw_bundle = bundle_from_2d_arrays(intensity=intensity, intensity_sigma=sigma, qx=qx, qy=qy, mask=mask) + + prepared = prepare_2d_bundle( + raw_bundle, + data_range=[0.0, 1.0], + ortho_q0_range=[0.0, 1.0], + ortho_q1_range=[0.0, 1.0], + nbins=0, + ) + + reconstructed = reconstruct_2d_from_clipped_bundle(prepared.clipped, np.array([100.0, 200.0])) + + assert reconstructed.shape == (2, 2) + assert np.isnan(reconstructed[0, 0]) + assert np.isnan(reconstructed[0, 1]) + np.testing.assert_allclose(reconstructed[1], np.array([100.0, 200.0])) diff --git a/tests/test_public_api_fast.py b/tests/test_public_api_fast.py new file mode 100644 index 0000000..a224503 --- /dev/null +++ b/tests/test_public_api_fast.py @@ -0,0 +1,97 @@ +import importlib.util +from pathlib import Path + +import numpy as np +import pandas + +import mcsas3 + + +def test_public_api_exports_canonical_workflow_entrypoints(): + assert mcsas3.DEFAULT_ANALYSIS_STAGE == mcsas3.STAGE_BINNED + assert mcsas3.ProcessingData is not None + assert mcsas3.DataBundle is not None + assert mcsas3.BaseData is not None + assert callable(mcsas3.prepare_1d_processing_data) + assert callable(mcsas3.prepare_1d_processing_data_from_file) + assert callable(mcsas3.prepare_2d_processing_data) + assert callable(mcsas3.prepare_2d_processing_data_from_file) + assert callable(mcsas3.optimize_processing_data) + assert callable(mcsas3.load_result_processing_data) + assert callable(mcsas3.selected_bundle_from_processing) + + +def test_public_api_prepare_1d_processing_data_from_dataframe(): + frame = pandas.DataFrame( + { + "Q": np.array([0.5, 1.0, 2.0, 4.0, 5.0], dtype=float), + "I": np.array([5.0, 10.0, 20.0, 40.0, 50.0], dtype=float), + "ISigma": np.array([0.5, 1.0, 2.0, 4.0, 5.0], dtype=float), + } + ) + + processing = mcsas3.prepare_1d_processing_data( + frame, + data_range=[1.0, 5.0], + omit_q_ranges=[[1.5, 3.0]], + nbins=0, + analysis_stage=mcsas3.STAGE_CLIPPED, + ) + + assert isinstance(processing, mcsas3.ProcessingData) + assert getattr(processing, "analysis_stage") == mcsas3.STAGE_CLIPPED + np.testing.assert_allclose(mcsas3.selected_bundle_from_processing(processing)["Q"].signal, np.array([1.0, 4.0])) + + +def test_public_api_prepare_1d_processing_data_from_file(tmp_path): + filename = tmp_path / "input.csv" + filename.write_text("0.1;1.0;0.1\n0.2;2.0;0.2\n") + + processing = mcsas3.prepare_1d_processing_data_from_file( + filename, + csvargs={"sep": ";", "header": None, "names": ["Q", "I", "ISigma"]}, + QUnits="1 / angstrom", + IUnits="1 / centimeter / steradian", + nbins=0, + ) + + selected = mcsas3.selected_bundle_from_processing(processing) + np.testing.assert_allclose(selected["Q"].signal, np.array([1.0, 2.0])) + np.testing.assert_allclose(selected["signal"].signal, np.array([100.0, 200.0])) + np.testing.assert_allclose(selected["signal"].uncertainties["propagate_to_all"], np.array([10.0, 20.0])) + + +def test_public_api_result_processing_round_trip(tmp_path): + result_file = tmp_path / "result.h5" + processing = mcsas3.prepare_1d_processing_data( + pandas.DataFrame( + { + "Q": np.array([1.0, 2.0], dtype=float), + "I": np.array([10.0, 20.0], dtype=float), + "ISigma": np.array([1.0, 2.0], dtype=float), + } + ), + nbins=0, + ) + + mcsas3.store_result_processing_data(result_file, processing, metadata={"filename": Path("input.dat")}) + restored = mcsas3.load_result_processing_data(result_file) + + np.testing.assert_allclose(restored[mcsas3.STAGE_RAW]["Q"].signal, np.array([1.0, 2.0])) + + +def test_quickstart_notebook_uses_canonical_workflow_api(): + notebook_text = Path("notebooks/McSAS3.ipynb").read_text() + + assert "prepare_1d_processing_data_from_file" in notebook_text + assert "optimize_processing_data" in notebook_text + assert "McAnalysis(resPath, processing" in notebook_text + assert "McData1D" not in notebook_text + assert "measDataLink" not in notebook_text + assert "analysis_data_from_bundle" not in notebook_text + + +def test_legacy_mcdata_modules_are_removed(): + assert importlib.util.find_spec("mcsas3.mc_data") is None + assert importlib.util.find_spec("mcsas3.mc_data_1d") is None + assert importlib.util.find_spec("mcsas3.mc_data_2d") is None diff --git a/tests/test_runtime_paths_fast.py b/tests/test_runtime_paths_fast.py new file mode 100644 index 0000000..c1d6667 --- /dev/null +++ b/tests/test_runtime_paths_fast.py @@ -0,0 +1,12 @@ +from pathlib import Path + +from mcsas3.runtime_paths import example_configuration_path, quickstart_testdata_path, runtime_resource_root + + +def test_runtime_resource_paths_resolve_checkout_resources(): + root = runtime_resource_root() + + assert root == Path(__file__).resolve().parents[1] + assert example_configuration_path("read_config_csv.yaml").is_file() + assert example_configuration_path("hist_config_dual.yaml").is_file() + assert quickstart_testdata_path("quickstartdemo1.csv").is_file() diff --git a/tests/test_workflows_fast.py b/tests/test_workflows_fast.py new file mode 100644 index 0000000..f35bed7 --- /dev/null +++ b/tests/test_workflows_fast.py @@ -0,0 +1,204 @@ +from pathlib import Path + +import h5py +import numpy as np +import pandas +import pytest + +from mcsas3.data_adapters import STAGE_BINNED, STAGE_CLIPPED, STAGE_RAW, selected_bundle_from_processing +from mcsas3.workflows import ( + load_result_processing_data, + optimize_processing_data, + prepare_1d_processing_data, + prepare_1d_processing_data_from_file, + prepare_2d_processing_data_from_file, + store_result_processing_data, +) + + +def _sample_frame() -> pandas.DataFrame: + return pandas.DataFrame( + data={ + "Q": np.array([0.5, 1.0, 2.0, 4.0, 5.0], dtype=float), + "I": np.array([5.0, 10.0, 20.0, 40.0, 50.0], dtype=float), + "ISigma": np.array([0.5, 1.0, 2.0, 4.0, 5.0], dtype=float), + } + ) + + +def _write_test_2d_nexus(filename: Path) -> None: + qx = np.array([[-0.5, 0.5], [-0.5, 0.5]], dtype=float) + qy = np.array([[-0.5, -0.5], [0.5, 0.5]], dtype=float) + q = np.stack([qy, qx, np.zeros_like(qx)], axis=0) + intensity = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=float) + sigma = np.array([[0.1, 0.2], [0.3, 0.4]], dtype=float) + mask = np.array([[False, True], [False, False]], dtype=bool) + + with h5py.File(filename, "w") as h5f: + h5f.attrs["default"] = "entry" + entry = h5f.create_group("entry") + entry.attrs["default"] = "data" + data = entry.create_group("data") + data.attrs["signal"] = "I" + data.attrs["I_uncertainty"] = "I_unc" + data.attrs["mask"] = "mask" + data.attrs["axes"] = np.array(["q"], dtype="S") + signal = data.create_dataset("I", data=intensity) + signal.attrs["units"] = "1 / centimeter / steradian" + sigma_ds = data.create_dataset("I_unc", data=sigma) + sigma_ds.attrs["units"] = "1 / centimeter / steradian" + q_ds = data.create_dataset("q", data=q) + q_ds.attrs["units"] = "1 / angstrom" + data.create_dataset("mask", data=mask) + + +def test_prepare_1d_processing_data_builds_canonical_stages(): + processing = prepare_1d_processing_data( + _sample_frame(), + data_range=[1.0, 5.0], + omit_q_ranges=[[1.5, 3.0]], + nbins=0, + analysis_stage=STAGE_CLIPPED, + ) + + assert set(processing.keys()) == {STAGE_RAW, STAGE_CLIPPED, STAGE_BINNED} + assert getattr(processing, "analysis_stage") == STAGE_CLIPPED + np.testing.assert_allclose(processing[STAGE_RAW]["Q"].signal, np.array([0.5, 1.0, 2.0, 4.0, 5.0])) + np.testing.assert_allclose(selected_bundle_from_processing(processing)["Q"].signal, np.array([1.0, 4.0])) + np.testing.assert_allclose(selected_bundle_from_processing(processing)["signal"].signal, np.array([10.0, 40.0])) + + +def test_store_and_load_result_processing_data_round_trip(tmp_path): + result_file = tmp_path / "workflow_result.h5" + processing = prepare_1d_processing_data(_sample_frame(), data_range=[1.0, 5.0], nbins=2) + + store_result_processing_data( + result_file, + processing, + result_index=2, + metadata={"filename": Path("input.dat"), "nbins": 2}, + ) + + restored = load_result_processing_data(result_file, result_index=2) + + assert getattr(restored, "analysis_stage") == getattr(processing, "analysis_stage") + np.testing.assert_allclose(restored[STAGE_RAW]["Q"].signal, processing[STAGE_RAW]["Q"].signal) + np.testing.assert_allclose(restored[STAGE_BINNED]["signal"].signal, processing[STAGE_BINNED]["signal"].signal) + with h5py.File(result_file, "r") as h5f: + assert "/analyses/MCResult2/mcdata/filename" in h5f + assert h5f["/analyses/MCResult2/mcdata/nbins"][()] == 2 + + +def test_optimize_processing_data_runs_mchat_on_selected_bundle_and_stores_processing(tmp_path): + class RecordingHat: + def __init__(self) -> None: + self.calls = [] + + def run(self, analysis_data, filename, resultIndex=1) -> None: + self.calls.append((analysis_data, Path(filename), resultIndex)) + + processing = prepare_1d_processing_data( + _sample_frame(), + data_range=[1.0, 5.0], + omit_q_ranges=[[1.5, 3.0]], + analysis_stage=STAGE_CLIPPED, + ) + result_file = tmp_path / "optimized_result.h5" + hat = RecordingHat() + + returned = optimize_processing_data( + processing, + result_file, + result_index=2, + hat=hat, + processing_metadata={"filename": Path("input.dat")}, + ) + + assert returned is hat + assert len(hat.calls) == 1 + analysis_data, filename, result_index = hat.calls[0] + assert analysis_data is selected_bundle_from_processing(processing) + assert filename == result_file + assert result_index == 2 + + restored = load_result_processing_data(result_file, result_index=2) + np.testing.assert_allclose(restored[STAGE_CLIPPED]["Q"].signal, np.array([1.0, 4.0])) + + +def test_optimize_processing_data_rejects_hat_and_hat_kwargs_together(tmp_path): + class RecordingHat: + def run(self, analysis_data, filename, resultIndex=1) -> None: + return None + + with pytest.raises(ValueError, match="either an McHat instance or McHat keyword arguments"): + optimize_processing_data( + prepare_1d_processing_data(_sample_frame(), nbins=0), + tmp_path / "unused_result.h5", + hat=RecordingHat(), + nRep=1, + ) + + +def test_prepare_1d_processing_data_from_csv_file(tmp_path): + filename = tmp_path / "input.csv" + filename.write_text("0.1;1.0;0.1\n0.2;2.0;0.2\n") + + processing = prepare_1d_processing_data_from_file( + filename, + csvargs={"sep": ";", "header": None, "names": ["Q", "I", "ISigma"]}, + QUnits="1 / angstrom", + IUnits="1 / centimeter / steradian", + nbins=0, + ) + + np.testing.assert_allclose(processing[STAGE_RAW]["Q"].signal, np.array([1.0, 2.0])) + np.testing.assert_allclose(processing[STAGE_RAW]["signal"].signal, np.array([100.0, 200.0])) + + +def test_prepare_1d_processing_data_from_nexus_file_detects_units(tmp_path): + filename = tmp_path / "input.nxs" + + with h5py.File(filename, "w") as h5f: + h5f.attrs["default"] = "entry" + entry = h5f.create_group("entry") + entry.attrs["default"] = "data" + data = entry.create_group("data") + data.attrs["signal"] = "I" + data.attrs["I_uncertainty"] = "I_unc" + data.attrs["axes"] = np.array(["q"], dtype="S") + signal = data.create_dataset("I", data=np.array([1.0, 2.0], dtype=float)) + signal.attrs["units"] = "1 / centimeter / steradian" + sigma = data.create_dataset("I_unc", data=np.array([0.1, 0.2], dtype=float)) + sigma.attrs["units"] = "1 / centimeter / steradian" + q = data.create_dataset("q", data=np.array([0.1, 0.2], dtype=float)) + q.attrs["units"] = "1 / angstrom" + + processing = prepare_1d_processing_data_from_file(filename, nbins=0) + + np.testing.assert_allclose(processing[STAGE_RAW]["Q"].signal, np.array([1.0, 2.0])) + np.testing.assert_allclose(processing[STAGE_RAW]["signal"].signal, np.array([100.0, 200.0])) + + +def test_prepare_1d_processing_data_rejects_invalid_omit_ranges(): + with pytest.raises(ValueError, match="omit_q_ranges\\[0\\] must contain exactly two values"): + prepare_1d_processing_data(_sample_frame(), omit_q_ranges=[[1.0]], nbins=0) + + +def test_prepare_2d_processing_data_from_nexus_file_detects_units(tmp_path): + filename = tmp_path / "input_2d.nxs" + _write_test_2d_nexus(filename) + + processing = prepare_2d_processing_data_from_file( + filename, + dataRange=[0.0, 10.0], + orthoQ0Range=[0.0, 10.0], + orthoQ1Range=[0.0, 10.0], + nbins=0, + analysisStage=STAGE_CLIPPED, + ) + + assert getattr(processing, "analysis_stage") == STAGE_CLIPPED + np.testing.assert_allclose(processing[STAGE_RAW]["Qx"].signal, np.array([[-5.0, 5.0], [-5.0, 5.0]])) + np.testing.assert_allclose(processing[STAGE_RAW]["Qy"].signal, np.array([[-5.0, -5.0], [5.0, 5.0]])) + np.testing.assert_allclose(processing[STAGE_RAW]["signal"].signal, np.array([[100.0, 200.0], [300.0, 400.0]])) + np.testing.assert_array_equal(processing[STAGE_CLIPPED]["mask"].signal, np.array([[False, True], [False, False]])) diff --git a/tools/build_standalone.py b/tools/build_standalone.py new file mode 100644 index 0000000..73188dd --- /dev/null +++ b/tools/build_standalone.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""Build standalone CLI bundles for the supported McSAS3 entry points.""" + +from __future__ import annotations + +import importlib +import json +import os +import platform +import shutil +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SRC_DIR = ROOT / "src" +HOOKS_DIR = ROOT / "tools" / "pyinstaller_hooks" +BUILD_ROOT = ROOT / "build" / "standalone" +DIST_ROOT = ROOT / "dist" / "standalone" +os.environ.setdefault("PYINSTALLER_CONFIG_DIR", str(BUILD_ROOT / "pyinstaller-cache")) +os.environ.setdefault("MPLCONFIGDIR", str(BUILD_ROOT / "matplotlib-cache")) + +ENTRYPOINTS = ( + ( + "mcsas3-runner", + ROOT / "src" / "mcsas3" / "mcsas3_cli_runner.py", + ("plotly", "IPython", "jedi", "nbformat", "jsonschema", "zmq"), + ), + ( + "mcsas3-histogrammer", + ROOT / "src" / "mcsas3" / "mcsas3_cli_histogrammer.py", + (), + ), +) + + +def _platform_tag() -> str: + system = platform.system().lower() + machine = platform.machine().lower().replace("x86_64", "amd64") + return f"{system}-{machine}" + + +def _add_data_arg(source: Path, destination: str) -> str: + return f"{source}{':' if platform.system() != 'Windows' else ';'}{destination}" + + +def _bundle_root() -> Path: + return DIST_ROOT / _platform_tag() + + +def _pyinstaller_args(name: str, script_path: Path, bundle_root: Path, excluded_modules: tuple[str, ...]) -> list[str]: + args = [ + "--noconfirm", + "--clean", + "--onedir", + "--name", + name, + "--paths", + str(SRC_DIR), + "--additional-hooks-dir", + str(HOOKS_DIR), + "--distpath", + str(bundle_root / "apps"), + "--workpath", + str(BUILD_ROOT / "work"), + "--specpath", + str(BUILD_ROOT / "spec"), + "--hidden-import", + "modacor", + "--hidden-import", + "modacor.units", + "--hidden-import", + "modacor.dataclasses.basedata", + "--hidden-import", + "modacor.dataclasses.databundle", + "--hidden-import", + "modacor.dataclasses.processing_data", + "--add-data", + _add_data_arg(ROOT / "example_configurations", "example_configurations"), + "--add-data", + _add_data_arg(ROOT / "testdata" / "quickstartdemo1.csv", "testdata"), + str(script_path), + ] + for module_name in excluded_modules: + args.extend(["--exclude-module", module_name]) + return args + + +def _write_bundle_readme(bundle_root: Path) -> None: + lines = [ + "McSAS3 standalone CLI bundle", + "", + "Included executables:", + "- apps/mcsas3-runner/", + "- apps/mcsas3-histogrammer/", + "", + "Each executable directory also includes bundled example configurations and quickstart data", + "so the built-in default CLI paths resolve correctly in standalone mode.", + "", + "Examples:", + " ./apps/mcsas3-runner/mcsas3-runner --help", + " ./apps/mcsas3-histogrammer/mcsas3-histogrammer --help", + ] + (bundle_root / "README_STANDALONE.txt").write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def _write_build_info(bundle_root: Path) -> None: + payload = { + "platform": _platform_tag(), + "system": platform.system(), + "machine": platform.machine(), + "artifacts": [name for name, _script, _excluded_modules in ENTRYPOINTS], + } + (bundle_root / "build_info.json").write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") + + +def _archive_bundle(bundle_root: Path) -> Path: + archive_base = DIST_ROOT / f"mcsas3-standalone-{bundle_root.name}" + archive_path = Path( + shutil.make_archive( + str(archive_base), + "zip", + root_dir=bundle_root.parent, + base_dir=bundle_root.name, + ) + ) + return archive_path + + +def _pyinstaller_main(): + """Import and return the PyInstaller entry module only when building.""" + + return importlib.import_module("PyInstaller.__main__") + + +def _run_smoke_test(bundle_root: Path) -> None: + suffix = ".exe" if platform.system() == "Windows" else "" + for entry_name, _script, _excluded_modules in ENTRYPOINTS: + executable = bundle_root / "apps" / entry_name / f"{entry_name}{suffix}" + result = subprocess.run( + [str(executable), "--help"], + check=False, + capture_output=True, + text=True, + ) + if result.returncode != 0: + raise RuntimeError( + f"Standalone smoke test failed for {entry_name}: exit {result.returncode}\n" + f"stdout:\n{result.stdout}\n" + f"stderr:\n{result.stderr}" + ) + + +def main() -> None: + """Build standalone CLI directories and a zip archive for the current platform.""" + + bundle_root = _bundle_root() + shutil.rmtree(BUILD_ROOT, ignore_errors=True) + shutil.rmtree(bundle_root, ignore_errors=True) + bundle_root.mkdir(parents=True, exist_ok=True) + pyinstaller_main = _pyinstaller_main() + + for entry_name, script_path, excluded_modules in ENTRYPOINTS: + pyinstaller_main.run(_pyinstaller_args(entry_name, script_path, bundle_root, excluded_modules)) + + _write_bundle_readme(bundle_root) + _write_build_info(bundle_root) + archive_path = _archive_bundle(bundle_root) + _run_smoke_test(bundle_root) + print(f"Standalone bundle created at {bundle_root}") + print(f"Standalone archive created at {archive_path}") + + +if __name__ == "__main__": + main() diff --git a/tools/generate_dependency_diagram.py b/tools/generate_dependency_diagram.py new file mode 100644 index 0000000..8d937ea --- /dev/null +++ b/tools/generate_dependency_diagram.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +"""Generate a Mermaid dependency diagram for internal McSAS3 modules.""" + +from __future__ import annotations + +import ast +from collections import defaultdict +from datetime import date +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PACKAGE_DIR = ROOT / "src" / "mcsas3" +OUTPUT_PATH = ROOT / "design_documentation" / "generated_module_dependencies.md" + +LAYER_ORDER = ( + "Public API and entry points", + "Canonical data and workflow layer", + "Optimization and analysis core", + "Other internal modules", +) + +LAYER_MEMBERS = { + "Public API and entry points": { + "__init__", + "__main__", + "cli_tools", + "mc_plot", + "mcsas3_cli_histogrammer", + "mcsas3_cli_runner", + "workflows", + }, + "Canonical data and workflow layer": { + "data_adapters", + "data_model", + "ingestion", + "mc_hdf", + "optimizer_input", + "preprocessing", + }, + "Optimization and analysis core": { + "mc_analysis", + "mc_core", + "mc_hat", + "mc_model", + "mc_model_histogrammer", + "mc_opt", + "osb", + }, +} + + +def discover_modules() -> dict[str, Path]: + """Return the top-level Python modules that make up the McSAS3 package.""" + + return {path.stem: path for path in sorted(PACKAGE_DIR.glob("*.py"))} + + +def resolve_local_dependency(import_name: str, local_modules: set[str]) -> str | None: + """Map an import target to a local top-level module when possible.""" + + if import_name.startswith("mcsas3."): + candidate = import_name.split(".", maxsplit=2)[1] + else: + candidate = import_name.split(".", maxsplit=1)[0] + return candidate if candidate in local_modules else None + + +def parse_local_dependencies(module_name: str, module_path: Path, local_modules: set[str]) -> set[str]: + """Parse a module and collect imports that point to local McSAS3 modules.""" + + tree = ast.parse(module_path.read_text(encoding="utf-8"), filename=str(module_path)) + dependencies: set[str] = set() + + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + dependency = resolve_local_dependency(alias.name, local_modules) + if dependency is not None and dependency != module_name: + dependencies.add(dependency) + elif isinstance(node, ast.ImportFrom): + if node.level > 0: + if node.module: + dependency = resolve_local_dependency(node.module, local_modules) + if dependency is not None and dependency != module_name: + dependencies.add(dependency) + else: + for alias in node.names: + dependency = resolve_local_dependency(alias.name, local_modules) + if dependency is not None and dependency != module_name: + dependencies.add(dependency) + elif node.module is not None: + dependency = resolve_local_dependency(node.module, local_modules) + if dependency is not None and dependency != module_name: + dependencies.add(dependency) + + return dependencies + + +def module_layer(module_name: str) -> str: + """Return the display layer used for a module in the diagram.""" + + for layer_name, members in LAYER_MEMBERS.items(): + if module_name in members: + return layer_name + return "Other internal modules" + + +def mermaid_node_id(module_name: str) -> str: + """Return a Mermaid-safe node identifier.""" + + return f"module_{module_name.replace('-', '_').replace('.', '_')}" + + +def generate_markdown() -> str: + """Generate the dependency diagram markdown.""" + + modules = discover_modules() + local_modules = set(modules) + dependencies = { + module_name: parse_local_dependencies(module_name, module_path, local_modules) + for module_name, module_path in modules.items() + } + layer_to_modules: dict[str, list[str]] = defaultdict(list) + for module_name in modules: + layer_to_modules[module_layer(module_name)].append(module_name) + for module_names in layer_to_modules.values(): + module_names.sort() + + lines: list[str] = [ + "# McSAS3 Module Dependency Diagram", + "", + f"Generated on: {date.today().isoformat()}", + "", + "This file is generated by `python tools/generate_dependency_diagram.py`.", + "It captures imports between top-level modules in `src/mcsas3` and is intended as a", + "maintained structure overview rather than a full call graph.", + "", + "```mermaid", + "flowchart LR", + ] + + class_members: dict[str, list[str]] = defaultdict(list) + class_members["public"] = [] + class_members["canonical"] = [] + class_members["core"] = [] + class_members["other"] = [] + class_name_for_layer = { + "Public API and entry points": "public", + "Canonical data and workflow layer": "canonical", + "Optimization and analysis core": "core", + "Other internal modules": "other", + } + + for layer_name in LAYER_ORDER: + modules_in_layer = layer_to_modules.get(layer_name, []) + if not modules_in_layer: + continue + lines.append(f' subgraph {mermaid_node_id(layer_name)}["{layer_name}"]') + for module_name in modules_in_layer: + node_id = mermaid_node_id(module_name) + lines.append(f' {node_id}["{module_name}"]') + class_members[class_name_for_layer[layer_name]].append(node_id) + lines.append(" end") + + for module_name in sorted(dependencies): + source_id = mermaid_node_id(module_name) + for dependency in sorted(dependencies[module_name]): + target_id = mermaid_node_id(dependency) + lines.append(f" {source_id} --> {target_id}") + + lines.extend( + [ + " classDef public fill:#e7f0ff,stroke:#3a66b3,color:#1d2b45;", + " classDef canonical fill:#e8f8ee,stroke:#2a7f45,color:#153523;", + " classDef core fill:#fff2df,stroke:#b06a00,color:#4d3200;", + " classDef other fill:#f2f2f2,stroke:#777777,color:#333333;", + ] + ) + + for class_name, node_ids in class_members.items(): + if node_ids: + lines.append(f" class {','.join(node_ids)} {class_name};") + + lines.extend( + [ + "```", + "", + "## Regeneration", + "", + "Run:", + "", + "```bash", + "./.venv/bin/python tools/generate_dependency_diagram.py", + "```", + "", + "## Notes", + "", + "- The diagram is generated from import statements, so it shows module coupling rather " + "than runtime call flow.", + "- External dependencies are intentionally omitted.", + "- The layer grouping is curated for readability; edges within and across layers remain generated.", + ] + ) + return "\n".join(lines) + "\n" + + +def main() -> None: + """Write the generated dependency diagram to the tracked markdown file.""" + + OUTPUT_PATH.write_text(generate_markdown(), encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/tools/pyinstaller_hooks/hook-sasmodels.py b/tools/pyinstaller_hooks/hook-sasmodels.py new file mode 100644 index 0000000..4021d4e --- /dev/null +++ b/tools/pyinstaller_hooks/hook-sasmodels.py @@ -0,0 +1,30 @@ +"""Local PyInstaller hook for sasmodels. + +This keeps the runtime kernel/model data required by McSAS3 while avoiding +bundling sasmodels documentation trees that significantly inflate the +standalone archives. +""" + +from __future__ import annotations + +from sasmodels import data_files + +hiddenimports = [ + "pyopencl", + "sasmodels.compare_many", + "sasmodels.guyou", + "sasmodels.jitter", + "sasmodels.list_pars", + "sasmodels.multiscat", + "sasmodels.special", + "sasmodels.models.two_yukawa", +] +module_collection_mode = "py" + +datas: list[tuple[str, str]] = [] +for target, filenames in data_files(): + if target.endswith("/models/img"): + continue + for filename in filenames: + datas.append((filename, target)) + datas.append((filename, target.replace("sasmodels-data", "sasmodels"))) diff --git a/tox.ini b/tox.ini index 306a5c3..4e65065 100644 --- a/tox.ini +++ b/tox.ini @@ -2,9 +2,10 @@ envlist = clean, build, + standalone, check, docs, - #{py38,py39,py310,py311}, + #{py38,py39,py310}, py, # generic Python3 with unspecified version using defaults report ignore_basepython_conflict = true @@ -14,8 +15,7 @@ basepython = py38: {env:TOXPYTHON:python3.8} py39: {env:TOXPYTHON:python3.9} py310: {env:TOXPYTHON:python3.10} - py311: {env:TOXPYTHON:python3.11} - {bootstrap,clean,build,check,report,docs, py}: {env:TOXPYTHON:python3} + {bootstrap,clean,build,check,report,docs,notebooks, py}: {env:TOXPYTHON:python3} setenv = PYTHONPATH={toxinidir}/tests PYTHONUNBUFFERED=yes @@ -37,16 +37,20 @@ deps = build skip_install = true commands = python -m build +[testenv:standalone] +deps = pyinstaller +usedevelop = true +commands = python tools/build_standalone.py + [testenv:check] deps = check-manifest - flake8 - isort + ruff skip_install = true commands = check-manifest {toxinidir} - flake8 - isort --verbose --check-only --diff --filter-files . + ruff check . + ruff format --check . [testenv:docs] deps = @@ -60,6 +64,11 @@ commands = # run the linkcheck only if the git repo exists, maybe pure local initially sh -c 'test -e .git && sphinx-build -b linkcheck docs dist/docs || true' +[testenv:notebooks] +description = Run optional notebook execution checks outside the required CI path. +commands = + pytest -vv notebooks/McSAS3.ipynb {posargs} + [testenv:report] deps = coverage @@ -77,14 +86,6 @@ commands = coverage erase rm -Rf dist build htmlcov src/mcsas3.egg-info -[flake8] -max-line-length = 100 -extend-exclude = - .ipynb_checkpoints -per-file-ignores = - # __init__.py:F401 # example -extend-ignore = E203 - [pytest] # If a pytest section is found in one of the possible config files # (pytest.ini, tox.ini or setup.cfg), then pytest will not look for any others, @@ -108,13 +109,15 @@ addopts = testpaths = src tests - notebooks # Idea from: https://til.simonwillison.net/pytest/treat-warnings-as-errors filterwarnings = ignore::DeprecationWarning:distutils|notebook|_pytest|xarray|nbclient|pytest_notebook|debugpy ignore::DeprecationWarning:pkg_resources|openpyxl|zmq # ignore warnings about unknown pytest-notebook options below ignore::Warning:_pytest +markers = + integration: Cross-module or file-backed tests that exercise multiple layers together. + slow: Tests that are too expensive for the default fast feedback loop. nb_test_files = True nb_coverage = False nb_post_processors =