pax_global_header00006660000000000000000000000064147740220100014507gustar00rootroot0000000000000052 comment=46c582cfef591ca022a3271416d493e0ac3a7f4c bumps-bumps-46c582c/000077500000000000000000000000001477402201000143215ustar00rootroot00000000000000bumps-bumps-46c582c/.git-blame-ignore-revs000066400000000000000000000001521477402201000204170ustar00rootroot00000000000000# .git-blame-ignore-revs # Added formatting to backend (PR #193) 155b502bfdd4863919322774023bfdbca731731a bumps-bumps-46c582c/.gitattributes000066400000000000000000000002211477402201000172070ustar00rootroot00000000000000*.py text eol=lf *.c text eol=lf *.h text eol=lf *.cpp text eol=lf *.rst text eol=lf *.bat text eol=crlf *.BAT text eol=crlf *.ps1 text eol=crlf bumps-bumps-46c582c/.github/000077500000000000000000000000001477402201000156615ustar00rootroot00000000000000bumps-bumps-46c582c/.github/dependabot.yaml000066400000000000000000000003301477402201000206460ustar00rootroot00000000000000# Set update schedule for GitHub Actions version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: # Check for updates to GitHub Actions every week interval: "monthly" bumps-bumps-46c582c/.github/release.yml000066400000000000000000000001631477402201000200240ustar00rootroot00000000000000# .github/release.yml changelog: exclude: labels: - ignore-for-release authors: - dependabotbumps-bumps-46c582c/.github/workflows/000077500000000000000000000000001477402201000177165ustar00rootroot00000000000000bumps-bumps-46c582c/.github/workflows/build-distributables.yml000066400000000000000000000163341477402201000245650ustar00rootroot00000000000000name: Build conda-packed distribution on: release: types: [published] workflow_dispatch: env: branch_name: master PACKAGE_NAME: bumps PYTHON_VERSION: 3.12 jobs: build_and_publish: runs-on: ${{ matrix.config.os }} defaults: run: shell: bash -el {0} strategy: matrix: config: - { os: ubuntu-latest } - { os: windows-latest } - { os: macos-latest } - { os: macos-13 } steps: - uses: actions/checkout@v4 with: ref: ${{ env.branch_name }} fetch-depth: 0 - uses: conda-incubator/setup-miniconda@v3 with: auto-update-conda: true python-version: ${{ env.PYTHON_VERSION }} miniforge-version: latest activate-environment: builder - name: Build conda-packed (all platforms) run: | conda install -y versioningit nodejs conda activate base ./extra/build_conda_packed.sh - name: Create MacOS App if: startsWith(matrix.config.os, 'macos') env: MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE_ISA }} MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_ISA_PWD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} NOTARIZATION_USERNAME: ${{ secrets.MACOS_NOTARIZATION_USERNAME }} NOTARIZATION_PASSWORD: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }} DEVELOPER_IDENTITY: "Developer ID Application: The International Scattering Alliance (8CX8K63BQM)" run: | echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12 security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security default-keychain -s build.keychain security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain security find-identity -p codesigning echo "Creating MacOS App" # This assumes that conda-packed enviroment exists at "conda_packed" VERSION=$(versioningit) MAJOR_MINOR_PATCH=$(sed -E 's/(^[0-9]+\.[0-9]+\.[0-9]+).*$/\1/' <<< "$VERSION") PRODUCT_NAME="$PACKAGE_NAME-$VERSION" FULL_PACKAGE_NAME="$PRODUCT_NAME-$(uname -s)-$(uname -m)" BUILD_DIR="${PACKAGE_NAME}_installer" mkdir -p $BUILD_DIR APP_FOLDER="$BUILD_DIR/$PRODUCT_NAME" APP="$APP_FOLDER/${PACKAGE_NAME}.app" PYTHON_FRAMEWORK="$APP/Contents/Frameworks/python.framework" PYTHON_ENV="$PYTHON_FRAMEWORK/Resources/env" ln -s /Applications "$BUILD_DIR/Applications" cp -R -P "extra/platform_resources/macos/app_folder_template" "$APP_FOLDER" cp -R -P "conda_packed" "$PYTHON_ENV" plutil -replace CFBundleVersion -string "$VERSION" "$APP/Contents/Info.plist" plutil -replace CFBundleShortVersionString -string "$MAJOR_MINOR_PATCH" "$APP/Contents/Info.plist" plutil -replace NSHumanReadableCopyright -string "Copyright $(date +'%Y') the $PACKAGE_NAME developers" "$APP/Contents/Info.plist" sign_file() { codesign --force --timestamp --options=runtime --verify --verbose=4 --sign "${DEVELOPER_IDENTITY}" "$1" } SRC_ENTITLEMENTS="extra/platform_resources/macos/app_folder_template/${PACKAGE_NAME}.app/Contents/Frameworks/python.framework/Resources/Entitlements.plist" sign_with_entitlements() { codesign --verify --options=runtime --entitlements "${SRC_ENTITLEMENTS}" --timestamp --verbose=4 --force --sign "${DEVELOPER_IDENTITY}" "$1" } # Sign shared libraries find "$PYTHON_ENV" -name "*\.so" -print0 | while IFS= read -r -d '' file; do sign_file "$file"; done; find "$PYTHON_ENV" -name "*\.dylib" -print0 | while IFS= read -r -d '' file; do sign_file "$file"; done; find "$PYTHON_ENV/bin" -type f -perm +111 -print0 | while IFS= read -r -d '' file; do sign_file "$file"; done; # Sign python framework with entitlements sign_with_entitlements "$PYTHON_FRAMEWORK/Versions/A/Resources/Entitlements.plist" PYTHON_EXECUTABLE="$(realpath $PYTHON_ENV/bin/python)" sign_with_entitlements "$PYTHON_EXECUTABLE" sign_with_entitlements "$PYTHON_FRAMEWORK" # Sign apps codesign --verify --options=runtime --timestamp --verbose=4 --force --sign "${DEVELOPER_IDENTITY}" "$APP/Contents/MacOS/${PACKAGE_NAME}_webview" codesign --verify --options=runtime --timestamp --verbose=4 --force --sign "${DEVELOPER_IDENTITY}" "$APP" codesign --verify --options=runtime --timestamp --verbose=4 --force --sign "${DEVELOPER_IDENTITY}" "$APP_FOLDER/${PACKAGE_NAME}_shell.app" DMG="artifacts/$FULL_PACKAGE_NAME.dmg" mkdir -p artifacts hdiutil create $DMG -srcfolder "$BUILD_DIR" -ov -format UDZO codesign -s "${DEVELOPER_IDENTITY}" $DMG # Clean up signing keys security delete-keychain build.keychain rm certificate.p12 # Notarize xcrun notarytool submit --wait --apple-id "$NOTARIZATION_USERNAME" --password "$NOTARIZATION_PASSWORD" --team-id 8CX8K63BQM $DMG # Staple xcrun stapler staple $DMG - name: Create Windows Installer if: startsWith(matrix.config.os, 'windows') run: | echo "Creating Windows Installer" # This assumes that conda-packed enviroment exists at "conda_packed" VERSION=$(versioningit) MAJOR_MINOR_PATCH=$(sed -E 's/(^[0-9]+\.[0-9]+\.[0-9]+).*$/\1/' <<< "$VERSION") FULL_PACKAGE_NAME="$PACKAGE_NAME-$VERSION-Windows-$(uname -m)" # clean up junk from conda-pack operation: find conda_packed -name "*.conda_trash" -delete conda install -y nsis # create the installer: PRODUCT_NAME="$PACKAGE_NAME-$VERSION" $CONDA_PREFIX/NSIS/makensis.exe -DPRODUCT_NAME="$PRODUCT_NAME" -DPRODUCT_VERSION="$MAJOR_MINOR_PATCH.0" extra/installer.nsi mkdir -p artifacts mv extra/BumpsWebviewSetup.exe "artifacts/$FULL_PACKAGE_NAME-installer.exe" - name: Create Linux Installer if: startsWith(matrix.config.os, 'ubuntu') run: | echo "Creating Linux Installer" conda info VERSION=$(versioningit) FULL_PACKAGE_NAME="$PACKAGE_NAME-$VERSION-$(uname -s)-$(uname -m)" PRODUCT_NAME="$PACKAGE_NAME-$VERSION" mkdir -p "$PRODUCT_NAME" cp extra/platform_resources/linux/* "$PRODUCT_NAME" mv conda_packed/ "$PRODUCT_NAME/env" mkdir -p artifacts tar -czf "artifacts/$FULL_PACKAGE_NAME.tar.gz" "$PRODUCT_NAME" - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: artifacts-${{ matrix.config.os }}-${{ matrix.config.py }} path: artifacts/* - name: Enumerate artifacts run: | # Collect the distributables { echo 'DISTRIBUTABLES<> "$GITHUB_ENV" - name: Update current release if: startsWith(github.ref, 'refs/tags') uses: johnwbyrd/update-release@v1.0.0 with: token: ${{ secrets.GITHUB_TOKEN }} files: | ${{ env.DISTRIBUTABLES }} bumps-bumps-46c582c/.github/workflows/build-webview.yml000066400000000000000000000066201477402201000232120ustar00rootroot00000000000000name: Build webview on: workflow_dispatch: env: branch_name: master jobs: test_and_build: runs-on: ${{ matrix.config.os }} strategy: matrix: config: #- { os: ubuntu-latest, py: 3.8, doc: 1 } - { os: windows-latest, py: "3.10", exe: 1, whl: 1 } #- { os: macos-latest, py: 3.8, whl: 1 } # all using to stable abi steps: - uses: actions/checkout@v4 with: ref: ${{ env.branch_name }} - name: Set up Python ${{ matrix.config.py }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.config.py }} - uses: conda-incubator/setup-miniconda@v3 with: auto-update-conda: true python-version: ${{ matrix.config.py }} - name: Build installer run: | pwsh -command ".\$GITHUB_WORKSPACE\extra\build_conda_packed.ps1" mkdir unstable pwd dir dist dir extra mv dist\bumps*.tar.gz "unstable\Bumps-windows-exe-$($env:branch_name).tar.gz" # See the following for how to upload to a release # https://eugene-babichenko.github.io/blog/2020/05/09/github-actions-cross-platform-auto-releases/ - name: Archive artifacts uses: actions/upload-artifact@v4 with: name: artifacts path: | unstable/* updateUnstable: needs: test_and_build runs-on: ubuntu-latest steps: - name: Retrieve all artifacts uses: actions/download-artifact@v4 with: name: artifacts - name: show files run: | ls * -l - name: repack self-extracting run: | sudo apt-get install -y p7zip-full mkdir self_extracting curl -L https://www.7-zip.org/a/7z2106-x64.exe --output 7z.exe 7z e 7z.exe -aoa -oself_extracting 7z.sfx tar -xzf "Bumps-windows-exe-$branch_name.tar.gz" -C self_extracting cd self_extracting && 7z a -mhe=on -mx=1 -sfx7z.sfx "../Bumps-$branch_name-self-extracting.exe" bumps*/ - name: Update release assets and text uses: actions/github-script@v7 with: github-token: ${{secrets.GITHUB_TOKEN}} script: | const fs = require('fs'); const { owner, repo } = context.repo; let sid_release = await github.rest.repos.getReleaseByTag({ owner, repo, tag: "sid" }); await github.rest.repos.updateRelease({ owner, repo, release_id: sid_release.data.id, body: "A persistent prerelease where build artifacts for the current tip will be deposited\n\n## Last updated: " + (new Date()).toDateString() }); // delete existing release assets (if needed) and upload new ones: const to_update = ["Bumps-windows-exe-${{ env.branch_name }}.tar.gz", "Bumps-${{ env.branch_name }}-self-extracting.exe"]; for (let fn of to_update) { let asset_id = (sid_release.data.assets.find((a) => (a.name == fn)) ?? {}).id; if (asset_id) { await github.rest.repos.deleteReleaseAsset({ owner, repo, asset_id }); } await github.rest.repos.uploadReleaseAsset({ owner, repo, release_id: sid_release.data.id, name: fn, data: await fs.readFileSync(fn) }); } bumps-bumps-46c582c/.github/workflows/test-publish.yml000066400000000000000000000065551477402201000230770ustar00rootroot00000000000000name: Test and Publish to PyPI on: push: branches: [master] pull_request: release: types: [published] workflow_dispatch: defaults: run: shell: bash jobs: # Build a pure Python wheel and upload as an artifact build-wheel: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Checkout Random123 uses: actions/checkout@v4 with: repository: "DEShawResearch/random123" ref: v1.14.0 path: bumps/dream/random123 - name: Install uv uses: astral-sh/setup-uv@v5 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.10" - uses: actions/setup-node@v4 with: node-version: 20 - name: build webview run: | cd bumps/webview/client npm install npm run build - name: Create the wheel run: uv build - name: Upload the wheel and source distribution artifacts uses: actions/upload-artifact@v4 with: name: artifacts path: | dist/bumps-*-py3-none-any.whl dist/bumps-*.tar.gz # Test the wheel on different platforms, test webview, and check docs build test: runs-on: ${{ matrix.cfg.os }} needs: build-wheel strategy: matrix: cfg: - { os: ubuntu-latest, py: 3.8 } - { os: ubuntu-latest, py: 3.9, doc: 1 } - { os: ubuntu-latest, py: "3.10" } - { os: ubuntu-latest, py: 3.11 } - { os: ubuntu-latest, py: 3.12 } - { os: windows-latest, py: "3.10" } - { os: macos-latest, py: "3.10" } fail-fast: false steps: - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v5 - name: Set up Python ${{ matrix.cfg.py }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.cfg.py }} - name: Download the wheel uses: actions/download-artifact@v4 with: name: artifacts path: dist - name: Install the wheel run: | uv venv find dist -name "bumps-*-py3-none-any.whl" -exec uv pip install {}[dev] \; - name: Run tests, except for webview run: uv run python -m pytest -v --ignore=bumps/webview - name: Install dependencies for webview testing run: find dist -name "bumps-*-py3-none-any.whl" -exec uv pip install {}[webview] \; - name: Run tests run: uv run python -m pytest -v - name: Check examples run: uv run python check_examples.py --chisq - name: Check fitters run: uv run python check_fitters.py - name: Check that the docs build (linux only) if: matrix.cfg.doc == 1 run: | source .venv/bin/activate make -j 4 -C doc SPHINXOPTS="-W --keep-going -n" html # Upload wheel to PyPI only when a tag is pushed, and its name begins with 'v' upload-to-pypi: runs-on: ubuntu-latest needs: test if: startsWith(github.ref, 'refs/tags/v') permissions: id-token: write steps: - name: Retrieve all artifacts uses: actions/download-artifact@v4 - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: artifacts/ bumps-bumps-46c582c/.github/workflows/test-webview-client.yaml000066400000000000000000000035021477402201000245030ustar00rootroot00000000000000name: Test Webview Client on: pull_request defaults: run: working-directory: ./bumps/webview/client jobs: # test that app can build without issues test-build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Bun uses: oven-sh/setup-bun@v2 - name: Install packages run: bun install - name: Run test run: bun run build # test that app is properly formatted and linted test-lint: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Bun uses: oven-sh/setup-bun@v2 - name: Install packages run: bun install - name: Run test run: bun run test:lint # test that app has no typescript errors test-types: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Bun uses: oven-sh/setup-bun@v2 - name: Install packages run: bun install - name: Run test run: bun run test:types # run unit tests # test-unit: # runs-on: ubuntu-latest # steps: # - name: Checkout code # uses: actions/checkout@v4 # - name: Set up Bun # uses: oven-sh/setup-bun@v2 # - name: Install packages # run: bun install # - name: Run test # run: bun run test:unit # run end to end integration tests # test-e2e: # runs-on: ubuntu-latest # steps: # - name: Checkout code # uses: actions/checkout@v4 # - name: Set up Bun # uses: oven-sh/setup-bun@v2 # - name: Install packages # run: bun install # - name: Install Playwright # run: bunx playwright install # - name: Run test # run: bun run test:e2e bumps-bumps-46c582c/.github/workflows/webview-client-publish.yml000066400000000000000000000013651477402201000250360ustar00rootroot00000000000000name: Publish Package to npmjs on: push: # Pattern matched against refs/tags tags: - 'client-v*' # Push events to every tag like client-v0.6.15 workflow_dispatch: jobs: build: runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - uses: actions/checkout@v4 # Setup .npmrc file to publish to npm - uses: actions/setup-node@v4 with: node-version: '20.x' registry-url: 'https://registry.npmjs.org' - run: | cd bumps/webview/client npm i npm run build_prod - run: cd bumps/webview/client && npm publish --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} bumps-bumps-46c582c/.gitignore000066400000000000000000000007331477402201000163140ustar00rootroot00000000000000# Eclipse/pycharm settings files .idea .project .pydevproject # editor backup files *.swp *~ *.bak # build/test .settings .coverage /build/ /dist/ /bumps.egg-info/ bumps.iss-include iss-version bumps/_version.py # doc targets /doc/_build/ /doc/api/ /doc/tutorial/ /doc/dream/ # python droppings from running in place __pycache__/ *.pyc *.pyo *.so *.pyd *.dll *.dyld # run in place sets .mplconfig .mplconfig bumps/webview/client/node_modules/ bumps/webview/client/dist bumps-bumps-46c582c/.pre-commit-config.yaml000066400000000000000000000030411477402201000206000ustar00rootroot00000000000000repos: - repo: local hooks: - id: eslint name: eslint entry: eslint files: \.(vue|js|ts|mjs|tsx|jsx)$ # *.js, *.jsx, *.ts and *.tsx types: [file] args: ["-c", "bumps/webview/client/eslint.config.js", "--fix"] additional_dependencies: ["eslint@9.15.0"] language: node - repo: local hooks: - id: prettier name: prettier entry: prettier files: \.(json|yaml|md|vue|js|ts|mjs|tsx|jsx)$ types: [file] args: ["--write", "--config", "bumps/webview/client/prettier.config.js"] additional_dependencies: ["prettier@3.4.1"] language: node - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: check-added-large-files args: [--maxkb=8192] exclude: bumps/dream/random123 - id: check-merge-conflict - id: check-yaml args: [--allow-multiple-documents] - id: end-of-file-fixer exclude: | bumps/dream/random123 bumps/webview/client/src - id: trailing-whitespace exclude: | bumps/dream/random123 bumps/webview/client/src - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.6.1 hooks: # - id: ruff # args: [--fix, --exit-non-zero-on-fix] - id: ruff-format args: [--config=pyproject.toml] exclude: | bumps/dream/random123 bumps/webview/client/src extra/platform_scripts ci: autoupdate_schedule: monthly skip: [eslint] bumps-bumps-46c582c/.readthedocs.yaml000066400000000000000000000005671477402201000175600ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 tools: python: "3.11" # Build documentation in the doc/ directory with Sphinx sphinx: configuration: doc/conf.py # If using Sphinx, optionally build your docs in additional formats such as PDF formats: - pdf - epub # Python environment required to build your docs python: install: - requirements: doc/requirements.txt bumps-bumps-46c582c/CHANGES.rst000066400000000000000000000213671477402201000161340ustar00rootroot00000000000000************** Change History ************** v0.9.3 2024-07-09 ----------------- * fixed issues with numpy > = 2.0 (see #140, `numpy.NaN` deprecated and removed) * fixed issues with refactor of scipy.stats (see #139) v0.9.2 2024-03-05 ----------------- * added testing for python 3.12 * fixed issue with matplotlib >= 3.8.0 (see #129) * added missing documents to dream manual (Convergence tests, Parallel coordinates plot) * added numba.njit-accelerated fallback bounds.apply methods to dream (still uses compiled C DLL if available) * provide MAX_CORR attribute on the CorrelationView; clear the figure if the number of variables exceeds MAX_CORR v0.9.1 2023-04-10 ----------------- * added support for python 3.11, scipy 1.10, numpy 1.24, wx 4.1.1 * fixed covariance calculation for n-D datasets * fixed batch mode I/O redirection cleanup * fixed issue with DREAM bounds checker when running in parallel * default to single precision derivatives with lm (fixes issue in SasView where OpenCL models failed with Levenberg-Marquardt) * improved support for repeat fitting within scripts and notebooks (*start_mapper* should now work after *stop_mapper*) v0.9.0 2022-03-15 ----------------- * use MPFit in place of scipy.leastsq for bounds-constrained Levenberg-Marquardt Breaking change: * simultaneous fit now scales individual nllfs by squared weight rather than weight v0.8.1 2021-11-18 ----------------- * "apply parameters" action added to GUI menu (does the same as --pars flag in CLI) * operators refactored (no more eval) * BoundedNormal keywords renamed (sigma, mu) -> (std, mean) * support for numba usage in models * fixed Parameters view jumping to top after toggling fit (linux, Mac) * fixed Summary view sliders disappearing in linux * fixed uncertainty plots regenerating at each parameter update * improved documentation of uncertainty analysis Breaking change: * python 2.7 support discontinued v0.8.0 2020-12-16 ----------------- * add stopping conditions to DREAM, using *--alpha=p-value* to reject convergence * require *--overwrite* or *--resume* when reusing a store directory * enable outlier trimming in DREAM with --outliers=iqr * add fitted slope and loglikelihood distribution to the loglikelihood plot * display seed value used for fit so it can be rerun with *--seed* * save MCMC files using gzip * remove R stat from saved state * restore *--pars* option, which was broken in 0.7.17 * terminate the MPI session when the fit is complete instead of waiting for the allocation to expire * allow a series of fits in the same MPI session * support newest matplotlib v0.7.18 2020-11-16 ------------------ * restore python 2.7 support v0.7.17 2020-11-06 ------------------ * restore DREAM fitter efficiency (it should now require fewer burn-in steps) * errplot.reload_errors allows full path to model file * clip values within bounds at start of fit so constraints aren't infinite * allow *--entropy=gmm|mvn|wnn|llf* to specify entropy estimation algorithm * allow duplicate parameter names in model on reload * expand tilde in path names * GUI: restore parallel processing * GUI: suppress uncertainty updates during fit to avoid memory leak * disable broken fitters: particle swarm, random lines, snobfit * minor doc changes v0.7.16 2020-06-11 ------------------ * improved handling of parameters for to_dict() json pickling v0.7.15 2020-06-09 ------------------ * parallel fitting suppressed in GUI for now---need to reuse thread pool * support *limits=(min, max)* for pm and pmp parameter ranges * cleaner handling of single/multiple fit specification * fix *--entropy* command line option * better support for pathlib with virtual file system v0.7.14 2020-01-03 ------------------ * support for *--checkpoint=n*, which updates the .mc files every n hours * fix bug for stuck fits on *--resume*: probabilities contain NaN * better error message for missing store directory * Python 3.8 support (time.clock no longer exists) v0.7.13 2019-10-15 ------------------ * fix pickle problem for parameterized functions * support multi-valued functions in Curve, shown with a coupled ODE example * update support for newer numpy and matplotlib v0.7.12 2019-07-30 ------------------ * --parallel defaults to using one process per CPU. * --pop=-k sets population size to k rather than k times num parameters * --resume=- resumes from --store=/path/to/store * use expanded canvas for parameter histograms to make plots more readable * use regular spaced tics for parameter histograms rather than 1- and 2-sigma * improve consistency between values of cov, stderr and chisq * fix handling of degenerate ranges on parameter output * add entropy calculator using gaussian mixture models (default is still Kramer) * vfs module allows loading of model and data from zip file (not yet enabled) * warn when model has no fitted parameters * update mpfit to support python 3 * support various versions of scipy and numpy v0.7.11 2018-09-24 ------------------ * add support for parameter serialization v0.7.10 2018-06-15 ------------------ * restructure parameter table in gui v0.7.9 2018-06-14 ----------------- * full support for python 3 in wx GUI * allow added or missing parameters in reloaded .par file * add dream state to return from fit() call v0.7.8 2018-05-18 ----------------- * fix source distribution (bin directory was missing) v0.7.7 2018-05-17 ----------------- * merge in amdahl branch for improved performance * update plot so that the displayed "chisq" is consistent with nllf * slight modification to the DREAM DE crossover ratio so that no crossover weight ever goes to zero. * par.dev(std) now uses the initial value of the parameter as the center of the distribution for a gaussian prior on par, as stated in the documentation. In older releases it was incorrectly defaulting to mean=0 if the mean was not specified. * save parameters and uncertainties as JSON as well as text * convert discrete variables to integer prior to computing DREAM statistics * allow relative imports from model files * support latest numpy/matplotlib stack * initial support for wxPhoenix/python 4 GUI (fit ranges can't yet be set) v0.7.6 2016-08-05 ----------------- * add --view option to command line which gets propagated to the model plotter * add support for probability p(x) for vector x using VectorPDF(f,x0) * rename DirectPDF to DirectProblem, and allow it to run in GUI * data reader supports multi-part files, with parts separated by blank lines * add gaussian mixture and laplace examples * bug fix: plots were failing if model name contains a '.' * miscellaneous code cleanup v0.7.5.10 2016-05-04 -------------------- * gui: undo code cleaning operation which broke the user interface v0.7.5.9 2016-04-22 ------------------- * population initializers allow indefinite bounds * use single precision criterion for levenberg-marquardt and bfgs * implement simple, faster, less accurate Hessian & Jacobian * compute uncertainty estimate from Jacobian if problem is sum of squares * gui: fit selection window acts like a dialog v0.7.5.8 2016-04-18 ------------------- * accept model.par output from a different model * show residuals with curve fit output * only show correlations for selected variables * show tics on correlations if small number * improve handling of uncertainty estimate from curvature * tweak dream algorithm -- maybe improve the acceptance ratio? * allow model to set visible variables in output * improve handling of arbitrary probability density functions * simplify loading of pymc models * update to numdifftools 0.9.14 * bug fix: improved handling of ill-conditioned fits * bug fix: avoid copying mcmc chain during run * bug fix: more robust handling of --time limit * bug fix: support newer versions of matplotlib and numpy * miscellaneous tweaks and fixes v0.7.5.7 2015-09-21 ------------------- * add entropy calculator (still unreliable for high dimensional problems) * adjust scaling of likelihood (the green line) to match histogram area * use --samples to specify the number of samples from the distribution * mark this and future releases with a DOI at zenodo.org v0.7.5.6 2015-06-03 ------------------- * tweak uncertainty calculations so they don't fail on bad models v0.7.5.5 2015-05-07 ------------------- * documentation updates v0.7.5.4 2014-12-05 ------------------- * use relative rather than absolute noise in dream, which lets us fit target values in the order of 1e-6 or less. * fix covariance population initializer v0.7.5.3 2014-11-21 ------------------- * use --time to stop after a given number of hours * Levenberg-Marquardt: fix "must be 1-d or 2-d" bug * improve curvefit interface v0.7.5.2 2014-09-26 ------------------- * pull numdifftools dependency into the repository v0.7.5.1 2014-09-25 ------------------- * improve the load_model interface v0.7.5 2014-09-10 ----------------- * Pure python release bumps-bumps-46c582c/LICENSE.txt000077500000000000000000000151501477402201000161510ustar00rootroot00000000000000Bumps is in the public domain. Code in individual files has copyright and license set by individual authors. bumps.gui, bumps.quasinewton ---------------------------- Copyright (C) 2006-2011, University of Maryland Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/ or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. DREAM ----- Copyright (c) 2008, Los Alamos National Security, LLC All rights reserved. Copyright 2008. Los Alamos National Security, LLC. This software was produced under U.S. Government contract DE-AC52-06NA25396 for Los Alamos National Laboratory (LANL), which is operated by Los Alamos National Security, LLC for the U.S. Department of Energy. The U.S. Government has rights to use, reproduce, and distribute this software. NEITHER THE GOVERNMENT NOR LOS ALAMOS NATIONAL SECURITY, LLC MAKES A NY WARRANTY, EXPRESS OR IMPLIED, OR ASSUMES ANY LIABILITY FOR THE USE OF THIS SOFTWARE. If software is modified to produce derivative works, such modified software should be clearly marked, so as not to confuse it with the version available from LANL. Additionally, redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Los Alamos National Security, LLC, Los Alamos National Laboratory, LANL the U.S. Government, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY LOS ALAMOS NATIONAL SECURITY, LLC AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL LOS ALAMOS NATIONAL SECURITY, LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Random123 --------- Copyright 2010-2012, D. E. Shaw Research. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions, and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions, and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of D. E. Shaw Research nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. MPFit ----- The original version of this software, called LMFIT, was written in FORTRAN as part of the MINPACK-1 package by XXX. Craig Markwardt converted the FORTRAN code to IDL. The information for the IDL version is: Craig B. Markwardt, NASA/GSFC Code 662, Greenbelt, MD 20770 craigm@lheamail.gsfc.nasa.gov UPDATED VERSIONs can be found on my WEB PAGE: http://cow.physics.wisc.edu/~craigm/idl/idl.html Mark Rivers created this Python version from Craig's IDL version. Mark Rivers, University of Chicago Building 434A, Argonne National Laboratory 9700 South Cass Avenue, Argonne, IL 60439 rivers@cars.uchicago.edu Updated versions can be found at http://cars.uchicago.edu/software bumps.simplex ------------- ******NOTICE*************** From optimize.py module by Travis E. Oliphant You may copy and use this module as you see fit with no guarantee implied provided you keep this notice in all copies. *****END NOTICE************ bumps.cli.warn_with_traceback ----------------------------- From http://stackoverflow.com/questions/22373927/get-traceback-of-warnings answered by mgab (2014-03-13) edited by Gareth Rees (2015-11-28) bumps.dream.entropy.Timer ------------------------- Based on: Eli Bendersky https://stackoverflow.com/a/5849861 Extended with tic/toc by Paul Kienzle bumps.dream.entropy.MultivariateT.rvs ------------------------------------- From farhawa on stack overflow https://stackoverflow.com/questions/29798795/multivariate-student-t-distribution-with-python bumps.lsqerror.comb ------------------- From dheerosaur https://stackoverflow.com/questions/4941753/is-there-a-math-ncr-function-in-python/4941932#4941932 bumps-bumps-46c582c/MANIFEST.in000077500000000000000000000023251477402201000160640ustar00rootroot00000000000000# The purpose of this file is to modify the list of files to include/exclude in # the source archive created by the 'python setup.py sdist' command. Executing # setup.py in the top level directory creates a default list (or manifest) and # the directives in this file add or subtract files from the resulting MANIFEST # file that drives the creation of the archive. # # Note: apparently due to a bug in setup, you cannot include a file whose name # starts with 'build' as in 'build_everything.py'. # Add files to the archive in addition to those that are installed by running # 'python setup.py install'. Typically these extra files are build related. include MANIFEST.in # this file include bin/* recursive-include bumps/webview/client/dist *.html *.js *.css *.svg *.png recursive-include bumps/webview/client/src *.html *.js *.mjs *.css *.svg *.png *.ts *.vue recursive-include bumps/dream/random123/include *.h include bumps/dream/random123/LICENSE include bumps/dream/compiled.c include bumps/webview/client/*.html include bumps/webview/client/*.js include bumps/webview/client/*.json include bumps/webview/client/*.txt include extra/*.svg extra/*.png # Delete files # ex. prune this that prune bumps/webview/client/node_modules bumps-bumps-46c582c/Makefile000066400000000000000000000044471477402201000157720ustar00rootroot00000000000000ROOTDIR = $(shell pwd) # Check for the presence of bun or npm ifneq ($(shell which bun),) FE_CMD=bun else ifneq ($(shell which npm),) FE_CMD=npm else echo "No frontend build tool found. Please install 'bun' or 'npm'." exit 1 endif # This nifty perl one-liner collects all comments headed by the double "#" symbols next to each target and recycles them as comments .PHONY: help help: ## Print this help message @perl -nle'print $& if m{^[/a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}' .PHONY: clean clean: ## Delete some cruft from builds/testing/etc. rm -f `find . -type f -name '*.py[co]'` rm -rf `find . -name __pycache__ -o -name "*.egg-info"` \ `find . -name 'output-*'` \ .coverage build dist \ doc/_build doc/api doc/tutorial \ .pytest_cache \ .ruff_cache .PHONY: test test: ## Run pytest and doc tests pytest -v python check_examples.py --chisq python check_fitters.py ####################### ### Dev environment ### ####################### .PHONY: dev-backend dev-backend: ## Start the backend server in headless mode bumps-webview --port 8080 --headless .PHONY: dev-frontend dev-frontend: ## Start the frontend server in development mode cd bumps/webview/client && \ $(FE_CMD) run dev ############################## ### Linting and formatting ### ############################## .PHONY: lint lint: lint-backend lint-frontend ## Run all linters .PHONY: lint-backend-check lint-backend-check: ## Run ruff linting on python code @ruff check bumps/ run.py test.py check_*.py .PHONY: lint-backend lint-backend: ## Run ruff linting fix on python code @ruff check --fix bumps/ run.py test.py check_*.py .PHONY: lint-frontend-check lint-frontend-check: ## Run bun linting check on javascript code cd bumps/webview/client && \ $(FE_CMD) run test:lint .PHONY: lint-frontend lint-frontend: ## Run bun linting fix on javascript code cd bumps/webview/client && \ $(FE_CMD) run lint .PHONY: format format: format-backend format-frontend ## Run all formatters .PHONY: format-backend format-backend: ## Run ruff formatting on python code @ruff format bumps/ run.py test.py check_*.py .PHONY: format-frontend format-frontend: ## Run bun formatting on javascript code cd bumps/webview/client && \ $(FE_CMD) run format bumps-bumps-46c582c/README.amdahl000066400000000000000000000041101477402201000164220ustar00rootroot00000000000000For large fits across mutliple nodes you may find that the proposal step is a bottleneck. The compiled DE stepper can speed this up by a factor of 2 compared to the numba version that is usually used. This might help on large allocations, but not enough to support it in the automatic build infrastructure. Update: MSVC is 6x faster than numba on one machine. Need to check performance with and without compiled on HPC hardware to know if the compiled version is required. To use the compiled de stepper and bounds checks, first make sure the "random123" library submodule has been checked out git clone --branch v1.14.0 https://github.com/DEShawResearch/random123.git bumps/dream/random123 Then, to compile on unix use: (cd bumps/dream && cc compiled.c -I ./random123/include/ -O2 -fopenmp -shared -lm -o _compiled.so -fPIC -DMAX_THREADS=64) On OS/X clang doesn't support OpenMP: (cd bumps/dream && cc compiled.c -I ./random123/include/ -O2 -shared -lm -o _compiled.so -fPIC -DMAX_THREADS=64) MSVC on windows using Visual Studio build tools (2022): % set up compiler environment "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" x86_amd64 cd bumps\dream cl compiled.c -I .\random123\include /O2 /openmp /LD /GL /Fe_compiled.so This only works when _compiled.so is in the bumps/dream directory. If running from a pip installed version, you will need to fetch the bumps repository: $ git clone https://github.com/bumps/bumps.git $ cd bumps Compile as above, then find the bumps install path using the following: $ python -c "import bumps.dream; print(bumps.dream.__file__)" #dream/path/__init__.py Copy the compiled module to the install, with the #dream/path printed above: $ cp bumps/dream/_compiled.so #dream/path There is no provision for using _compiled.so in a frozen application. Run with no more than 64 OMP threads. If the number of processors is more than 64, then use: OMP_NUM_THREADS=64 ./run.py ... I don't know how OMP_NUM_THREADS behaves if it is larger than the number of processors. bumps-bumps-46c582c/README.rst000077500000000000000000000033201477402201000160110ustar00rootroot00000000000000============================================== Bumps: data fitting and uncertainty estimation ============================================== Bumps provides data fitting and Bayesian uncertainty modeling for inverse problems. It has a variety of optimization algorithms available for locating the most like value for function parameters given data, and for exploring the uncertainty around the minimum. Installation is with the usual python installation command:: pip install bumps Once the system is installed, you can verify that it is working with:: bumps doc/examples/peaks/model.py --chisq Documentation is available at `readthedocs `_. See `CHANGES.rst `_ for details on recent changes. If a compiler is available, then significant speedup is possible for DREAM using:: python -m bumps.dream.build_compiled (If you have installed from source, you must first check out the random123 library):: git clone --branch v1.14.0 https://github.com/DEShawResearch/random123.git bumps/dream/random123 python -m bumps.dream.build_compiled For now this requires an install from source rather than pip. |CI| |RTD| |DOI| .. |CI| image:: https://github.com/bumps/bumps/actions/workflows/test-publish.yml/badge.svg :alt: Build status :target: https://github.com/bumps/bumps/actions/workflows/test-publish.yml .. |DOI| image:: https://zenodo.org/badge/18489/bumps/bumps.svg :alt: DOI tag :target: https://zenodo.org/badge/latestdoi/18489/bumps/bumps .. |RTD| image:: https://readthedocs.org/projects/bumps/badge/?version=latest :alt: Documentation status :target: https://bumps.readthedocs.io/en/latest/?badge=latest bumps-bumps-46c582c/bumps/000077500000000000000000000000001477402201000154475ustar00rootroot00000000000000bumps-bumps-46c582c/bumps/__init__.py000066400000000000000000000024421477402201000175620ustar00rootroot00000000000000# This program is in the public domain # Author: Paul Kienzle """ Bumps: curve fitter with uncertainty estimation This package provides tools for modeling parametric systems in a Bayesian context, with routines for finding the maximum likelihood and the posterior probability density function. A graphical interface allows direct manipulation of the model parameters. See https://bumps.readthedocs.io for online manuals. """ try: from ._version import __version__ # noqa: F401 except ImportError: __version__ = "unknown" __schema_version__ = "1" def data_files(): """ Return the data files associated with the package for setup_py2exe.py. The format is a list of (directory, [files...]) pairs which can be used directly in the py2exe setup script as:: setup(..., data_files=data_files(), ...) """ from .gui.utilities import data_files return data_files() def package_data(): """ Return the data files associated with the package for setup.py. The format is a dictionary of {'fully.qualified.module', [files...]} used directly in the setup script as:: setup(..., package_data=package_data(), ...) """ from .gui.utilities import package_data return package_data() bumps-bumps-46c582c/bumps/__main__.py000066400000000000000000000003061477402201000175400ustar00rootroot00000000000000""" Bumps application. Run "bumps --help" for details, or "python -m bumps -help" if bumps isn't on your path. """ from bumps.webview.server.cli import main if __name__ == "__main__": main() bumps-bumps-46c582c/bumps/bounds.py000066400000000000000000000650761477402201000173310ustar00rootroot00000000000000# This program is in the public domain # Author: Paul Kienzle """ Parameter bounds and prior probabilities. Parameter bounds encompass several features of our optimizers. First and most trivially they allow for bounded constraints on parameter values. Secondly, for parameter values known to follow some distribution, the bounds encodes a penalty function as the value strays from its nominal value. Using a negative log likelihood cost function on the fit, then this value naturally contributes to the overall likelihood measure. Predefined bounds are:: Unbounded range (-inf, inf) BoundedBelow range (base, inf) BoundedAbove range (-inf, base) Bounded range (low, high) Normal range (-inf, inf) with gaussian probability BoundedNormal range (low, high) with gaussian probability within SoftBounded range (low, high) with gaussian probability outside New bounds can be defined following the abstract base class interface defined in :class:`Bounds`, or using Distribution(rv) where rv is a scipy.stats continuous distribution. For generating bounds given a value, we provide a few helper functions:: v +/- d: pm(x,dx) or pm(x,-dm,+dp) or pm(x,+dp,-dm) return (x-dm,x+dm) limited to 2 significant digits v +/- p%: pmp(x,p) or pmp(x,-pm,+pp) or pmp(x,+pp,-pm) return (x-pm*x/100, x+pp*x/100) limited to 2 sig. digits pm_raw(x,dx) or raw_pm(x,-dm,+dp) or raw_pm(x,+dp,-dm) return (x-dm,x+dm) pmp_raw(x,p) or raw_pmp(x,-pm,+pp) or raw_pmp(x,+pp,-pm) return (x-pm*x/100, x+pp*x/100) nice_range(lo,hi) return (lo,hi) limited to 2 significant digits """ __all__ = [ "pm", "pmp", "pm_raw", "pmp_raw", "nice_range", "init_bounds", "DistProtocol", "Bounds", "Unbounded", "Bounded", "BoundedAbove", "BoundedBelow", "Distribution", "Normal", "BoundedNormal", "SoftBounded", ] from dataclasses import dataclass, field import math from math import log, log10, sqrt, pi, ceil, floor from numpy import inf, isinf, isfinite, clip import numpy.random as RNG try: from scipy.stats import norm as normal_distribution except ImportError: # Normal distribution is an optional dependency. Leave it as a runtime # failure if it doesn't exist. pass from typing import Optional, Any, Dict, Union, Literal, Tuple, Protocol LimitValue = Union[float, Literal["-inf", "inf"]] LimitsType = Tuple[Union[float, Literal["-inf"]], Union[float, Literal["inf"]]] # TODO: should we use this in the bounds limits? # @dataclass(init=False) # class ExtendedFloat(float): # __root__: Union[float, Literal["inf", "-inf"]] # def __new__(cls, *args, **kw): # return super().__new__(cls, *args) # def __init__(self, __root__=None): # pass # def __repr__(self): # return float.__repr__(self) def pm(v, plus, minus=None, limits: Optional[LimitsType] = None): """ Return the tuple (~v-dv,~v+dv), where ~expr is a 'nice' number near to to the value of expr. For example:: >>> r = pm(0.78421, 0.0023145) >>> print("%g - %g"%r) 0.7818 - 0.7866 If called as pm(value, +dp, -dm) or pm(value, -dm, +dp), return (~v-dm, ~v+dp). """ return nice_range(limited_range(pm_raw(v, plus, minus), limits=limits)) def pmp(v, plus, minus=None, limits=None): """ Return the tuple (~v-%v,~v+%v), where ~expr is a 'nice' number near to the value of expr. For example:: >>> r = pmp(0.78421, 10) >>> print("%g - %g"%r) 0.7 - 0.87 >>> r = pmp(0.78421, 0.1) >>> print("%g - %g"%r) 0.7834 - 0.785 If called as pmp(value, +pp, -pm) or pmp(value, -pm, +pp), return (~v-pm%v, ~v+pp%v). """ return nice_range(limited_range(pmp_raw(v, plus, minus), limits=limits)) # Generate ranges using x +/- dx or x +/- p%*x def pm_raw(v, plus, minus=None): """ Return the tuple [v-dv,v+dv]. If called as pm_raw(value, +dp, -dm) or pm_raw(value, -dm, +dp), return (v-dm, v+dp). """ if minus is None: minus = -plus if plus < minus: plus, minus = minus, plus return v + minus, v + plus def pmp_raw(v, plus, minus=None): """ Return the tuple [v-%v,v+%v] If called as pmp_raw(value, +pp, -pm) or pmp_raw(value, -pm, +pp), return (v-pm%v, v+pp%v). """ if minus is None: minus = -plus if plus < minus: plus, minus = minus, plus b1, b2 = v * (1 + 0.01 * minus), v * (1 + 0.01 * plus) return (b1, b2) if v > 0 else (b2, b1) def limited_range(bounds, limits=None): """ Given a range and limits, fix the endpoints to lie within the range """ if limits is not None: return clip(bounds[0], *limits), clip(bounds[1], *limits) return bounds def nice_range(bounds): """ Given a range, return an enclosing range accurate to two digits. """ step = bounds[1] - bounds[0] if step > 0: d = 10 ** (floor(log10(step)) - 1) return floor(bounds[0] / d) * d, ceil(bounds[1] / d) * d else: return bounds def init_bounds(v) -> "Bounds": """ Returns a bounds object of the appropriate type given the arguments. This is a helper factory to simplify the user interface to parameter objects. """ # if it is none, then it is unbounded if v is None: return Unbounded() # if it isn't a tuple, assume it is a bounds type. try: lo, hi = v except TypeError: return v # if it is a tuple, then determine what kind of bounds we have if lo is None: lo = -inf if hi is None: hi = inf # TODO: consider issuing a warning instead of correcting reversed bounds if lo >= hi: lo, hi = hi, lo if isinf(lo) and isinf(hi): return Unbounded() elif isinf(lo): return BoundedAbove(hi) elif isinf(hi): return BoundedBelow(lo) else: return Bounded(lo, hi) class Bounds: """ Bounds abstract base class. A range is used for several purposes. One is that it transforms parameters between unbounded and bounded forms depending on the needs of the optimizer. Another is that it generates random values in the range for stochastic optimizers, and for initialization. A third is that it returns the likelihood of seeing that particular value for optimizers which use soft constraints. Assuming the cost function that is being optimized is also a probability, then this is an easy way to incorporate information from other sorts of measurements into the model. """ # TODO: need derivatives wrt bounds transforms @property def limits(self): return (-inf, inf) def get01(self, x): """ Convert value into [0,1] for optimizers which are bounds constrained. This can also be used as a scale bar to show approximately how close to the end of the range the value is. """ def put01(self, v): """ Convert [0,1] into value for optimizers which are bounds constrained. """ def getfull(self, x): """ Convert value into (-inf,inf) for optimizers which are unconstrained. """ def putfull(self, v): """ Convert (-inf,inf) into value for optimizers which are unconstrained. """ def random(self, n=1, target=1.0): """ Return a randomly generated valid value. *target* gives some scale independence to the random number generator, allowing the initial value of the parameter to influence the randomly generated value. Otherwise fits without bounds have too large a space to search through. """ def nllf(self, value): """ Return the negative log likelihood of seeing this value, with likelihood scaled so that the maximum probability is one. For uniform bounds, this either returns zero or inf. For bounds based on a probability distribution, this returns values between zero and inf. The scaling is necessary so that indefinite and semi-definite ranges return a sensible value. The scaling does not affect the likelihood maximization process, though the resulting likelihood is not easily interpreted. """ def residual(self, value): """ Return the parameter 'residual' in a way that is consistent with residuals in the normal distribution. The primary purpose is to graphically display exceptional values in a way that is familiar to the user. For fitting, the scaled likelihood should be used. To do this, we will match the cumulative density function value with that for N(0,1) and find the corresponding percent point function from the N(0,1) distribution. In this way, for example, a value to the right of 2.275% of the distribution would correspond to a residual of -2, or 2 standard deviations below the mean. For uniform distributions, with all values equally probable, we use a value of +/-4 for values outside the range, and 0 for values inside the range. """ def start_value(self): """ Return a default starting value if none given. """ return self.put01(0.5) def __contains__(self, v): return self.limits[0] <= v <= self.limits[1] def __str__(self): limits = tuple(num_format(v) for v in self.limits) return "(%s,%s)" % limits def satisfied(self, v) -> bool: lo, hi = self.limits return v >= lo and v <= hi def penalty(self, v) -> float: """ return a (differentiable) nonzero value when outside the bounds """ lo, hi = self.limits dlo = 0.0 if v >= lo else abs(v - lo) dhi = 0.0 if v <= hi else abs(v - hi) return dlo + dhi def to_dict(self): return dict( type=type(self).__name__, limits=self.limits, ) # CRUFT: python 2.5 doesn't format indefinite numbers properly on windows def num_format(v): """ Number formating which supports inf/nan on windows. """ if isfinite(v): return "%g" % v elif isinf(v): return "inf" if v > 0 else "-inf" else: return "NaN" @dataclass(init=False) class Unbounded(Bounds): """ Unbounded parameter. The random initial condition is assumed to be between 0 and 1 The probability is uniformly 1/inf everywhere, which means the negative log likelihood of P is inf everywhere. A value inf will interfere with optimization routines, and so we instead choose P == 1 everywhere. """ type = "Unbounded" def __init__(self, *args, **kw): pass def random(self, n=1, target=1.0): scale = target + (target == 0.0) return RNG.randn(n) * scale def nllf(self, value): return 0 def residual(self, value): return 0 def get01(self, x): return _get01_inf(x) def put01(self, v): return _put01_inf(v) def getfull(self, x): return x def putfull(self, v): return v @dataclass(init=True) class BoundedBelow(Bounds): """ Semidefinite range bounded below. The random initial condition is assumed to be within 1 of the maximum. [base,inf] <-> (-inf,inf) is direct above base+1, -1/(x-base) below [base,inf] <-> [0,1] uses logarithmic compression. Logarithmic compression works by converting sign*m*2^e+base to sign*(e+1023+m), yielding a value in [0,2048]. This can then be converted to a value in [0,1]. Note that the likelihood function is problematic: the true probability of seeing any particular value in the range is infinitesimal, and that is indistinguishable from values outside the range. Instead we say that P = 1 in range, and 0 outside. """ base: float type = "BoundedBelow" @property def limits(self): return (self.base, inf) def start_value(self): return self.base + 1 def random(self, n=1, target: float = 1.0): target = max(abs(target), abs(self.base)) scale = target + float(target == 0.0) return self.base + abs(RNG.randn(n) * scale) def nllf(self, value): return 0 if value >= self.base else inf def residual(self, value): return 0 if value >= self.base else -4 def get01(self, x): m, e = math.frexp(x - self.base) if m >= 0 and e <= _E_MAX: v = (e + m) / (2.0 * _E_MAX) return v else: return 0 if m < 0 else 1 def put01(self, v): v = v * 2 * _E_MAX e = int(v) m = v - e x = math.ldexp(m, e) + self.base return x def getfull(self, x): v = x - self.base return v if v >= 1 else 2 - 1.0 / v def putfull(self, v): x = v if v >= 1 else 1.0 / (2 - v) return x + self.base @dataclass(init=True) class BoundedAbove(Bounds): """ Semidefinite range bounded above. [-inf,base] <-> [0,1] uses logarithmic compression [-inf,base] <-> (-inf,inf) is direct below base-1, 1/(base-x) above Logarithmic compression works by converting sign*m*2^e+base to sign*(e+1023+m), yielding a value in [0,2048]. This can then be converted to a value in [0,1]. Note that the likelihood function is problematic: the true probability of seeing any particular value in the range is infinitesimal, and that is indistinguishable from values outside the range. Instead we say that P = 1 in range, and 0 outside. """ base: float @property def limits(self): return (-inf, self.base) def start_value(self): return self.base - 1 def random(self, n=1, target: float = 1.0): target = max(abs(self.base), abs(target)) scale = target + float(target == 0.0) return self.base - abs(RNG.randn(n) * scale) def nllf(self, value): return 0 if value <= self.base else inf def residual(self, value): return 0 if value <= self.base else 4 def get01(self, x): m, e = math.frexp(self.base - x) if m >= 0 and e <= _E_MAX: v = (e + m) / (2.0 * _E_MAX) return 1 - v else: return 1 if m < 0 else 0 def put01(self, v): v = (1 - v) * 2 * _E_MAX e = int(v) m = v - e x = -(math.ldexp(m, e) - self.base) return x def getfull(self, x): v = x - self.base return v if v <= -1 else -2 - 1.0 / v def putfull(self, v): x = v if v <= -1 else -1.0 / (v + 2) return x + self.base @dataclass(init=True) class Bounded(Bounds): """ Bounded range. [lo,hi] <-> [0,1] scale is simple linear [lo,hi] <-> (-inf,inf) scale uses exponential expansion While technically the probability of seeing any value within the range is 1/range, for consistency with the semi-infinite ranges and for a more natural mapping between nllf and chisq, we instead set the probability to 0. This choice will not affect the fits. """ lo: float = field(metadata={"description": "lower end of bounds"}) hi: float = field(metadata={"description": "upper end of bounds"}) # @classmethod # def from_dict(cls, limits=None): # lo, hi = limits # return cls(lo, hi) # def __init__(self, lo, hi): # self.lo = lo # self.hi = hi # self._nllf_scale = log(hi - lo) @property def limits(self): return (self.lo, self.hi) def random(self, n=1, target=1.0): # print("= uniform",lo,hi) return RNG.uniform(self.lo, self.hi, size=n) def nllf(self, value): return 0 if self.lo <= value <= self.hi else inf # return self._nllf_scale if lo<=value<=hi else inf def residual(self, value): return -4 if self.lo > value else (4 if self.hi < value else 0) def get01(self, x): lo, hi = self.limits return float(x - lo) / (hi - lo) if hi - lo > 0 else 0 def put01(self, v): lo, hi = self.limits return (hi - lo) * v + lo def getfull(self, x): return _put01_inf(self.get01(x)) def putfull(self, v): return self.put01(_get01_inf(v)) class DistProtocol(Protocol): """ Protocol for a distribution object, implementing the scipy.stats interface. (also including args, kwds and name) """ name: str args: Tuple[float, ...] kwds: Dict[str, Any] def rvs(self, n: int) -> float: ... def nnlf(self, value: float) -> float: ... def cdf(self, value: float) -> float: ... def ppf(self, value: float) -> float: ... def pdf(self, value: float) -> float: ... class Distribution(Bounds): """ Parameter is pulled from a distribution. *dist* must implement the distribution interface from scipy.stats, described in the DistProtocol class. """ dist: DistProtocol = None def __init__(self, dist): object.__setattr__(self, "dist", dist) def random(self, n=1, target=1.0): return self.dist.rvs(n) def nllf(self, value): return -log(self.dist.pdf(value)) def residual(self, value): return normal_distribution.ppf(self.dist.cdf(value)) def get01(self, x): return self.dist.cdf(x) def put01(self, v): return self.dist.ppf(v) def getfull(self, x): return x def putfull(self, v): return v def __getstate__(self): # WARNING: does not preserve and restore seed return self.dist.__class__, self.dist.args, self.dist.kwds def __setstate__(self, state): cls, args, kwds = state self.dist = cls(*args, **kwds) def __str__(self): return "%s(%s)" % (self.dist.dist.name, ",".join(str(s) for s in self.dist.args)) def to_dict(self): return dict( type=type(self).__name__, limits=self.limits, # TODO: how to handle arbitrary distribution function in save/load? dist=type(self.dist).__name__, ) @dataclass(frozen=True) class Normal(Distribution): """ Parameter is pulled from a normal distribution. If you have measured a parameter value with some uncertainty (e.g., the film thickness is 35+/-5 according to TEM), then you can use this measurement to restrict the values given to the search, and to penalize choices of this fitting parameter which are different from this value. *mean* is the expected value of the parameter and *std* is the 1-sigma standard deviation. class is 'frozen' because a new object should be created if `mean` or `std` are changed. """ mean: float = 0.0 std: float = 1.0 _nllf_scale: float = field(init=False) def __post_init__(self): Distribution.__init__(self, normal_distribution(self.mean, self.std)) object.__setattr__(self, "_nllf_scale", log(2 * pi * self.std**2) / 2) def nllf(self, value): # P(v) = exp(-0.5*(v-mean)**2/std**2)/sqrt(2*pi*std**2) # -log(P(v)) = -(-0.5*(v-mean)**2/std**2 - log( (2*pi*std**2) ** 0.5)) # = 0.5*(v-mean)**2/std**2 + log(2*pi*std**2)/2 mean, std = self.dist.args return 0.5 * ((value - mean) / std) ** 2 + self._nllf_scale def residual(self, value): mean, std = self.dist.args return (value - mean) / std def __getstate__(self): return self.dist.args # args is mean,std def __setstate__(self, state): mean, std = state self.__init__(mean=mean, std=std) @dataclass(init=False, frozen=True) class BoundedNormal(Bounds): """ truncated normal bounds """ mean: float = 0.0 std: float = 1.0 lo: Union[float, Literal["-inf"]] hi: Union[float, Literal["inf"]] _left: float = field(init=False) _delta: float = field(init=False) _nllf_scale: float = field(init=False) def __init__(self, mean: float = 0, std: float = 1, limits=(-inf, inf), hi="inf", lo="-inf"): if limits is not None: # for backward compatibility: lo, hi = limits limits = (-inf if lo is None else float(lo), inf if hi is None else float(hi)) object.__setattr__(self, "lo", limits[0]) object.__setattr__(self, "hi", limits[1]) object.__setattr__(self, "mean", mean) object.__setattr__(self, "std", std) object.__setattr__(self, "_left", normal_distribution.cdf((limits[0] - mean) / std)) object.__setattr__(self, "_delta", normal_distribution.cdf((limits[1] - mean) / std) - self._left) object.__setattr__(self, "_nllf_scale", log(2 * pi * std**2) / 2 + log(self._delta)) @property def limits(self): return (self.lo, self.hi) def get01(self, x): """ Convert value into [0,1] for optimizers which are bounds constrained. This can also be used as a scale bar to show approximately how close to the end of the range the value is. """ v = (normal_distribution.cdf((x - self.mean) / self.std) - self._left) / self._delta return clip(v, 0, 1) def put01(self, v): """ Convert [0,1] into value for optimizers which are bounds constrained. """ x = v * self._delta + self._left return normal_distribution.ppf(x) * self.std + self.mean def getfull(self, x): """ Convert value into (-inf,inf) for optimizers which are unconstrained. """ raise NotImplementedError def putfull(self, v): """ Convert (-inf,inf) into value for optimizers which are unconstrained. """ raise NotImplementedError def random(self, n=1, target=1.0): """ Return a randomly generated valid value, or an array of values """ return self.get01(RNG.rand(n)) def nllf(self, value): """ Return the negative log likelihood of seeing this value, with likelihood scaled so that the maximum probability is one. """ if value in self: return 0.5 * ((value - self.mean) / self.std) ** 2 + self._nllf_scale else: return inf def residual(self, value): """ Return the parameter 'residual' in a way that is consistent with residuals in the normal distribution. The primary purpose is to graphically display exceptional values in a way that is familiar to the user. For fitting, the scaled likelihood should be used. For the truncated normal distribution, we can just use the normal residuals. """ return (value - self.mean) / self.std def start_value(self): """ Return a default starting value if none given. """ return self.put01(0.5) def __contains__(self, v): return self.limits[0] <= v <= self.limits[1] def __str__(self): vals = ( self.limits[0], self.limits[1], self.mean, self.std, ) return "(%s,%s), norm(%s,%s)" % tuple(num_format(v) for v in vals) @dataclass(init=False, frozen=True) class SoftBounded(Bounds): """ Parameter is pulled from a stretched normal distribution. This is like a rectangular distribution, but with gaussian tails. The intent of this distribution is for soft constraints on the values. As such, the random generator will return values like the rectangular distribution, but the likelihood will return finite values based on the distance from the from the bounds rather than returning infinity. Note that for bounds constrained optimizers which force the value into the range [0,1] for each parameter we don't need to use soft constraints, and this acts just like the rectangular distribution. """ lo: float = 0.0 hi: float = 1.0 std: float = 1.0 def __init__(self, lo, hi, std=1.0): self.lo, self.hi, self.std = lo, hi, std self._nllf_scale = log(hi - lo + sqrt(2 * pi * std)) @property def limits(self): return (self.lo, self.hi) def random(self, n=1, target=1.0): return RNG.uniform(self.lo, self.hi, size=n) def nllf(self, value): # To turn f(x) = 1 if x in [lo,hi] else G(tail) # into a probability p, we need to normalize by \int{f(x)dx}, # which is just hi-lo + sqrt(2*pi*std**2). if value < self.lo: z = self.lo - value elif value > self.hi: z = value - self.hi else: z = 0 return (z / self.std) ** 2 / 2 + self._nllf_scale def residual(self, value): if value < self.lo: z = self.lo - value elif value > self.hi: z = value - self.hi else: z = 0 return z / self.std def get01(self, x): v = float(x - self.lo) / (self.hi - self.lo) return v if 0 <= v <= 1 else (0 if v < 0 else 1) def put01(self, v): return v * (self.hi - self.lo) + self.lo def getfull(self, x): return x def putfull(self, v): return v def __str__(self): return "box_norm(%g,%g,sigma=%g)" % (self.lo, self.hi, self.std) _E_MIN = -1023 _E_MAX = 1024 def _get01_inf(x): """ Convert a floating point number to a value in [0,1]. The value sign*m*2^e to sign*(e+1023+m), yielding a value in [-2048,2048]. This can then be converted to a value in [0,1]. Sort order is preserved. At least 14 bits of precision are lost from the 53 bit mantissa. """ # Arctan alternative # Arctan is approximately linear in (-0.5, 0.5), but the # transform is only useful up to (-10**15,10**15). # return atan(x)/pi + 0.5 m, e = math.frexp(x) s = math.copysign(1.0, m) v = (e - _E_MIN + m * s) * s v = v / (4 * _E_MAX) + 0.5 v = 0 if _E_MIN > e else (1 if _E_MAX < e else v) return v def _put01_inf(v): """ Convert a value in [0,1] to a full floating point number. Sort order is preserved. Reverses :func:`_get01_inf`, but with fewer bits of precision. """ # Arctan alternative # return tan(pi*(v-0.5)) v = (v - 0.5) * 4 * _E_MAX s = math.copysign(1.0, v) v *= s e = int(v) m = v - e x = math.ldexp(s * m, e + _E_MIN) # print "< x,e,m,s,v",x,e+_e_min,s*m,s,v return x BoundsType = Union[Unbounded, Bounded, BoundedAbove, BoundedBelow, BoundedNormal, SoftBounded, Normal] def test_normal(): """ Test the normal distribution """ epsilon = 1e-10 n = Normal(mean=0.5, std=1.0) assert abs(n.nllf(0.5) - 0.9189385332046727) < epsilon assert abs(n.nllf(1.0) - n.nllf(0.0)) < epsilon assert abs(n.residual(0.5) - 0.0) < epsilon assert abs(n.residual(1.0) - 0.5) < epsilon bumps-bumps-46c582c/bumps/bspline.py000066400000000000000000000313411477402201000174570ustar00rootroot00000000000000# This program is public domain """ BSpline calculator. Given a set of knots, compute the cubic B-spline interpolation. """ __all__ = ["bspline", "pbs"] import numpy as np from numpy import maximum as max, minimum as min def pbs(x, y, t, clamp=True, parametric=True): """ Evaluate the parametric B-spline px(t),py(t). *x* and *y* are the control points, and *t* are the points in [0,1] at which they are evaluated. The *x* values are sorted so that the spline describes a function. The spline goes through the control points at the ends. If *clamp* is True, the derivative of the spline at both ends is zero. If *clamp* is False, the derivative at the ends is equal to the slope connecting the final pair of control points. If *parametric* is False, then parametric points t' are chosen such that x(t') = *t*. The B-spline knots are chosen to be equally spaced within [0,1]. """ x = list(sorted(x)) knot = np.hstack((0, 0, np.linspace(0, 1, len(y)), 1, 1)) cx = np.hstack((x[0], x[0], x[0], (2 * x[0] + x[1]) / 3, x[1:-1], (2 * x[-1] + x[-2]) / 3, x[-1])) if clamp: cy = np.hstack((y[0], y[0], y[0], y, y[-1])) else: cy = np.hstack((y[0], y[0], y[0], y[0] + (y[1] - y[0]) / 3, y[1:-1], y[-1] + (y[-2] - y[-1]) / 3, y[-1])) if parametric: return _bspline3(knot, cx, t), _bspline3(knot, cy, t) # Find parametric t values corresponding to given z values # First try a few newton steps xt = np.interp(t, x, np.linspace(0, 1, len(x))) with np.errstate(all="ignore"): for _ in range(6): pt, dpt = _bspline3(knot, cx, xt, nderiv=1) xt -= (pt - t) / dpt idx = np.isnan(xt) | (abs(_bspline3(knot, cx, xt) - t) > 1e-9) # Use bisection when newton fails if idx.any(): missing = t[idx] # print missing t_lo, t_hi = 0 * missing, 1 * missing for _ in range(30): # bisection with about 1e-9 tolerance trial = (t_lo + t_hi) / 2 ptrial = _bspline3(knot, cx, trial) tidx = ptrial < missing t_lo[tidx] = trial[tidx] t_hi[~tidx] = trial[~tidx] xt[idx] = (t_lo + t_hi) / 2 # print "err",np.max(abs(_bspline3(knot,cx,t)-xt)) # Return y evaluated at the interpolation points return _bspline3(knot, cx, xt), _bspline3(knot, cy, xt) def bspline(y, xt, clamp=True): """ Evaluate the B-spline with control points *y* at positions *xt* in [0,1]. The spline goes through the control points at the ends. If *clamp* is True, the derivative of the spline at both ends is zero. If *clamp* is False, the derivative at the ends is equal to the slope connecting the final pair of control points. B-spline knots are chosen to be equally spaced within [0,1]. """ knot = np.hstack((0, 0, np.linspace(0, 1, len(y)), 1, 1)) if clamp: cy = np.hstack(([y[0]] * 3, y, y[-1])) else: cy = np.hstack((y[0], y[0], y[0], y[0] + (y[1] - y[0]) / 3, y[1:-1], y[-1] + (y[-2] - y[-1]) / 3, y[-1])) return _bspline3(knot, cy, xt) def _bspline3(knot, control, t, nderiv=0): """ Evaluate the B-spline specified by the given *knot* sequence and *control* values at the parametric points *t*. *nderiv* selects the function or derivative to evaluate. """ knot, control, t = [np.asarray(v) for v in (knot, control, t)] # Deal with values outside the range valid = (t > knot[0]) & (t <= knot[-1]) tv = t[valid] f = np.zeros(t.shape) f[t <= knot[0]] = control[0] f[t >= knot[-1]] = control[-1] # Find B-Spline parameters for the individual segments end = len(knot) - 1 segment = knot.searchsorted(tv) - 1 tm2 = knot[max(segment - 2, 0)] tm1 = knot[max(segment - 1, 0)] tm0 = knot[max(segment - 0, 0)] tp1 = knot[min(segment + 1, end)] tp2 = knot[min(segment + 2, end)] tp3 = knot[min(segment + 3, end)] p4 = control[min(segment + 3, end)] p3 = control[min(segment + 2, end)] p2 = control[min(segment + 1, end)] p1 = control[min(segment + 0, end)] # Compute second and third derivatives. if nderiv > 1: # Normally we require a recursion for Q, R and S to compute # df, d2f and d3f respectively, however Q can be computed directly # from intermediate values of P, S has a recursion of depth 0, # which leaves only the R recursion of depth 1 in the calculation # below. q4 = (p4 - p3) * 3 / (tp3 - tm0) q3 = (p3 - p2) * 3 / (tp2 - tm1) q2 = (p2 - p1) * 3 / (tp1 - tm2) r4 = (q4 - q3) * 2 / (tp2 - tm0) r3 = (q3 - q2) * 2 / (tp1 - tm1) if nderiv > 2: s4 = (r4 - r3) / (tp1 - tm0) d3f = np.zeros(t.shape) d3f[valid] = s4 r4 = ((tv - tm0) * r4 + (tp1 - tv) * r3) / (tp1 - tm0) d2f = np.zeros(t.shape) d2f[valid] = r4 # Compute function value and first derivative p4 = ((tv - tm0) * p4 + (tp3 - tv) * p3) / (tp3 - tm0) p3 = ((tv - tm1) * p3 + (tp2 - tv) * p2) / (tp2 - tm1) p2 = ((tv - tm2) * p2 + (tp1 - tv) * p1) / (tp1 - tm2) p4 = ((tv - tm0) * p4 + (tp2 - tv) * p3) / (tp2 - tm0) p3 = ((tv - tm1) * p3 + (tp1 - tv) * p2) / (tp1 - tm1) if nderiv >= 1: df = np.zeros(t.shape) df[valid] = (p4 - p3) * 3 / (tp1 - tm0) p4 = ((tv - tm0) * p4 + (tp1 - tv) * p3) / (tp1 - tm0) f[valid] = p4 if nderiv == 0: return f elif nderiv == 1: return f, df elif nderiv == 2: return f, df, d2f else: return f, df, d2f, d3f """ def bspline_control(y, clamp=True): return _find_control(y, clamp=clamp) def pbs_control(x, y, clamp=True): return _find_control(x, clamp=clamp), _find_control(y, clamp=clamp) def _find_control(v, clamp=True): raise NotImplementedError("B-spline interpolation doesn't work yet") from scipy.linalg import solve_banded n = len(v) udiag = np.hstack([0, 0, 0, [1 / 6] * (n - 3), 0.25, 0.3]) ldiag = np.hstack([-0.3, 0.25, [1 / 6] * (n - 3), 0, 0, 0]) mdiag = np.hstack([1, 0.3, 7 / 12, [2 / 3] * (n - 4), 7 / 12, -0.3, 1]) A = np.vstack([ldiag, mdiag, udiag]) if clamp: # First derivative is zero at ends bl, br = 0, 0 else: # First derivative at ends follows line between final control points bl, br = (v[1] - v[0]) * n, (v[-1] - v[-2]) * n b = np.hstack([v[0], bl, v[1:n - 1], br, v[-1]]) x = solve_banded((1, 1), A, b) return x # x[1:-1] """ # =========================================================================== # test code def speed_check(): """ Print the time to evaluate 400 points on a 7 knot spline. """ import time x = np.linspace(0, 1, 7) x[1], x[-2] = x[2], x[-3] y = [9, 11, 2, 3, 8, 0, 2] t = np.linspace(0, 1, 400) t0 = time.time() for _ in range(1000): bspline(y, t, clamp=True) print("bspline (ms)", (time.time() - t0) / 1000) def _check(expected, got, tol): """ Check that value matches expected within tolerance. If *expected* is never zero, use relative error for tolerance. """ relative = (np.isscalar(expected) and expected != 0) or (not np.isscalar(expected) and all(expected != 0)) if relative: norm = np.linalg.norm((expected - got) / expected) else: norm = np.linalg.norm(expected - got) if norm >= tol: msg = [ "expected %s" % str(expected), "got %s" % str(got), "tol %s norm %s" % (tol, norm), ] raise ValueError("\n".join(msg)) def _derivs(x, y): """ Compute numerical derivative for a function evaluated on a fine grid. """ # difference formula return (y[1] - y[0]) / (x[1] - x[0]), (y[-1] - y[-2]) / (x[-1] - x[-2]) # 5-point difference formula # left = (y[0]-8*y[1]+8*y[3]-y[4]) / 12 / (x[1]-x[0]) # right = (y[-5]-8*y[-4]+8*y[-2]-y[-1]) / 12 / (x[-1]-x[-2]) # return left, right def test(): """bspline tests""" h = 1e-10 t = np.linspace(0, 1, 100) dt = np.array([0, h, 2 * h, 3 * h, 4 * h, 1 - 4 * h, 1 - 3 * h, 1 - 2 * h, 1 - h, 1]) y = [9, 11, 2, 3, 8, 0, 2] n = len(y) xeq = np.linspace(0, 1, n) x = xeq + 0 x[0], x[-1] = (x[0] + x[1]) / 2, (x[-2] + x[-1]) / 2 dx = np.array( [ x[0], x[0] + h, x[0] + 2 * h, x[0] + 3 * h, x[0] + 4 * h, x[-1] - 4 * h, x[-1] - 3 * h, x[-1] - 2 * h, x[-1] - h, x[-1], ] ) # ==== Check that bspline mat:w # ches pbs with equally spaced x yt = bspline(y, t, clamp=True) xtp, ytp = pbs(xeq, y, t, clamp=True, parametric=False) _check(t, xtp, 1e-8) _check(yt, ytp, 1e-8) xtp, ytp = pbs(xeq, y, t, clamp=True, parametric=True) _check(t, xtp, 1e-8) _check(yt, ytp, 1e-8) yt = bspline(y, t, clamp=False) xtp, ytp = pbs(xeq, y, t, clamp=False, parametric=False) _check(t, xtp, 1e-8) _check(yt, ytp, 1e-8) xtp, ytp = pbs(xeq, y, t, clamp=False, parametric=True) _check(t, xtp, 1e-8) _check(yt, ytp, 1e-8) # ==== Check bspline f at end points yt = bspline(y, t, clamp=True) _check(y[0], yt[0], 1e-12) _check(y[-1], yt[-1], 1e-12) yt = bspline(y, t, clamp=False) _check(y[0], yt[0], 1e-12) _check(y[-1], yt[-1], 1e-12) xt, yt = pbs(x, y, t, clamp=True, parametric=False) _check(x[0], xt[0], 1e-8) _check(x[-1], xt[-1], 1e-8) _check(y[0], yt[0], 1e-8) _check(y[-1], yt[-1], 1e-8) xt, yt = pbs(x, y, t, clamp=True, parametric=True) _check(x[0], xt[0], 1e-8) _check(x[-1], xt[-1], 1e-8) _check(y[0], yt[0], 1e-8) _check(y[-1], yt[-1], 1e-8) xt, yt = pbs(x, y, t, clamp=False, parametric=False) _check(x[0], xt[0], 1e-8) _check(x[-1], xt[-1], 1e-8) _check(y[0], yt[0], 1e-8) _check(y[-1], yt[-1], 1e-8) xt, yt = pbs(x, y, t, clamp=False, parametric=True) _check(x[0], xt[0], 1e-8) _check(x[-1], xt[-1], 1e-8) _check(y[0], yt[0], 1e-8) _check(y[-1], yt[-1], 1e-8) # ==== Check f' at end points yt = bspline(y, dt, clamp=True) left, right = _derivs(dt, yt) _check(0, left, 1e-8) _check(0, right, 1e-8) xt, yt = pbs(x, y, dx, clamp=True, parametric=False) left, right = _derivs(xt, yt) _check(0, left, 1e-8) _check(0, right, 1e-8) xt, yt = pbs(x, y, dt, clamp=True, parametric=True) left, right = _derivs(xt, yt) _check(0, left, 1e-8) _check(0, right, 1e-8) yt = bspline(y, dt, clamp=False) left, right = _derivs(dt, yt) _check((y[1] - y[0]) * (n - 1), left, 5e-4) _check((y[-1] - y[-2]) * (n - 1), right, 5e-4) xt, yt = pbs(x, y, dx, clamp=False, parametric=False) left, right = _derivs(xt, yt) _check((y[1] - y[0]) / (x[1] - x[0]), left, 5e-4) _check((y[-1] - y[-2]) / (x[-1] - x[-2]), right, 5e-4) xt, yt = pbs(x, y, dt, clamp=False, parametric=True) left, right = _derivs(xt, yt) _check((y[1] - y[0]) / (x[1] - x[0]), left, 5e-4) _check((y[-1] - y[-2]) / (x[-1] - x[-2]), right, 5e-4) # ==== Check interpolator # yc = bspline_control(y) # print("y",y) # print("p(yc)",bspline(yc,xeq)) def demo(): """Show bsline curve for a set of control points.""" from pylab import linspace, subplot, plot, legend, show # y = [9 ,6, 1, 3, 8, 4, 2] # y = [9, 11, 13, 3, -2, 0, 2] y = [9, 11, 2, 3, 8, 0] # y = [9, 9, 1, 3, 8, 2, 2] x = linspace(0, 1, len(y)) t = linspace(x[0], x[-1], 400) subplot(211) plot(t, bspline(y, t, clamp=False), "-.y", label="unclamped bspline") # bspline # bspline plot(t, bspline(y, t, clamp=True), "-y", label="clamped bspline") plot(sorted(x), y, ":oy", label="control points") legend() # left, right = _derivs(t, bspline(y, t, clamp=False)) # print(left, (y[1] - y[0]) / (x[1] - x[0])) subplot(212) xt, yt = pbs(x, y, t, clamp=False) plot(xt, yt, "-.b", label="unclamped pbs") # pbs xt, yt = pbs(x, y, t, clamp=True) plot(xt, yt, "-b", label="clamped pbs") # pbs # xt,yt = pbs(x,y,t,clamp=True, parametric=True) # plot(xt,yt,'-g') # pbs plot(sorted(x), y, ":ob", label="control points") legend() show() # B-Spline control point inverse function is not yet implemented """ def demo_interp(): from pylab import linspace, plot, show x = linspace(0, 1, 7) y = [9, 11, 2, 3, 8, 0, 2] t = linspace(0, 1, 400) yc = bspline_control(y, clamp=True) xc = linspace(x[0], x[-1], 9) plot(xc, yc, ':oy', x, y, 'xg') #knot = np.hstack((0, np.linspace(0,1,len(y)), 1)) #fy = _bspline3(knot,yc,t) fy = bspline(yc, t, clamp=True) plot(t, fy, '-.y') show() """ if __name__ == "__main__": # test() demo() # demo_interp() # speed_check() bumps-bumps-46c582c/bumps/cheby.py000066400000000000000000000146301477402201000171170ustar00rootroot00000000000000r""" Freeform modeling with Chebyshev polynomials. `Chebyshev polynomials `_ $T_k$ form a basis set for functions over $[-1,1]$. The truncated interpolating polynomial $P_n$ is a weighted sum of Chebyshev polynomials up to degree $n$: .. math:: f(x) \approx P_n(x) = \sum_{k=0}^n c_i T_k(x) The interpolating polynomial exactly matches $f(x)$ at the chebyshev nodes $z_k$ and is near the optimal polynomial approximation to $f$ of degree $n$ under the maximum norm. For well behaved functions, the coefficients $c_k$ decrease rapidly, and furthermore are independent of the degree $n$ of the polynomial. The models can either be defined directly in terms of the Chebyshev coefficients $c_k$ with *method* = 'direct', or in terms of control points $(z_k, f(z_k))$ at the Chebyshev nodes :func:`cheby_points` with *method* = 'interp'. Bounds on the parameters are easier to control using 'interp', but the function may oscillate wildly outside the bounds. Bounds on the oscillation are easier to control using 'direct', but the shape of the profile is difficult to control. """ # TODO: clipping volume fraction to [0,1] distorts parameter space # Option 0: clip to [0,1] # - Bayesian analysis: parameter values outside the domain will be equally # probable out to infinity # - Newton methods: the fit space is flat outside the domain, which leads # to a degenerate hessian. # - Direct methods: won't fail, but will be subject to random walk # performance outside the domain. # - trivial to implement! # Option 1: compress (-inf,0.001] and [0.999,inf) into (0,0.001], [0.999,1) # - won't address any of the problems of clipping # Option 2: have chisq return inf for points outside the domain # - Bayesian analysis: correctly assigns probability zero # - Newton methods: degenerate Hessian outside domain # - Direct methods: random walk outside domain # - easy to implement # Option 3: clip outside domain but add penalty based on amount of clipping # A profile based on clipping may have lower chisq than any profile that # can be described by a valid model (e.g., by having a sharper transition # than would be allowed by the model), leading to a minimum outside D. # Adding a penalty constant outside D would help, but there is no constant # that works everywhere. We could use a constant greater than the worst # chisq seen so far in D, which can guarantee an arbitrarily low P(x) and # a global minimum within D, but for Newton methods, the boundary may still # have spurious local minima and objective value now depends on history. # Linear compression of profile to fit within the domain would avoid # unreachable profile shapes (this is just a linear transform on chebyshev # coefficients), and the addition of the penalty value would reduce # parameter correlations that result from having transformed parameters # resulting in identical profiles. Returning T = ||A(x)|| from render, # with A being a transform that brings the profile within [0,1], the # objective function can return P'(x) = P(x)/(10*(1+sum(T_i)^4) for all # slabs i, or P(x) if no slabs return a penalty value. So long as T is # monotonic with increasing badness, with value of 0 within D, and so long # as no values of x outside D can generate models that cannot be # expressed for any x within D, then any optimizer should return a valid # result at the global minimum. There may still be local minima outside # the boundary, so information that the the value is outside the domain # still needs to pass through a local optimizer to the fitting program. # This approach could be used to transform a box constrained # problem to an unconstrained problem using clipping+penalty on the # parameter values and removing the need for constrained Newton optimizers. # - Bayesian analysis: parameters outside D have incorrect probability, but # with a sufficiently large penalty, P(x) ~ 0; if the penalty value is # too low, details of the correlations outside D may leak into D. # - Newton methods: Hessian should point back to domain # - Direct methods: random walk should be biased toward the domain # - moderately complicated __all__ = ["profile", "cheby_approx", "cheby_val", "cheby_points", "cheby_coeff"] import numpy as np from numpy import real, exp, pi, cos, arange, asarray from numpy.fft import fft def profile(c, t, method): r""" Evaluate the chebyshev approximation c at points x. If method is 'direct' then $c_i$ are the coefficients for the chebyshev polynomials $T_i$ yielding $P = \sum_i{c_i T_i(x)}$. If method is 'interp' then $c_i$ are the values of the interpolated function $f$ evaluated at the chebyshev points returned by :func:`cheby_points`. """ if method == "interp": c = cheby_coeff(c) return cheby_val(c, t) def cheby_approx(n, f, range=(0, 1)): """ Return the coefficients for the order n chebyshev approximation to function f evaluated over the range [low,high]. """ fx = f(cheby_points(n, range=range)) return cheby_coeff(fx) def cheby_val(c, x): r""" Evaluate the chebyshev approximation c at points x. The values $c_i$ are the coefficients for the chebyshev polynomials $T_i$ yielding $p(x) = \sum_i{c_i T_i(x)}$. """ c = np.asarray(c) if len(c) == 0: return 0 * x # Crenshaw recursion from numerical recipes sec. 5.8 y = 4 * x - 2 d = dd = 0 for c_j in c[:0:-1]: d, dd = y * d + (c_j - dd), d return y * (0.5 * d) + (0.5 * c[0] - dd) def cheby_points(n, range=(0, 1)): r""" Return the points in at which a function must be evaluated to generate the order $n$ Chebyshev approximation function. Over the range [-1,1], the points are $p_k = \cos(\pi(2 k + 1)/(2n))$. Adjusting the range to $[x_L,x_R]$, the points become $x_k = \frac{1}{2} (p_k - x_L + 1)/(x_R-x_L)$. """ return 0.5 * (cos(pi * (arange(n) + 0.5) / n) - range[0] + 1) / (range[1] - range[0]) def cheby_coeff(fx): """ Compute chebyshev coefficients for a polynomial of order n given the function evaluated at the chebyshev points for order n. This can be used as the basis of a direct interpolation method where the n control points are positioned at cheby_points(n). """ fx = asarray(fx) n = len(fx) w = exp((-0.5j * pi / n) * arange(n)) y = np.hstack((fx[0::2], fx[1::2][::-1])) c = (2.0 / n) * real(fft(y) * w) return c bumps-bumps-46c582c/bumps/cli.py000066400000000000000000000611051477402201000165730ustar00rootroot00000000000000""" Bumps command line interface. The functions in this module are used by the bumps command to implement the command line interface. Bumps plugin models can use them to create stand alone applications with a similar interface. For example, the Refl1D application uses the following:: from . import fitplugin import bumps.cli bumps.cli.set_mplconfig(appdatadir='Refl1D') bumps.cli.install_plugin(fitplugin) bumps.cli.main() After completing a set of fits on related systems, a post-analysis script can use :func:`load_model` to load the problem definition and :func:`load_best` to load the best value found in the fit. This can be used for example in experiment design, where you look at the expected parameter uncertainty when fitting simulated data from a range of experimental systems. """ __all__ = [ "main", "install_plugin", "set_mplconfig", "config_matplotlib", "load_model", "preview", "load_best", "save_best", "resynth", ] import sys import os import re import warnings import traceback import shutil import numpy as np # np.seterr(all="raise") from .fitters import FitDriver, StepMonitor, ConsoleMonitor, CheckpointMonitor, nllf_scale from .mapper import MPMapper, MPIMapper, SerialMapper from . import util from . import initpop from . import __version__ from . import plugin from .util import pushdir def install_plugin(p): """ Replace symbols in :mod:`bumps.plugin` with application specific methods. """ for symbol in plugin.__all__: if hasattr(p, symbol): setattr(plugin, symbol, getattr(p, symbol)) def load_model(path, model_options=None): """ Load a model file. *path* contains the path to the model file. *model_options* are any additional arguments to the model. The sys.argv variable will be set such that *sys.argv[1:] == model_options*. """ from .fitproblem import load_problem # Change to the target path before loading model so that data files # can be given as relative paths in the model file. Add the directory # to the python path (at the end) so that imports work as expected. directory, filename = os.path.split(path) with pushdir(directory): # Try a specialized model loader problem = plugin.load_model(filename) if problem is None: # print "loading",filename,"from",directory # TODO: eliminate pickle!! if filename.endswith("pickle"): try: import dill as pickle except ImportError: import pickle # First see if it is a pickle with open(filename, "rb") as fd: problem = pickle.load(fd) else: # Then see if it is a python model script problem = load_problem(filename, options=model_options) # Guard against the user changing parameters after defining the problem. problem.model_reset() problem.path = os.path.abspath(path) if not hasattr(problem, "title"): problem.title = filename problem.name, _ = os.path.splitext(filename) problem.options = model_options return problem def preview(problem, view=None): """ Show the problem plots and parameters. """ import pylab problem.show() problem.plot(view=view) pylab.show() def save_best(fitdriver, problem, best, view=None): """ Save the fit data, including parameter values, uncertainties and plots. *fitdriver* is the fitter that was used to drive the fit. *problem* is a FitProblem instance. *best* is the parameter set to save. """ # Make sure the problem contains the best value # TODO: avoid recalculating if problem is already at best. problem.setp(best) # print "remembering best" pardata = "".join("%s %.15g\n" % (name, value) for name, value in zip(problem.labels(), problem.getp())) open(problem.output_path + ".par", "wt").write(pardata) fitdriver.save(problem.output_path) with util.redirect_console(problem.output_path + ".err"): fitdriver.show() fitdriver.plot(output_path=problem.output_path, view=view) fitdriver.show() # print "plotting" PARS_PATTERN = re.compile(r"^(?P