diff --git a/.codecov.yml b/.codecov.yml index 94bab7aef..100244eab 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -4,8 +4,11 @@ coverage: default: target: 80% patch: off + ignore: - - "tests/.*" - - "examples/.*" + - "**/tests/**/*" + - "**/tests/*" + - "**/examples/**/*" + - "**/examples/*" comment: false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..78c668cdb --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto +featomic/include/featomic.h eol=lf diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index 1165945fd..7df86875b 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -5,12 +5,25 @@ on: branches: [main] tags: ["*"] pull_request: - # Check all PR + paths: + # build wheels in PR if this file changed + - '.github/workflows/build-wheels.yml' + # build wheels in PR if any of the build system files changed + - '**/VERSION' + - '**/setup.py' + - '**/pyproject.toml' + - '**/MANIFEST.in' + - '**/Cargo.toml' + - '**/CMakeLists.txt' + - '**/build.rs' concurrency: group: wheels-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} +env: + FEATOMIC_NO_LOCAL_DEPS: "1" + jobs: build-wheels: runs-on: ${{ matrix.os }} @@ -21,20 +34,23 @@ jobs: - name: x86_64 Linux os: ubuntu-22.04 rust-target: x86_64-unknown-linux-gnu - cibw_arch: x86_64 + cibw-arch: x86_64 + - name: arm64 Linux + os: ubuntu-22.04 + rust-target: aarch64-unknown-linux-gnu + cibw-arch: aarch64 - name: x86_64 macOS os: macos-13 rust-target: x86_64-apple-darwin - cibw_arch: x86_64 + cibw-arch: x86_64 - name: M1 macOS os: macos-14 rust-target: aarch64-apple-darwin - cibw_arch: arm64 + cibw-arch: arm64 - name: x86_64 Windows - os: windows-2019 - # TODO: add a 32-bit windows builder? + os: windows-2022 rust-target: x86_64-pc-windows-msvc - cibw_arch: AMD64 + cibw-arch: AMD64 steps: - uses: actions/checkout@v4 with: @@ -52,34 +68,227 @@ jobs: python-version: "3.12" - name: install dependencies - run: python -m pip install cibuildwheel + run: python -m pip install cibuildwheel twine + + - name: Set up QEMU for docker + if: matrix.os == 'ubuntu-22.04' + uses: docker/setup-qemu-action@v3 - name: build manylinux with rust docker image if: matrix.os == 'ubuntu-22.04' - run: docker build -t rustc-manylinux2014_x86_64 python/scripts/rustc-manylinux2014_x86_64 + run: docker build -t rustc-manylinux2014_${{ matrix.cibw-arch }} python/scripts/rustc-manylinux2014_${{ matrix.cibw-arch }} - - name: build rascaline wheel - run: python -m cibuildwheel . + - name: build featomic wheel + run: python -m cibuildwheel python/featomic env: - CIBW_BUILD: cp310-* + CIBW_BUILD: cp312-* CIBW_SKIP: "*musllinux*" - CIBW_ARCHS: ${{ matrix.cibw_arch }} - CIBW_BUILD_VERBOSITY: 2 + CIBW_ARCHS: ${{ matrix.cibw-arch }} + CIBW_BUILD_VERBOSITY: 1 CIBW_MANYLINUX_X86_64_IMAGE: rustc-manylinux2014_x86_64 + CIBW_MANYLINUX_AARCH64_IMAGE: rustc-manylinux2014_aarch64 CIBW_REPAIR_WHEEL_COMMAND_MACOS: "delocate-wheel --ignore-missing-dependencies --require-archs {delocate_archs} -w {dest_dir} -v {wheel}" CIBW_REPAIR_WHEEL_COMMAND_LINUX: "auditwheel repair --exclude libmetatensor.so -w {dest_dir} {wheel}" + CIBW_ENVIRONMENT: > + MACOSX_DEPLOYMENT_TARGET=11 + FEATOMIC_NO_LOCAL_DEPS=1 - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: - name: wheels + name: wheel-${{ matrix.os }}-${{ matrix.cibw-arch }} path: ./wheelhouse/*.whl - - name: upload wheel to GitHub release - if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@v1 + + build-torch-wheels: + runs-on: ${{ matrix.os }} + name: ${{ matrix.name }} (torch v${{ matrix.torch-version }}) + strategy: + matrix: + torch-version: ['2.1', '2.2', '2.3', '2.4', '2.5'] + arch: ['arm64', 'x86_64'] + os: ['ubuntu-22.04', 'macos-13', 'macos-14', 'windows-2022'] + exclude: + # remove mismatched arch for macOS + - {os: macos-14, arch: x86_64} + - {os: macos-13, arch: arm64} + # no arm64-windows build + - {os: windows-2022, arch: arm64} + # arch x86_64 on macos is only supported for torch <2.3 + - {os: macos-13, arch: x86_64, torch-version: '2.3'} + - {os: macos-13, arch: x86_64, torch-version: '2.4'} + - {os: macos-13, arch: x86_64, torch-version: '2.5'} + include: + # add `cibw-arch` and `rust-target` to the different configurations + - name: x86_64 Linux + os: ubuntu-22.04 + arch: x86_64 + rust-target: x86_64-unknown-linux-gnu + cibw-arch: x86_64 + - name: arm64 Linux + os: ubuntu-22.04 + arch: arm64 + rust-target: aarch64-unknown-linux-gnu + cibw-arch: aarch64 + - name: x86_64 macOS + os: macos-13 + arch: x86_64 + rust-target: x86_64-apple-darwin + cibw-arch: x86_64 + - name: arm64 macOS + os: macos-14 + arch: arm64 + rust-target: aarch64-apple-darwin + cibw-arch: arm64 + - name: x86_64 Windows + os: windows-2022 + arch: x86_64 + rust-target: x86_64-pc-windows-msvc + cibw-arch: AMD64 + # add the right python version for each torch version + - {torch-version: '2.1', python-version: '3.11', cibw-python: 'cp311-*'} + - {torch-version: '2.2', python-version: '3.12', cibw-python: 'cp312-*'} + - {torch-version: '2.3', python-version: '3.12', cibw-python: 'cp312-*'} + - {torch-version: '2.4', python-version: '3.12', cibw-python: 'cp312-*'} + - {torch-version: '2.5', python-version: '3.12', cibw-python: 'cp312-*'} + steps: + - uses: actions/checkout@v4 with: - files: ./wheelhouse/*.whl + fetch-depth: 0 + + - name: setup rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + target: ${{ matrix.rust-target }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: install dependencies + run: python -m pip install cibuildwheel + + - name: Set up QEMU for docker + if: matrix.os == 'ubuntu-22.04' + uses: docker/setup-qemu-action@v3 + + - name: build manylinux with rust docker image + if: matrix.os == 'ubuntu-22.04' + run: docker buildx build -t rustc-manylinux2014_${{ matrix.cibw-arch }} python/scripts/rustc-manylinux2014_${{ matrix.cibw-arch }} + + - name: build featomic-torch wheel + run: python -m cibuildwheel python/featomic_torch env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CIBW_BUILD: ${{ matrix.cibw-python}} + CIBW_SKIP: "*musllinux*" + CIBW_ARCHS: ${{ matrix.cibw-arch }} + CIBW_BUILD_VERBOSITY: 1 + CIBW_MANYLINUX_X86_64_IMAGE: rustc-manylinux2014_x86_64 + CIBW_MANYLINUX_AARCH64_IMAGE: rustc-manylinux2014_aarch64 + # FEATOMIC_NO_LOCAL_DEPS is set to 1 when building a tag of + # featomic-torch, which will force to use the version of featomic + # already released on PyPI. Otherwise, this will use the version of + # featomic from git checkout (in case there are unreleased breaking + # changes). This means we can not release breaking changes in featomic + # and v-torch by putting a tag on the same commit. Instead featomic + # must be fully released before we start the build of featomic-torch + # wheels. + CIBW_ENVIRONMENT: > + FEATOMIC_NO_LOCAL_DEPS=${{ startsWith(github.ref, 'refs/tags/featomic-torch-v') && '1' || '0' }} + FEATOMIC_TORCH_BUILD_WITH_TORCH_VERSION=${{ matrix.torch-version }}.* + PIP_EXTRA_INDEX_URL=https://download.pytorch.org/whl/cpu + MACOSX_DEPLOYMENT_TARGET=11 + # do not complain for missing libtorch.so, libfeatomic.so, & co + CIBW_REPAIR_WHEEL_COMMAND_MACOS: | + delocate-wheel --ignore-missing-dependencies \ + --require-archs {delocate_archs} \ + -w {dest_dir} -v {wheel} + CIBW_REPAIR_WHEEL_COMMAND_LINUX: | + auditwheel repair --exclude libfeatomic.so \ + --exclude libmetatensor.so \ + --exclude libmetatensor_torch.so \ + --exclude libtorch.so \ + --exclude libtorch_cpu.so \ + --exclude libc10.so \ + -w {dest_dir} {wheel} + + - uses: actions/upload-artifact@v4 + with: + name: torch-single-version-wheel-${{ matrix.torch-version }}-${{ matrix.os }}-${{ matrix.arch }} + path: ./wheelhouse/*.whl + + merge-torch-wheels: + needs: build-torch-wheels + runs-on: ubuntu-22.04 + name: merge featomic-torch ${{ matrix.name }} + strategy: + matrix: + include: + - name: x86_64 Linux + os: ubuntu-22.04 + arch: x86_64 + - name: arm64 Linux + os: ubuntu-22.04 + arch: arm64 + - name: x86_64 macOS + os: macos-13 + arch: x86_64 + - name: arm64 macOS + os: macos-14 + arch: arm64 + - name: x86_64 Windows + os: windows-2022 + arch: x86_64 + steps: + - uses: actions/checkout@v4 + + - name: Download wheels + uses: actions/download-artifact@v4 + with: + pattern: torch-single-version-wheel-*-${{ matrix.os }}-${{ matrix.arch }} + merge-multiple: false + path: dist + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: install dependencies + run: python -m pip install twine wheel + + - name: merge wheels + run: | + # collect all torch versions used for the build + REQUIRES_TORCH=$(find dist -name "*.whl" -exec unzip -p {} "featomic_torch-*.dist-info/METADATA" \; | grep "Requires-Dist: torch") + MERGED_TORCH_REQUIRE=$(python scripts/create-torch-versions-range.py "$REQUIRES_TORCH") + + echo MERGED_TORCH_REQUIRE=$MERGED_TORCH_REQUIRE + + # unpack all single torch versions wheels in the same directory + mkdir dist/unpacked + find dist -name "*.whl" -print -exec python -m wheel unpack --dest dist/unpacked/ {} ';' + + sed -i "s/Requires-Dist: torch.*/$MERGED_TORCH_REQUIRE/" dist/unpacked/featomic_torch-*/featomic_torch-*.dist-info/METADATA + + echo "\n\n METADATA = \n\n" + cat dist/unpacked/featomic_torch-*/featomic_torch-*.dist-info/METADATA + + # check the right metadata was added to the file. grep will exit with + # code `1` if the line is not found, which will stop CI + grep "$MERGED_TORCH_REQUIRE" dist/unpacked/featomic_torch-*/featomic_torch-*.dist-info/METADATA + + # repack the directory as a new wheel + mkdir wheelhouse + python -m wheel pack --dest wheelhouse/ dist/unpacked/* + + - name: check wheels with twine + run: twine check wheelhouse/* + + - uses: actions/upload-artifact@v4 + with: + name: torch-wheel-${{ matrix.os }}-${{ matrix.arch }} + path: ./wheelhouse/*.whl build-sdist: runs-on: ubuntu-22.04 @@ -88,22 +297,93 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - - name: build sdist + + - name: build featomic sdist run: | pip install build - python -m build --sdist . - - uses: actions/upload-artifact@v3 + python -m build --sdist python/featomic --outdir ./dist/ + + - name: build featomic-torch sdist + run: | + python -m build --sdist python/featomic_torch --outdir ./dist/ + + - name: create C++ tarballs + run: | + ./scripts/package-featomic.sh dist/cxx/ + ./scripts/package-featomic-torch.sh dist/cxx/ + + - uses: actions/upload-artifact@v4 + with: + name: sdist + path: | + dist/*.tar.gz + dist/cxx/*.tar.gz + + merge-and-release: + name: Merge and release wheels/sdists + needs: [build-wheels, merge-torch-wheels, build-sdist] + runs-on: ubuntu-22.04 + permissions: + contents: write + pull-requests: write + steps: + - name: Download featomic wheels + uses: actions/download-artifact@v4 + with: + path: wheels + pattern: wheel-* + merge-multiple: true + + - name: Download featomic-torch wheels + uses: actions/download-artifact@v4 + with: + path: wheels + pattern: torch-wheel-* + merge-multiple: true + + - name: Download sdists + uses: actions/download-artifact@v4 + with: + path: wheels + name: sdist + + - name: Re-upload a single wheels artifact + uses: actions/upload-artifact@v4 with: name: wheels - path: dist/*.tar.gz - - name: upload sdist to GitHub release - if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@v1 + path: | + wheels/* + wheels/cxx/* + + - name: Comment with download link + uses: PicoCentauri/comment-artifact@v1 + with: + name: wheels + description: ⚙️ [Download Python wheels for this pull-request (you can install these with pip) + + - name: upload to GitHub release (featomic) + if: startsWith(github.ref, 'refs/tags/featomic-v') + uses: softprops/action-gh-release@v2 + with: + files: | + wheels/cxx/featomic-cxx-*.tar.gz + wheels/featomic-* + prerelease: ${{ contains(github.ref, '-rc') }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: upload to GitHub release (featomic-torch) + if: startsWith(github.ref, 'refs/tags/featomic-torch-v') + uses: softprops/action-gh-release@v2 with: - files: dist/*.tar.gz + files: | + wheels/cxx/featomic-torch-cxx-*.tar.gz + wheels/featomic_torch-* + prerelease: ${{ contains(github.ref, '-rc') }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/comment-wheels-pr.yml b/.github/workflows/comment-wheels-pr.yml deleted file mode 100644 index 78f240c70..000000000 --- a/.github/workflows/comment-wheels-pr.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Comment on pull request -on: - workflow_run: - workflows: ['Build Python wheels'] - types: [completed] - -jobs: - pr_comment: - if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' - runs-on: ubuntu-latest - steps: - - uses: actions/github-script@v6 - with: - script: | - async function insertUpdateComment(owner, repo, issue_number, purpose, body) { - const {data: comments} = await github.rest.issues.listComments( - {owner, repo, issue_number} - ); - const marker = ``; - body = marker + "\n" + body; - const existing = comments.filter((c) => c.body.includes(marker)); - if (existing.length > 0) { - const last = existing[existing.length - 1]; - core.info(`Updating comment ${last.id}`); - await github.rest.issues.updateComment({ - owner, repo, - body, - comment_id: last.id, - }); - } else { - core.info(`Creating a comment in issue / PR #${issue_number}`); - await github.rest.issues.createComment({issue_number, body, owner, repo}); - } - } - - const {owner, repo} = context.repo; - const run_id = ${{github.event.workflow_run.id}}; - const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }}; - if (!pull_requests.length) { - return core.error("This workflow doesn't match any pull requests!"); - } - - const artifacts = await github.paginate( - github.rest.actions.listWorkflowRunArtifacts, {owner, repo, run_id} - ); - - if (!artifacts.length) { - return core.error(`No artifacts found`); - } - - if (artifacts.length !== 1) { - return core.error(`more than one artifact found`); - } - const link = `https://nightly.link/${owner}/${repo}/actions/artifacts/${artifacts[0].id}.zip` - - let body = `Here is a pre-built version of the code in this pull request: [wheels.zip](${link}), `; - body += 'you can install it locally by unzipping `wheels.zip` and using `pip` to install the file matching your system'; - - core.info("Review thread message body:", body); - for (const pr of pull_requests) { - await insertUpdateComment(owner, repo, pr.number, "link-to-wheels", body); - } diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 541944c9f..7fe703f8d 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -44,9 +44,9 @@ jobs: run: sudo apt install -y lcov - name: Setup sccache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.6 with: - version: "v0.5.4" + version: "v0.8.2" - name: Setup sccache environnement variables run: | @@ -56,13 +56,11 @@ jobs: echo "CMAKE_CXX_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV - name: collect rust and C/C++ coverage - env: - RASCALINE_TEST_WITH_STATIC_LIB: "1" run: | - cargo tarpaulin --all-features --workspace --engine=llvm --out=xml --output-dir=target/tarpaulin --objects target/debug/librascaline.so + cargo tarpaulin --all-features --workspace --engine=llvm --out=xml --output-dir=target/tarpaulin --objects target/debug/libfeatomic.so # cleanup C/C++ coverage lcov --directory . --capture --output-file coverage.info - lcov --remove coverage.info '/usr/*' "$(pwd)/rascaline-c-api/tests/*" "$(pwd)/rascaline-c-api/examples/*" --output-file coverage.info + lcov --remove coverage.info '/usr/*' "$(pwd)/featomic/tests/*" "$(pwd)/featomic/examples/*" --output-file coverage.info - name: collect Python coverage run: | @@ -74,14 +72,11 @@ jobs: - name: combine Python coverage files run: | - coverage combine --append \ - ./.coverage \ - ./python/rascaline-torch/.coverage + coverage combine --append ./python/featomic/.coverage ./python/featomic_torch/.coverage coverage xml - name: upload to codecov.io - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: fail_ci_if_error: true files: target/tarpaulin/cobertura.xml,coverage.xml,coverage.info - token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 92559684f..7e732a7a1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -14,6 +14,9 @@ concurrency: jobs: build-and-publish: runs-on: ubuntu-22.04 + permissions: + contents: write + pull-requests: write steps: - name: free disk space run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android || true @@ -41,6 +44,19 @@ jobs: # Use the CPU only version of torch when building/running the code PIP_EXTRA_INDEX_URL: https://download.pytorch.org/whl/cpu + - name: store documentation as github artifact to be downloaded by users + uses: actions/upload-artifact@v4 + with: + name: docs + path: docs/build/html/* + overwrite: true # only keep the latest version of the documentation + + - name: Comment with download link + uses: PicoCentauri/comment-artifact@v1 + with: + name: docs + description: 📚 Download documentation for this pull-request + - name: put documentation in the website run: | git clone https://github.com/$GITHUB_REPOSITORY --branch gh-pages gh-pages @@ -58,7 +74,7 @@ jobs: - name: deploy to gh-pages if: github.event_name == 'push' - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./gh-pages/ diff --git a/.github/workflows/documentation-links.yml b/.github/workflows/documentation-links.yml deleted file mode 100644 index b6f9b8000..000000000 --- a/.github/workflows/documentation-links.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: readthedocs/actions - -on: - pull_request_target: - types: - - opened - -permissions: - pull-requests: write - -jobs: - documentation-links: - runs-on: ubuntu-latest - steps: - - uses: readthedocs/actions/preview@v1 - with: - project-slug: rascaline diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index f1930b4cb..3ca206b49 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -24,7 +24,7 @@ jobs: python-version: "3.12" - os: macos-14 python-version: "3.12" - - os: windows-2019 + - os: windows-2022 python-version: "3.12" steps: - uses: actions/checkout@v4 @@ -90,7 +90,7 @@ jobs: python -m pip install tox - name: python build tests - run: tox -e build-python + run: tox -e build-tests env: # Use the CPU only version of torch when building/running the code PIP_EXTRA_INDEX_URL: https://download.pytorch.org/whl/cpu diff --git a/.github/workflows/rust-tests.yml b/.github/workflows/rust-tests.yml index e66d02dd5..e825f0c1f 100644 --- a/.github/workflows/rust-tests.yml +++ b/.github/workflows/rust-tests.yml @@ -27,7 +27,7 @@ jobs: build-type: debug test-static-lib: true extra-name: / static C library - working-directory: /home/runner/work/rascaline/rascaline/ + working-directory: /home/runner/work/featomic/featomic/ - os: ubuntu-22.04 rust-version: stable @@ -36,7 +36,7 @@ jobs: cargo-build-flags: --release do-valgrind: true extra-name: / release valgrind - working-directory: /home/runner/work/rascaline/rascaline/ + working-directory: /home/runner/work/featomic/featomic/ # check the build on a stock Ubuntu 20.04, including cmake 3.16 - os: ubuntu-22.04 @@ -45,19 +45,19 @@ jobs: rust-target: x86_64-unknown-linux-gnu build-type: debug extra-name: / cmake 3.16 - working-directory: /__w/rascaline/rascaline + working-directory: /__w/featomic/featomic - os: macos-14 rust-version: stable rust-target: aarch64-apple-darwin build-type: debug - working-directory: /Users/runner/work/rascaline/rascaline/ + working-directory: /Users/runner/work/featomic/featomic/ - - os: windows-2019 + - os: windows-2022 rust-version: stable rust-target: x86_64-pc-windows-msvc build-type: debug - working-directory: C:\\rascaline + working-directory: C:\\featomic steps: - name: install dependencies in container @@ -74,9 +74,15 @@ jobs: run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android || true - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure git safe directory + if: matrix.container == 'ubuntu:20.04' + run: git config --global --add safe.directory /__w/featomic/featomic - name: "copy the code to C: drive" - if: matrix.os == 'windows-2019' + if: matrix.os == 'windows-2022' run: cp -r ${{ github.workspace }} ${{ matrix.working-directory }} working-directory: / @@ -114,14 +120,15 @@ jobs: run: cargo test --lib --target ${{ matrix.rust-target }} ${{ matrix.cargo-build-flags }} - name: documentation tests + # we need rustc 1.78 to load libmetatensor.so in doctests + if: matrix.rust-version == 'stable' run: cargo test --doc --target ${{ matrix.rust-target }} ${{ matrix.cargo-build-flags }} - name: integration tests env: - RASCALINE_TEST_WITH_STATIC_LIB: ${{ matrix.test-static-lib || 0 }} + FEATOMIC_TEST_WITH_STATIC_LIB: ${{ matrix.test-static-lib || 0 }} run: | - cargo test --test "*" --package rascaline --target ${{ matrix.rust-target }} ${{ matrix.cargo-build-flags }} - cargo test --test "*" --package rascaline-c-api --target ${{ matrix.rust-target }} ${{ matrix.cargo-build-flags }} + cargo test --test "*" --package featomic --target ${{ matrix.rust-target }} ${{ matrix.cargo-build-flags }} # second set of jobs checking that (non-test) code still compiles/run as expected prevent-bitrot: @@ -136,9 +143,9 @@ jobs: toolchain: stable - name: Setup sccache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.6 with: - version: "v0.5.4" + version: "v0.8.2" - name: Setup sccache environnement variables run: | @@ -148,7 +155,9 @@ jobs: echo "CMAKE_CXX_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV - name: check that examples compile & run - run: cargo run --release --example compute-soap -- rascaline/examples/data/water.xyz + run: | + cargo run --release --features chemfiles --example compute-soap -- featomic/examples/data/water.xyz + cargo run --release --features chemfiles --example profiling -- featomic/examples/data/water.xyz - name: check that benchmarks compile and run once run: cargo bench -- --test diff --git a/.github/workflows/torch-tests.yml b/.github/workflows/torch-tests.yml index 0c5ee66e8..a06423e1e 100644 --- a/.github/workflows/torch-tests.yml +++ b/.github/workflows/torch-tests.yml @@ -14,6 +14,7 @@ jobs: tests: runs-on: ${{ matrix.os }} name: ${{ matrix.os }} / Torch ${{ matrix.torch-version }} + container: ${{ matrix.container }} strategy: matrix: include: @@ -28,19 +29,40 @@ jobs: cargo-test-flags: --release do-valgrind: true + - os: ubuntu-20.04 + container: ubuntu:20.04 + extra-name: ", cmake 3.16" + torch-version: 2.5.* + python-version: "3.12" + cargo-test-flags: "" + cxx-flags: -fsanitize=undefined -fsanitize=address -fno-omit-frame-pointer -g + - os: macos-14 torch-version: 2.3.* python-version: "3.12" cargo-test-flags: --release - - os: windows-2019 + - os: windows-2022 # Torch 2.3.0 is broken on Windows, and 2.2 has https://github.com/pytorch/pytorch/issues/118862 torch-version: 2.1.* python-version: "3.11" cargo-test-flags: --release steps: + - name: install dependencies in container + if: matrix.container == 'ubuntu:20.04' + run: | + apt update + apt install -y software-properties-common + apt install -y cmake make gcc g++ git curl + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure git safe directory + if: matrix.container == 'ubuntu:20.04' + run: git config --global --add safe.directory /__w/featomic/featomic - name: setup rust uses: dtolnay/rust-toolchain@master @@ -60,9 +82,9 @@ jobs: sudo apt-get install -y valgrind - name: Setup sccache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.6 with: - version: "v0.5.4" + version: "v0.8.2" - name: Setup sccache environnement variables run: | @@ -72,8 +94,9 @@ jobs: echo "CMAKE_CXX_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV - name: run TorchScript C++ tests - run: cargo test --package rascaline-torch ${{ matrix.cargo-test-flags }} + run: cargo test --package featomic-torch ${{ matrix.cargo-test-flags }} env: # Use the CPU only version of torch when building/running the code PIP_EXTRA_INDEX_URL: https://download.pytorch.org/whl/cpu - RASCALINE_TORCH_TEST_VERSION: ${{ matrix.torch-version }} + FEATOMIC_TORCH_TEST_VERSION: ${{ matrix.torch-version }} + CXXFLAGS: ${{ matrix.cxx-flags }} diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index 1c5f0a224..000000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,37 +0,0 @@ -# .readthedocs.yml -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -# Required -version: 2 - -# Set the version of Python and other tools we need -build: - os: ubuntu-22.04 - apt_packages: - - cmake - tools: - python: "3.12" - rust: "1.75" - jobs: - post_install: - # install rascaline-torch with the CPU version of PyTorch. We can not use - # the `python` section below since it does not allow to specify - # `--extra-index-url` - - pip install --extra-index-url https://download.pytorch.org/whl/cpu python/rascaline-torch - pre_build: - # Pre-build Rust code here to avoid timeout when building docs - - cargo build - - cargo build --release - - cargo doc -p rascaline - -# Build documentation in the docs/ directory with Sphinx -sphinx: - configuration: docs/src/conf.py - -# Declare the Python requirements required to build the docs -python: - install: - - method: pip - path: . - - requirements: docs/requirements.txt diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index ccd9975b6..50332de9e 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,13 +1,13 @@ -By contributing to rascaline, you accept and agree to the following terms and -conditions for your present and future contributions submitted to rascaline. -Except for the license granted herein to rascaline and recipients of software -distributed by rascaline, you reserve all right, title, and interest in and to +By contributing to featomic, you accept and agree to the following terms and +conditions for your present and future contributions submitted to featomic. +Except for the license granted herein to featomic and recipients of software +distributed by featomic, you reserve all right, title, and interest in and to your contributions. Code of Conduct --------------- -As contributors and maintainers of rascaline, we pledge to respect all people +As contributors and maintainers of featomic, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting merge requests or patches, and other activities. @@ -44,20 +44,20 @@ available from `Github`_. Before submitting a merge request, please open an issue to discuss your changes. Use the only `main` branch for submitting your requests. -.. _`Github` : https://github.com/Luthaf/rascaline +.. _`Github` : https://github.com/metatensor/featomic Required tools -------------- You will need to install and get familiar with the following tools when working -on rascaline: +on featomic: - **git**: the software we use for version control of the source code. See https://git-scm.com/downloads for installation instructions. - **the rust compiler**: you will need both ``rustc`` (the compiler) and ``cargo`` (associated build tool). You can install both using `rustup`_, or use a version provided by your operating system. We need at least Rust version - 1.74 to build rascaline. + 1.74 to build featomic. - **Python**: you can install ``Python`` and ``pip`` from your operating system. We require a Python version of at least 3.6. - **tox**: a Python test runner, cf https://tox.readthedocs.io/en/latest/. You @@ -76,17 +76,17 @@ not have to interact with them directly: Getting the code ---------------- -The first step when developing rascaline is to `create a fork`_ of the main +The first step when developing featomic is to `create a fork`_ of the main repository on github, and then clone it locally: .. code-block:: bash git clone - cd rascaline + cd featomic # setup the local repository so that the main branch tracks changes in # the original repository - git remote add upstream https://github.com/Luthaf/rascaline/ + git remote add upstream https://github.com/metatensor/featomic/ git fetch upstream git branch main --set-upstream-to=upstream/main @@ -115,7 +115,7 @@ You can run all tests by .. code-block:: bash - cd + cd cargo test # or cargo test --release to run tests in release mode These are exactly the same tests that will be performed online in our @@ -123,19 +123,18 @@ Github CI workflows. You can also run only a subset of tests with one of these commands: - ``cargo test`` runs everything -- ``cargo test --package=rascaline`` to run the calculators tests; -- ``cargo test --package=rascaline-c-api`` to run the C/C++ tests only; +- ``cargo test --package=featomic`` to run the main tests; - ``cargo test --test=run-cxx-tests`` will run the unit tests for the C/C++ API. If `valgrind`_ is installed, it will be used to check for memory - errors. You can disable this by setting the `RASCALINE_DISABLE_VALGRIND` - environment variable to 1 (`export RASCALINE_DISABLE_VALGRIND=1` for most + errors. You can disable this by setting the `FEATOMIC_DISABLE_VALGRIND` + environment variable to 1 (`export FEATOMIC_DISABLE_VALGRIND=1` for most Linux/macOS shells); - ``cargo test --test=check-cxx-install`` will build the C/C++ interfaces, install them and the associated CMake files and then try to build a basic project depending on this interface with CMake; -- ``cargo test --package=rascaline-torch`` to run the C++ TorchScript extension +- ``cargo test --package=featomic-torch`` to run the C++ TorchScript extension tests only; - ``cargo test --test=run-torch-tests`` will run the unit tests for the @@ -144,7 +143,7 @@ You can also run only a subset of tests with one of these commands: extension, install it and then try to build a basic project depending on this extension with CMake; -- ``cargo test --package=rascaline-python`` (or ``tox`` directly, see below) to +- ``cargo test --package=featomic-python`` (or ``tox`` directly, see below) to run Python tests only; - ``cargo test --lib`` to run unit tests; - ``cargo test --doc`` to run documentation tests; @@ -188,7 +187,7 @@ browser tox coverage combine --append \ ./.coverage \ - ./python/rascaline-torch/.coverage + ./python/featomic_torch/.coverage coverage html firefox htmlcov/index.html @@ -200,12 +199,12 @@ Writing your own calculator For adding a new calculator take a look at the tutorial for `adding a new calculator`_. -.. _adding a new calculator: https://luthaf.fr/rascaline/latest/devdoc/how-to/new-calculator.html +.. _adding a new calculator: https://metatensor.github.io/featomic/latest/devdoc/how-to/new-calculator.html Contributing to the documentation --------------------------------- -The documentation of rascaline is written in reStructuredText (rst) +The documentation of featomic is written in reStructuredText (rst) and uses `sphinx`_ documentation generator. In order to modify the documentation, first create a local version on your machine as described above. Then, build the documentation: diff --git a/Cargo.toml b/Cargo.toml index 87e813095..37d528563 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,8 @@ resolver = "2" members = [ - "rascaline", - "rascaline-c-api", + "featomic", + "featomic-torch", "python", - "rascaline-torch", - "docs/rascaline-json-schema", + "docs/featomic-json-schema", ] diff --git a/LICENSE b/LICENSE index 6485984a1..b5b80e30c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2023, rascaline developers +Copyright (c) 2023, featomic developers Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 91256809e..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,31 +0,0 @@ -global-exclude *.pyc -global-exclude .DS_Store - -prune docs - -recursive-include rascaline * -recursive-include rascaline-c-api * -recursive-include docs/rascaline-json-schema * - -# include the minimal crates from the Cargo workspace -include python/Cargo.toml -include python/lib.rs -include rascaline-torch/Cargo.toml -include rascaline-torch/lib.rs - -include Cargo.* -include pyproject.toml -include AUTHORS -include LICENSE - -prune python/tests -prune python/*.egg-info - -prune rascaline/tests -prune rascaline/benches/data -prune rascaline/examples - -prune rascaline-c-api/tests -prune rascaline-c-api/examples - -exclude tox.ini diff --git a/README.rst b/README.rst index 760d0e29f..c075c66fd 100644 --- a/README.rst +++ b/README.rst @@ -1,9 +1,9 @@ -Rascaline +Featomic ========= |test| |docs| |cov| -Rascaline is a library for the efficient computing of representations for atomistic +Featomic is a library for the efficient computing of representations for atomistic machine learning also called "descriptors" or "fingerprints". These representations can be used for atomistic machine learning (ml) models including ml potentials, visualization or similarity analysis. @@ -13,7 +13,7 @@ APIs for C/C++ and Python as well. .. warning:: - **Rascaline is still as the proof of concept stage. You should not use it for + **Featomic is still as the proof of concept stage. You should not use it for anything important.** List of implemented representations @@ -59,16 +59,16 @@ List of implemented representations For details, tutorials, and examples, please have a look at our `documentation`_. -.. _`documentation`: https://luthaf.fr/rascaline/index.html +.. _`documentation`: https://metatensor.github.io/featomic/index.html -.. |test| image:: https://img.shields.io/github/check-runs/Luthaf/rascaline/main?logo=github&label=tests +.. |test| image:: https://img.shields.io/github/check-runs/metatensor/featomic/main?logo=github&label=tests :alt: Tests status - :target: https://github.com/Luthaf/rascaline/actions?query=branch%3Amain + :target: https://github.com/metatensor/featomic/actions?query=branch%3Amain .. |docs| image:: https://img.shields.io/badge/documentation-latest-sucess :alt: Documentation :target: `documentation`_ -.. |cov| image:: https://codecov.io/gh/Luthaf/rascaline/branch/main/graph/badge.svg +.. |cov| image:: https://codecov.io/gh/metatensor/featomic/branch/main/graph/badge.svg :alt: Coverage Status - :target: https://codecov.io/gh/Luthaf/rascaline + :target: https://codecov.io/gh/metatensor/featomic diff --git a/docs/Doxyfile b/docs/Doxyfile index a4992a77a..6d185548d 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -42,7 +42,7 @@ DOXYFILE_ENCODING = UTF-8 # title of most generated pages and in a few other places. # The default value is: My Project. -PROJECT_NAME = rascaline +PROJECT_NAME = featomic # The PROJECT_NUMBER tag can be used to enter a project or revision number. This # could be handy for archiving the generated documentation or if some version @@ -944,9 +944,9 @@ WARN_LOGFILE = # Note: If this tag is empty the current directory is searched. INPUT = \ - ../rascaline-c-api/include/ \ - ../rascaline-torch/include/rascaline \ - ../rascaline-torch/include/rascaline/torch + ../featomic/include/ \ + ../featomic-torch/include/featomic \ + ../featomic-torch/include/featomic/torch # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses @@ -2355,7 +2355,7 @@ INCLUDE_FILE_PATTERNS = # recursively expanded use the := operator instead of the = operator. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -PREDEFINED = RASCALINE_TORCH_EXPORT= +PREDEFINED = FEATOMIC_TORCH_EXPORT= # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The diff --git a/docs/extensions/featomic_json_schema.py b/docs/extensions/featomic_json_schema.py new file mode 100644 index 000000000..25c7b7f1b --- /dev/null +++ b/docs/extensions/featomic_json_schema.py @@ -0,0 +1,351 @@ +import copy +import json +import os + +from docutils import nodes +from docutils.parsers.rst import Directive +from html_hidden import html_hidden +from markdown_it import MarkdownIt +from myst_parser.config.main import MdParserConfig +from myst_parser.mdit_to_docutils.base import DocutilsRenderer + + +def markdown_to_docutils(text): + parser = MarkdownIt() + tokens = parser.parse(text) + + renderer = DocutilsRenderer(parser) + return renderer.render(tokens, {"myst_config": MdParserConfig()}, {}) + + +def _target_id(text): + return nodes.make_id("json-schema-" + text) + + +class JsonSchemaDirective(Directive): + required_arguments = 1 + + def __init__(self, *args, **kwargs): + super(JsonSchemaDirective, self).__init__(*args, **kwargs) + self.docs_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + # use Dict[str, None] as an ordered set + self._definitions = {} + + def run(self, *args, **kwargs): + schema, content = self._load_schema() + + title = f"{schema['title']} hyper-parameters" + root_target, section = self._transform(schema, title) + + schema_node = html_hidden(toggle="Show full JSON schema") + schema_node += nodes.literal_block(text=content) + section.insert(1, schema_node) + + # add missing entries to self._definitions + for name in schema.get("$defs", {}).keys(): + self._definition_used(name) + + for name in self._definitions.keys(): + definition = schema["$defs"][name] + target, subsection = self._transform(definition, name) + + section += target + section += subsection + + return [root_target, section] + + def _definition_used(self, name): + self._definitions[name] = None + + def _transform(self, schema, name): + target_id = _target_id(name) + target = nodes.target( + "", "", ids=[target_id], names=[target_id], line=self.lineno + ) + + section = nodes.section() + section += nodes.title(text=name) + + description = schema.get("description", "") + section.extend(markdown_to_docutils(description)) + + section += self._json_schema_to_nodes(schema) + + return (target, section) + + def _load_schema(self): + path = os.path.join(self.docs_root, self.arguments[0]) + if not os.path.exists(path): + raise Exception(f"Unable to find JSON schema at '{path}'.") + + self.state.document.settings.env.note_dependency(path) + + with open(path) as fd: + content = fd.read() + + schema = json.loads(content) + + schema["$$rust-type"] = schema["title"] + schema["title"] = os.path.basename(path).split(".")[0] + + return schema, content + + def _json_schema_to_nodes( + self, + schema, + inline=False, + description=True, + optional=False, + ): + """Transform the schema for a single type to docutils nodes""" + + if optional: + # can only use optional for inline mode + assert inline + + optional_str = "?" if optional else "" + + if "$ref" in schema: + assert "properties" not in schema + assert "oneOf" not in schema + assert "anyOf" not in schema + assert "allOf" not in schema + + ref = schema["$ref"] + assert ref.startswith("#/$defs/") + type_name = ref.split("/")[-1] + + self._definition_used(type_name) + + refid = _target_id(type_name) + container = nodes.generated() + container += nodes.reference( + internal=True, + refid=refid, + text=type_name + optional_str, + ) + + return container + + # enums values are represented as allOf + if "allOf" in schema: + assert "properties" not in schema + assert "oneOf" not in schema + assert "anyOf" not in schema + assert "$ref" not in schema + + assert len(schema["allOf"]) == 1 + return self._json_schema_to_nodes(schema["allOf"][0]) + + # Enum variants uses "oneOf" + if "oneOf" in schema: + assert "anyOf" not in schema + assert "allOf" not in schema + assert "$ref" not in schema + + container = nodes.paragraph() + container += nodes.Text( + 'Pick one of the following according to its "type":' + ) + + global_properties = copy.deepcopy(schema.get("properties", {})) + + for prop in global_properties.values(): + prop["description"] = "See below." + + bullet_list = nodes.bullet_list() + for possibility in schema["oneOf"]: + possibility = copy.deepcopy(possibility) + possibility["properties"].update(global_properties) + + item = nodes.list_item() + item += self._json_schema_to_nodes( + possibility, inline=True, description=False + ) + + description = possibility.get("description", "") + item.extend(markdown_to_docutils(description)) + + item += self._json_schema_to_nodes(possibility, inline=False) + + bullet_list += item + + container += bullet_list + + global_properties = copy.deepcopy(schema) + global_properties.pop("oneOf") + if "properties" in global_properties: + container += nodes.transition() + container += self._json_schema_to_nodes(global_properties, inline=False) + + return container + + if "anyOf" in schema: + assert "properties" not in schema + assert "oneOf" not in schema + assert "allOf" not in schema + assert "$ref" not in schema + + # only supported for Option + assert len(schema["anyOf"]) == 2 + assert schema["anyOf"][1]["type"] == "null" + return self._json_schema_to_nodes( + schema["anyOf"][0], inline=True, optional=optional + ) + + if "type" in schema: + assert "oneOf" not in schema + assert "anyOf" not in schema + assert "allOf" not in schema + assert "$ref" not in schema + + if schema["type"] == "null": + assert not optional + return nodes.literal(text="null") + + elif schema["type"] == "object": + assert not optional + if not inline: + field_list = nodes.field_list() + for name, content in schema.get("properties", {}).items(): + name = nodes.field_name(text=name) + name += nodes.Text(": ") + if "default" in content: + name += nodes.Text("optional, ") + + name += self._json_schema_to_nodes( + content, inline=True, optional=False + ) + + field_list += name + + if description: + description_text = content.get("description", "") + + description = markdown_to_docutils(description_text) + body = nodes.field_body() + body.extend(description) + + field_list += body + + additional = schema.get("additionalProperties") + if additional is not None: + pass + + return field_list + else: + object_node = nodes.inline() + + object_node += nodes.Text("{") + + fields_unordered = schema.get("properties", {}) + # put "type" first in the output + fields = {} + if "type" in fields_unordered: + fields["type"] = fields_unordered.pop("type") + fields.update(fields_unordered) + + n_fields = len(fields) + for i_field, (name, content) in enumerate(fields.items()): + field = nodes.inline() + field += nodes.Text(f'"{name}": ') + + subfields = self._json_schema_to_nodes( + content, + inline=True, + optional="default" in content, + ) + if isinstance(subfields, nodes.literal): + subfields = [subfields] + + field += subfields + + if i_field != n_fields - 1: + field += nodes.Text(", ") + + object_node += field + + additional = schema.get("additionalProperties") + if isinstance(additional, dict): + # JSON Schema does not have a concept of key type being anything + # else than string. In featomic, we annotate `HashMap` with a + # custom `x-key-type` to carry this information all the way to + # here + key_type = schema.get("x-key-type") + if key_type is None: + key_type = "string" + + field = nodes.inline() + field += nodes.Text("[key: ") + field += nodes.literal(text=key_type) + field += nodes.Text("]: ") + + field += self._json_schema_to_nodes(additional) + + object_node += field + + object_node += nodes.Text("}") + + return object_node + + elif schema["type"] == "number": + assert schema["format"] == "double" + return nodes.literal(text="number" + optional_str) + + elif schema["type"] == "integer": + if "format" not in schema: + return nodes.literal(text="integer" + optional_str) + + if schema["format"].startswith("int"): + return nodes.literal(text="integer" + optional_str) + elif schema["format"].startswith("uint"): + return nodes.literal(text="positive integer" + optional_str) + else: + raise Exception(f"unknown integer format: {schema['format']}") + + elif schema["type"] == "string": + assert not optional + if "enum" in schema: + values = [f'"{v}"' for v in schema["enum"]] + return nodes.literal(text=" | ".join(values)) + elif "const" in schema: + return nodes.Text('"' + schema["const"] + '"') + else: + return nodes.literal(text="string") + + elif schema["type"] == "boolean": + if optional: + return nodes.literal(text="boolean?") + else: + return nodes.literal(text="boolean") + + elif isinstance(schema["type"], list): + # we only support list for Option + assert len(schema["type"]) == 2 + assert schema["type"][1] == "null" + + schema["type"] = schema["type"][0] + return self._json_schema_to_nodes( + schema, inline=True, optional=optional + ) + + elif schema["type"] == "array": + assert not optional + array_node = nodes.inline() + inner = self._json_schema_to_nodes(schema["items"], inline=True) + if isinstance(inner, nodes.literal): + array_node += nodes.literal(text=inner.astext() + "[]") + else: + array_node += inner + array_node += nodes.Text("[]") + return array_node + + else: + raise Exception(f"unsupported JSON type ({schema['type']}) in schema") + + raise Exception(f"unsupported JSON schema: {schema}") + + +def setup(app): + app.require_sphinx("3.3") + app.add_directive("featomic-json-schema", JsonSchemaDirective) diff --git a/docs/extensions/rascaline_json_schema.py b/docs/extensions/rascaline_json_schema.py deleted file mode 100644 index bb5f87159..000000000 --- a/docs/extensions/rascaline_json_schema.py +++ /dev/null @@ -1,213 +0,0 @@ -import json -import os - -from docutils import nodes -from docutils.parsers.rst import Directive -from html_hidden import html_hidden -from markdown_it import MarkdownIt -from myst_parser.config.main import MdParserConfig -from myst_parser.mdit_to_docutils.base import DocutilsRenderer - - -def markdow_to_docutils(text): - parser = MarkdownIt() - tokens = parser.parse(text) - - renderer = DocutilsRenderer(parser) - return renderer.render(tokens, {"myst_config": MdParserConfig()}, {}) - - -def _target_id(text): - return nodes.make_id("json-schema-" + text) - - -class JsonSchemaDirective(Directive): - required_arguments = 1 - - def __init__(self, *args, **kwargs): - super(JsonSchemaDirective, self).__init__(*args, **kwargs) - self._inline_call_count = 0 - self.docs_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) - - def run(self, *args, **kwargs): - schema, content = self._load_schema() - - title = f"{schema['title']} hyper-parameters" - root_target, section = self._transform(schema, title) - - schema_node = html_hidden(toggle="Show full JSON schema") - schema_node += nodes.literal_block(text=content) - section.insert(1, schema_node) - - for name, definition in schema.get("definitions", {}).items(): - target, subsection = self._transform(definition, name) - - section += target - section += subsection - - return [root_target, section] - - def _transform(self, schema, name): - target_id = _target_id(name) - target = nodes.target( - "", "", ids=[target_id], names=[target_id], line=self.lineno - ) - - section = nodes.section() - section += nodes.title(text=name) - - description = schema.get("description", "") - section.extend(markdow_to_docutils(description)) - - section += self._json_schema_to_nodes(schema) - - return (target, section) - - def _load_schema(self): - path = os.path.join(self.docs_root, self.arguments[0]) - if not os.path.exists(path): - raise Exception(f"Unable to find JSON schema at '{path}'.") - - self.state.document.settings.env.note_dependency(path) - - with open(path) as fd: - content = fd.read() - - schema = json.loads(content) - - schema["$$rust-type"] = schema["title"] - schema["title"] = os.path.basename(path).split(".")[0] - - return schema, content - - def _json_schema_to_nodes(self, schema, inline=False): - """Transform the schema for a single type to docutils nodes""" - - if "type" in schema: - if schema["type"] == "object": - if not inline: - field_list = nodes.field_list() - for name, content in schema.get("properties", {}).items(): - name = nodes.field_name(text=name) - name += nodes.Text(": ") - if "default" in content: - name += nodes.Text("optional, ") - - name += self._json_schema_to_nodes(content, inline=True) - - field_list += name - body = nodes.field_body() - - description = content.get("description", "") - body.extend(markdow_to_docutils(description)) - - field_list += body - - return field_list - else: - self._inline_call_count += 1 - - object_node = nodes.inline() - - if self._inline_call_count > 1: - object_node += nodes.Text("{") - - for name, content in schema.get("properties", {}).items(): - field = nodes.inline() - field += nodes.Text(f"{name}: ") - - subfields = self._json_schema_to_nodes(content, inline=True) - if isinstance(subfields, nodes.literal): - subfields = [subfields] - - for i, sf in enumerate(subfields): - field += sf - - if isinstance(sf, nodes.inline): - if i != len(subfields) - 2: - # len(xxx) - 2 to account for the final } - field += nodes.Text(", ") - object_node += field - - if self._inline_call_count > 1: - object_node += nodes.Text("}") - - self._inline_call_count -= 1 - return object_node - - elif schema["type"] == "number": - assert schema["format"] == "double" - return nodes.literal(text="number") - - elif schema["type"] == "integer": - if "format" not in schema: - return nodes.literal(text="integer") - - if schema["format"].startswith("int"): - return nodes.literal(text="integer") - elif schema["format"].startswith("uint"): - return nodes.literal(text="unsigned integer") - else: - raise Exception(f"unknown integer format: {schema['format']}") - - elif schema["type"] == "string": - if "enum" in schema: - values = [f'"{v}"' for v in schema["enum"]] - return nodes.literal(text=" | ".join(values)) - else: - return nodes.literal(text="string") - - elif schema["type"] == "boolean": - return nodes.literal(text="boolean") - - elif isinstance(schema["type"], list): - # we only support list for Option - assert len(schema["type"]) == 2 - assert schema["type"][1] == "null" - - schema["type"] = schema["type"][0] - return self._json_schema_to_nodes(schema, inline=True) - - elif schema["type"] == "array": - array_node = nodes.inline() - array_node += self._json_schema_to_nodes(schema["items"], inline=True) - array_node += nodes.Text("[]") - return array_node - - else: - raise Exception(f"unsupported JSON type ({schema['type']}) in schema") - - if "$ref" in schema: - ref = schema["$ref"] - assert ref.startswith("#/definitions/") - type_name = ref.split("/")[-1] - - refid = _target_id(type_name) - - return nodes.reference(internal=True, refid=refid, text=type_name) - - # enums values are represented as allOf - if "allOf" in schema: - assert len(schema["allOf"]) == 1 - return self._json_schema_to_nodes(schema["allOf"][0]) - - # Enum variants uses "oneOf" - if "oneOf" in schema: - bullet_list = nodes.bullet_list() - for possibility in schema["oneOf"]: - item = nodes.list_item() - item += self._json_schema_to_nodes(possibility, inline=True) - - description = possibility.get("description", "") - item.extend(markdow_to_docutils(description)) - - bullet_list += item - - return bullet_list - - raise Exception(f"unsupported JSON schema: {schema}") - - -def setup(app): - app.require_sphinx("3.3") - app.add_directive("rascaline-json-schema", JsonSchemaDirective) diff --git a/docs/featomic-json-schema/Cargo.toml b/docs/featomic-json-schema/Cargo.toml new file mode 100644 index 000000000..f32619f79 --- /dev/null +++ b/docs/featomic-json-schema/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "featomic-json-schema" +version = "0.1.0" +authors = ["Guillaume Fraux "] +edition = "2021" +publish = false +rust-version = "1.74" + +[[bin]] +name = "featomic-json-schema" +path = "main.rs" +bench = false +test = false + +[dependencies] +featomic = {path = "../../featomic"} +schemars = "=1.0.0-alpha.15" +serde_json = "1" diff --git a/docs/featomic-json-schema/main.rs b/docs/featomic-json-schema/main.rs new file mode 100644 index 000000000..d0dad7f67 --- /dev/null +++ b/docs/featomic-json-schema/main.rs @@ -0,0 +1,103 @@ +use std::path::PathBuf; + +use schemars::Schema; + +use featomic::calculators::AtomicComposition; +use featomic::calculators::SortedDistances; +use featomic::calculators::SphericalExpansionParameters; +use featomic::calculators::LodeSphericalExpansionParameters; +use featomic::calculators::PowerSpectrumParameters; +use featomic::calculators::RadialSpectrumParameters; +use featomic::calculators::NeighborList; + + +macro_rules! generate_schema { + ($Type: ty) => { + generate_schema!(stringify!($Type), $Type) + }; + ($name: expr, $Type: ty) => { + save_schema($name, schemars::schema_for!($Type)) + }; +} + +static REFS_TO_RENAME: &[RenameRefInSchema] = &[ + RenameRefInSchema { + in_code: "SphericalExpansionBasis_for_SoapRadialBasis", + in_docs: "SphericalExpansionBasis", + }, + RenameRefInSchema { + in_code: "SphericalExpansionBasis_for_LodeRadialBasis", + in_docs: "SphericalExpansionBasis", + }, + RenameRefInSchema { + in_code: "SoapRadialBasis", + in_docs: "RadialBasis", + }, + RenameRefInSchema { + in_code: "LodeRadialBasis", + in_docs: "RadialBasis", + }, +]; + +#[derive(Clone)] +struct RenameRefInSchema { + in_code: &'static str, + in_docs: &'static str, +} + +impl schemars::transform::Transform for RenameRefInSchema { + fn transform(&mut self, schema: &mut Schema) { + let in_code_reference = format!("#/$defs/{}", self.in_code); + if let Some(schema_object) = schema.as_object_mut() { + if let Some(reference) = schema_object.get_mut("$ref") { + if reference == &in_code_reference { + *reference = format!("#/$defs/{}", self.in_docs).into(); + } + } + } + schemars::transform::transform_subschemas(self, schema); + } +} + +fn save_schema(name: &str, mut schema: Schema) { + let schema_object = schema.as_object_mut().expect("schema should be an object"); + + // rename some of the autogenerate names. + // Step 1: rename the definitions + for transform in REFS_TO_RENAME { + if let Some(definitions) = schema_object.get_mut("$defs") { + let definitions = definitions.as_object_mut().expect("$defs should be an object"); + if let Some(value) = definitions.remove(transform.in_code) { + assert!(!definitions.contains_key(transform.in_docs)); + definitions.insert(transform.in_docs.into(), value); + } + } + } + + // Step 2: rename the references to these definitions + for transform in REFS_TO_RENAME { + schemars::transform::transform_subschemas(&mut transform.clone(), &mut schema); + } + + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.pop(); + path.push("build"); + path.push("json-schemas"); + std::fs::create_dir_all(&path).expect("failed to create JSON schema directory"); + + path.push(format!("{}.json", name)); + + let schema = serde_json::to_string_pretty(&schema).expect("failed to create JSON schema"); + std::fs::write(path, schema).expect("failed to save JSON schema to file"); +} + +fn main() { + generate_schema!(AtomicComposition); + generate_schema!(NeighborList); + generate_schema!(SortedDistances); + generate_schema!("SphericalExpansionByPair", SphericalExpansionParameters); + generate_schema!("SphericalExpansion", SphericalExpansionParameters); + generate_schema!("LodeSphericalExpansion", LodeSphericalExpansionParameters); + generate_schema!("SoapPowerSpectrum", PowerSpectrumParameters); + generate_schema!("SoapRadialSpectrum", RadialSpectrumParameters); +} diff --git a/docs/rascaline-json-schema/Cargo.toml b/docs/rascaline-json-schema/Cargo.toml deleted file mode 100644 index bff70a568..000000000 --- a/docs/rascaline-json-schema/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "rascaline-json-schema" -version = "0.1.0" -authors = ["Luthaf "] -edition = "2021" -publish = false -rust-version = "1.74" - -[[bin]] -name = "rascaline-json-schema" -path = "main.rs" -bench = false -test = false - -[dependencies] -rascaline = {path = "../../rascaline"} -schemars = "0.8.6" -serde_json = "1" diff --git a/docs/rascaline-json-schema/main.rs b/docs/rascaline-json-schema/main.rs deleted file mode 100644 index 28e1188db..000000000 --- a/docs/rascaline-json-schema/main.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::path::PathBuf; - -use schemars::schema::RootSchema; - -use rascaline::calculators::AtomicComposition; -use rascaline::calculators::SortedDistances; -use rascaline::calculators::SphericalExpansionParameters; -use rascaline::calculators::LodeSphericalExpansionParameters; -use rascaline::calculators::PowerSpectrumParameters; -use rascaline::calculators::RadialSpectrumParameters; -use rascaline::calculators::NeighborList; - - -macro_rules! generate_schema { - ($Type: ty) => { - generate_schema!(stringify!($Type), $Type) - }; - ($name: expr, $Type: ty) => { - save_schema($name, schemars::schema_for!($Type)) - }; -} - -fn save_schema(name: &str, schema: RootSchema) { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.pop(); - path.push("build"); - path.push("json-schemas"); - std::fs::create_dir_all(&path).expect("failed to create JSON schema directory"); - - path.push(format!("{}.json", name)); - - let schema = serde_json::to_string_pretty(&schema).expect("failed to create JSON schema"); - std::fs::write(path, schema).expect("failed to save JSON schema to file"); -} - -fn main() { - generate_schema!(AtomicComposition); - generate_schema!(NeighborList); - generate_schema!(SortedDistances); - generate_schema!("SphericalExpansionByPair", SphericalExpansionParameters); - generate_schema!("SphericalExpansion", SphericalExpansionParameters); - generate_schema!("LodeSphericalExpansion", LodeSphericalExpansionParameters); - generate_schema!("SoapPowerSpectrum", PowerSpectrumParameters); - generate_schema!("SoapRadialSpectrum", RadialSpectrumParameters); -} diff --git a/docs/requirements.txt b/docs/requirements.txt index 3fe557a58..ba000586a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,7 +6,7 @@ sphinx-gallery # convert python files into nice documentation sphinx-tabs # tabs for code examples (one tab per language) pygments >=2.11 # syntax highligthing toml # to extract version number out of Cargo.toml -myst-parser # markdown => rst translation, used in extensions/rascaline_json_schema +myst-parser # markdown => rst translation, used in extensions/featomic_json_schema # dependencies for the tutorials --extra-index-url https://download.pytorch.org/whl/cpu diff --git a/docs/src/conf.py b/docs/src/conf.py index 1aa61e596..daac36fe9 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -11,18 +11,18 @@ sys.path.append(os.path.join(ROOT, "docs", "extensions")) -os.environ["RASCALINE_IMPORT_FOR_SPHINX"] = "1" +os.environ["FEATOMIC_IMPORT_FOR_SPHINX"] = "1" os.environ["METATENSOR_IMPORT_FOR_SPHINX"] = "1" # -- Project information ----------------------------------------------------- -project = "Rascaline" +project = "Featomic" author = ", ".join(open(os.path.join(ROOT, "AUTHORS")).read().splitlines()) copyright = f"{datetime.now().date().year}, {author}" def load_version_from_cargo_toml(): - with open(os.path.join(ROOT, "rascaline", "Cargo.toml")) as fd: + with open(os.path.join(ROOT, "featomic", "Cargo.toml")) as fd: data = toml.load(fd) return data["package"]["version"] @@ -43,7 +43,7 @@ def build_cargo_docs(): "cargo", "doc", "--package", - "rascaline", + "featomic", "--package", "metatensor", "--no-deps", @@ -68,12 +68,12 @@ def build_cargo_docs(): def extract_json_schema(): - subprocess.run(["cargo", "run", "--package", "rascaline-json-schema"]) + subprocess.run(["cargo", "run", "--package", "featomic-json-schema"]) def build_doxygen_docs(): # we need to run a build to make sure the header is up to date - subprocess.run(["cargo", "build", "--package", "rascaline-c-api"]) + subprocess.run(["cargo", "build", "--package", "featomic"]) subprocess.run(["doxygen", "Doxyfile"], cwd=os.path.join(ROOT, "docs")) @@ -83,7 +83,7 @@ def build_doxygen_docs(): def setup(app): - app.add_css_file("rascaline.css") + app.add_css_file("featomic.css") # -- General configuration --------------------------------------------------- @@ -99,7 +99,7 @@ def setup(app): "breathe", "sphinx_gallery.gen_gallery", "sphinx_tabs.tabs", - "rascaline_json_schema", + "featomic_json_schema", "html_hidden", ] @@ -122,18 +122,18 @@ def setup(app): sphinx_gallery_conf = { "filename_pattern": "/*", - "examples_dirs": ["../../python/rascaline/examples"], + "examples_dirs": ["../../python/featomic/examples"], "gallery_dirs": ["examples"], "min_reported_time": 5, - # Make the code snippet for rascaline functions clickable - "reference_url": {"rascaline": None}, - "prefer_full_module": ["rascaline"], + # Make the code snippet for featomic functions clickable + "reference_url": {"featomic": None}, + "prefer_full_module": ["featomic"], } breathe_projects = { - "rascaline": os.path.join(ROOT, "docs", "build", "doxygen", "xml"), + "featomic": os.path.join(ROOT, "docs", "build", "doxygen", "xml"), } -breathe_default_project = "rascaline" +breathe_default_project = "featomic" breathe_domain_by_extension = { "h": "c", } @@ -166,7 +166,7 @@ def setup(app): "footer_icons": [ { "name": "GitHub", - "url": "https://github.com/Luthaf/rascaline", + "url": "https://github.com/metatensor/featomic", "html": "", "class": "fa-brands fa-github fa-2x", }, diff --git a/docs/src/devdoc/explanations/architecture.rst b/docs/src/devdoc/explanations/architecture.rst index fdbe213f6..1fe63cc3f 100644 --- a/docs/src/devdoc/explanations/architecture.rst +++ b/docs/src/devdoc/explanations/architecture.rst @@ -3,26 +3,26 @@ Code organization The code is organized in three main products, each in a separate directory: -- ``rascaline/`` contains the main Rust implementation of all calculators, and +- ``featomic/`` contains the main Rust implementation of all calculators, and the corresponding unit and regression tests; -- ``rascaline-c-api/`` is a Rust crate containing the implementation of the - rascaline C API; -- ``python/`` contains the Python interface to rascaline, and the corresponding - tests +- ``featomic-torch/`` contains the TorchScript bindings to featomic, written in + C++; +- ``python/`` contains the Python interface to featomic and featomic-torch, and + the corresponding tests Finally, ``docs/`` contains the documentation for everything related to -rascaline. +featomic. -The main rascaline crate +The main featomic crate ^^^^^^^^^^^^^^^^^^^^^^^^ -Inside the main rascaline crate, the following code organization is used: +Inside the main featomic crate, the following code organization is used: -- ``rascaline/benches``: benchmarks of the code on some simple systems; -- ``rascaline/tests``: regression tests for all calculators; -- ``rascaline/src/system/``: definition of everything related to systems: +- ``featomic/benches``: benchmarks of the code on some simple systems; +- ``featomic/tests``: regression tests for all calculators; +- ``featomic/src/system/``: definition of everything related to systems: ``UnitCell``, the ``System`` trait and ``SimpleSystem`` implementation; -- ``rascaline/src/calculator.rs``: convenience wrapper around implementations of +- ``featomic/src/calculator.rs``: convenience wrapper around implementations of ``CalculatorBase`` that setup everything before a calculation; -- ``rascaline/src/calculators/``: definition of the ``CalculatorBase`` trait and +- ``featomic/src/calculators/``: definition of the ``CalculatorBase`` trait and various implementations of this trait; diff --git a/docs/src/devdoc/explanations/index.rst b/docs/src/devdoc/explanations/index.rst index 9ccd23922..32c99881c 100644 --- a/docs/src/devdoc/explanations/index.rst +++ b/docs/src/devdoc/explanations/index.rst @@ -4,8 +4,8 @@ Explanations ============ The explanation section discusses topics that broaden your knowledge of -rascaline. Technical facts and some tidbits of useful information are found here -to give you more clarity and understanding of what rascaline is all about. +featomic. Technical facts and some tidbits of useful information are found here +to give you more clarity and understanding of what featomic is all about. .. toctree:: :maxdepth: 2 diff --git a/docs/src/devdoc/explanations/interfaces.rst b/docs/src/devdoc/explanations/interfaces.rst index d0d27267d..f26d968f7 100644 --- a/docs/src/devdoc/explanations/interfaces.rst +++ b/docs/src/devdoc/explanations/interfaces.rst @@ -4,32 +4,32 @@ Python and C interface How is the C interface exported ------------------------------- -Rascaline exports a C interface, defined in ``rascaline-c-api``. This C -interface is created directly in Rust, without involving any C code. +Featomic exports a C interface, created directly in Rust without involving any C +code. This is done by marking functions as ``#[no_mangle] extern pub fn `` in -``rascaline-c-api``, and only using types safe to send to C (mostly pointers and -basic values such as floats or integers). Of these markers, ``pub`` ensures that -the function is exported from the library (it should appear as a ``T`` symbol in -``nm`` output); ``extern`` forces the function to use the C calling convention -(a calling convention describes where in memory/CPU registers the caller should -put data that the function expects); and ``#[no_mangle]`` tells the compiler to -export the function under this exact name, instead of using a mangled named -containing the module path and functions parameters. +``featomic/src/c-api/*.rs``, and only using types safe to send to C (mostly +pointers and basic values such as floats or integers). Of these markers, ``pub`` +ensures that the function is exported from the library (it should appear as a +``T`` symbol in ``nm`` output); ``extern`` forces the function to use the C +calling convention (a calling convention describes where in memory/CPU registers +the caller should put data that the function expects); and ``#[no_mangle]`` +tells the compiler to export the function under this exact name, instead of +using a mangled named containing the module path and functions parameters. Additionally, the C interfaces expose C-compatible structs declared with ``#[repr(C)] pub struct {}``; where ``#[repr(C)]`` ensures that the compiler lays out the fields in the exact order they are declared, without re-organizing them. -``rascaline-c-api`` is then compiled to a shared library (``librascaline.so`` / -``librascaline.dylib`` / ``librascaline.dll``), which can be used by any -language able to call C code to call the exported functions without ever -realizing it is speaking with Rust code. +``featomic`` is then compiled to a shared library (``libfeatomic.so`` / +``libfeatomic.dylib`` / ``libfeatomic.dll``), which can be used by any language +able to call C code to call the exported functions without ever realizing it is +speaking with Rust code. -The list of exported functions, together with the types of the function's +The list of exported functions, together with the types of the function's parameters, and struct definitions are extracted from the rust source code using -`cbindgen`_, which creates the ``rascaline-c-api/rascaline.h`` header file +`cbindgen`_, which creates the ``featomic/include/featomic.h`` header file containing all of this information in a C compatible syntax. All of the documentation is also reproduced using `doxygen`_ syntax. @@ -41,27 +41,27 @@ The Python interface used the `ctypes`_ module to call exported symbols from the shared library. For the Python code to be able to call exported function safely, it needs to know a few things. In particular, it needs to know the name of the function, the number and types of parameters and the return type of the -function. All this information is available in ``rascaline-c-api/rascaline.h``, +function. All this information is available in ``featomic/include/featomic.h``, but not in a way that is easily accessible from `ctypes`_. There is a script in ``python/scripts/generate-declaration.py`` which reads the header file using -`pycparser`_, and creates the `python/rascaline/_rascaline.py` file which +`pycparser`_, and creates the `python/featomic/_c_api.py` file which declares all functions in the way expected by the `ctypes`_ module. You will need to manually re-run this script if you modify any of the exported functions -in `rascaline-c-api`. +in `featomic/src/c-api`. The schematic below describes all the relationships between the components involved in creating the Python interface. -.. figure:: ../../../static/images/rascaline-python.* +.. figure:: ../../../static/images/featomic-python.* :width: 400px :align: center Schematic representation of all components in the Python interface. The rust - crate ``rascaline-c-api`` is compiled to a shared library - (``librascaline.so`` on Linux), and `cbindgen`_ is used to generate the - corresponding header. This header is then read with `pycparser`_ to create - ctypes' compatible declarations, used to ensure that Python and rust agree - fully on the parameters types, allowing Python to directly call Rust code. + crate ``featomic`` is compiled to a shared library (``libfeatomic.so`` on + Linux), and `cbindgen`_ is used to generate the corresponding header. This + header is then read with `pycparser`_ to create ctypes' compatible + declarations, used to ensure that Python and rust agree fully on the + parameters types, allowing Python to directly call Rust code. .. _ctypes: https://docs.python.org/3/library/ctypes.html .. _pycparser: https://github.com/eliben/pycparser diff --git a/docs/src/devdoc/explanations/radial-integral.rst b/docs/src/devdoc/explanations/radial-integral.rst index a8e6587f1..899eca3cf 100644 --- a/docs/src/devdoc/explanations/radial-integral.rst +++ b/docs/src/devdoc/explanations/radial-integral.rst @@ -1,13 +1,13 @@ .. _radial-integral: SOAP and LODE radial integrals -=================================== +============================== -On this page, we describe the exact mathematical expression that are implemented in the -radial integral and the splined radial integral classes i.e. -:ref:`python-splined-radial-integral`. Note that this page assumes knowledge of -spherical expansion & friends and currently serves as a reference page for -the developers to support the implementation. +On this page, we describe the exact mathematical expression that are implemented +in the radial integral and the splined radial integral classes i.e. +:ref:`python-utils-splines`. Note that this page assumes knowledge of spherical +expansion & friends and currently serves as a reference page for the developers +to support the implementation. Preliminaries ------------- @@ -143,7 +143,7 @@ Gaussian Densities ~~~~~~~~~~~~~~~~~~ Here, we consider another popular use case, where the atomic density function is a -Gaussian. In rascaline, we use the convention +Gaussian. In featomic, we use the convention .. math:: diff --git a/docs/src/devdoc/how-to/new-calculator.rst b/docs/src/devdoc/how-to/new-calculator.rst index b0d597de5..49d0f6f44 100644 --- a/docs/src/devdoc/how-to/new-calculator.rst +++ b/docs/src/devdoc/how-to/new-calculator.rst @@ -32,18 +32,18 @@ programming languages is assumed. If you are just starting up, you may find the official `Rust book `_ useful; as well as the documentation for the `standard library `_; and the `API documentation`_ for -rascaline itself. +featomic itself. -We will also assume that you have a local copy of the rascaline git repository, +We will also assume that you have a local copy of the featomic git repository, and can build the code and run the tests. If not, please look at the :ref:`devdoc-get-started` sections. -.. _API documentation: ../reference/rust/rascaline/index.html +.. _API documentation: ../reference/rust/featomic/index.html The traits we'll use -------------------- -Two of the three :ref:`core concepts ` in rascaline are +Two of the three :ref:`core concepts ` in featomic are represented in the code as Rust traits: systems implements the `System`_ trait, and calculators implement the `CalculatorBase`_ trait. Traits (also called interfaces in other languages) define contracts that the implementing code must @@ -57,19 +57,19 @@ In this tutorial, our goal is to write a new struct implementing `System`_ trait objects, and using data from those fill up a `TensorMap`_ (defined in the metatensor crate). -.. _System: ../reference/rust/rascaline/systems/trait.System.html -.. _CalculatorBase: ../reference/rust/rascaline/calculators/trait.CalculatorBase.html -.. _Calculator: ../reference/rust/rascaline/struct.Calculator.html +.. _System: ../reference/rust/featomic/systems/trait.System.html +.. _CalculatorBase: ../reference/rust/featomic/calculators/trait.CalculatorBase.html +.. _Calculator: ../reference/rust/featomic/struct.Calculator.html .. _TensorMap: ../reference/rust/metatensor/tensor/struct.TensorMap.html Implementation -------------- -Let's start by creating a new file in ``rascaline/src/calculators/moments.rs``, +Let's start by creating a new file in ``featomic/src/calculators/moments.rs``, and importing everything we'll need. Everything in here will be explained when we get to using it. -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/s1_scaffold.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/s1_scaffold.rs :language: rust :start-after: [imports] :end-before: [imports] @@ -78,7 +78,7 @@ Then, we can define a struct for our new calculator ``GeometricMoments``. It will contain two fields: ``cutoff`` to store the cutoff radius, and ``max_moment`` to store the maximal moment to compute. -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/s1_scaffold.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/s1_scaffold.rs :language: rust :start-after: [struct] :end-before: [struct] @@ -90,7 +90,7 @@ calculator. Users might be more familiar with the concrete struct `Calculator`_, which uses a ``Box`` (i.e. a pointer to a ``CalculatorBase``) to provide its functionalities. -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/s1_scaffold.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/s1_scaffold.rs :language: rust :start-after: [impl] :end-before: [impl] @@ -111,7 +111,7 @@ trait. .. _Into: https://doc.rust-lang.org/std/convert/trait.Into.html -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/s2_metadata.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/s2_metadata.rs :language: rust :start-after: [CalculatorBase::name] :end-before: [CalculatorBase::name] @@ -119,18 +119,18 @@ trait. Then, the ``parameters`` function should return the parameters used to create the current instance of the calculator in JSON format. To this end, we -use `serde`_ and ``serde_json`` everywhere in rascaline, so it is a good idea to +use `serde`_ and ``serde_json`` everywhere in featomic, so it is a good idea to do the same here. Let's start by adding the corresponding ``#[derive]`` to the definition of ``GeometricMoments``, and use it to implement the function. .. _serde: https://serde.rs/ -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/s2_metadata.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/s2_metadata.rs :language: rust :start-after: [struct] :end-before: [struct] -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/s2_metadata.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/s2_metadata.rs :language: rust :start-after: [CalculatorBase::parameters] :end-before: [CalculatorBase::parameters] @@ -149,7 +149,7 @@ in neighbors lists. Here, we only have one --- ``self.cutoffs`` --- and we use ``std::slice::from_ref`` to construct a list with a single element from a scalar. -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/s2_metadata.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/s2_metadata.rs :language: rust :start-after: [CalculatorBase::cutoffs] :end-before: [CalculatorBase::cutoffs] @@ -184,7 +184,7 @@ whether atoms should be considered to be their own neighbor or not. .. _Labels: ../reference/rust/metatensor/labels/struct.Labels.html .. _LabelsBuilder: ../reference/rust/metatensor/labels/struct.LabelsBuilder.html -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/s2_metadata.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/s2_metadata.rs :language: rust :start-after: [CalculatorBase::keys] :end-before: [CalculatorBase::keys] @@ -203,7 +203,7 @@ unpacking: we are returning a `Result`_ since any call to a `System`_ function can fail. The non-error case of the result is a ``Vec``: we need one set of `Labels`_ for each key/block. -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/s2_metadata.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/s2_metadata.rs :language: rust :start-after: [CalculatorBase::samples] :end-before: [CalculatorBase::samples] @@ -224,7 +224,7 @@ information about symmetry operations or any kind of tensorial components. Here, we dont' have any components (the ``GeometricMoments`` representation is invariant), so we just return a list (one for each key) of empty vectors. -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/s2_metadata.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/s2_metadata.rs :language: rust :start-after: [CalculatorBase::components] :end-before: [CalculatorBase::components] @@ -245,7 +245,7 @@ each key in ``CalculatorBase::properties``, we use the fact that the properties are the same for each key/block and make copies of the ``Labels`` (since ``Labels`` are reference-counted, the copies are actually quite cheap). -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/s2_metadata.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/s2_metadata.rs :language: rust :start-after: [CalculatorBase::properties] :end-before: [CalculatorBase::properties] @@ -261,7 +261,7 @@ be computed by the current calculator. Typically ``parameter`` is either ``"positions"``, ``"cell"```, or ``"strain"``. Here we only support computing the gradients with respect to positions. -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/s2_metadata.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/s2_metadata.rs :language: rust :start-after: [CalculatorBase::supports_gradient] :end-before: [CalculatorBase::supports_gradient] @@ -292,7 +292,7 @@ list of systems on which we want to run the calculation. We are again using the ``AtomCenteredSamples`` here to share code between multiple calculators all using atom-centered samples. -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/s2_metadata.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/s2_metadata.rs :language: rust :start-after: [CalculatorBase::positions_gradient_samples] :end-before: [CalculatorBase::positions_gradient_samples] @@ -323,7 +323,7 @@ This being said, let's start writing our ``compute`` function. We'll defensively check that the tensor map keys match what we expect from them, and return a unit value ``()`` wrapped in ``Ok`` at the end of the function. -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/s3_compute_1.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/s3_compute_1.rs :language: rust :start-after: [compute] :end-before: [compute] @@ -343,7 +343,7 @@ question mark ``?`` operator does exactly that: if the value returned by the called function is ``Err(e)``, ``?`` immediately returns ``Err(e)``; and if the result is ``Ok(v)``, ``?`` extract the ``v`` and the execution continues. -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/s3_compute_2.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/s3_compute_2.rs :language: rust :start-after: [compute] :end-before: [compute] @@ -371,7 +371,7 @@ candidate samples and check for their presence. If neither of the samples was requested, then we can skip the calculation for this pair. We also use ``system.pairs_containing()`` to get the number of neighbors a given center has. -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/s3_compute_3.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/s3_compute_3.rs :language: rust :start-after: [compute] :end-before: [compute] @@ -384,7 +384,7 @@ Now, we can check if the samples are present, and if they are, iterate over the requested features, compute the moments for the current pair distance, and accumulate it in the descriptor values array: -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/s3_compute_4.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/s3_compute_4.rs :language: rust :start-after: [compute] :end-before: [compute] @@ -415,7 +415,7 @@ gradients of the descriptor centered on :math:`i` with respect to atom :math:`j`, we also need to account for the gradient of the descriptor centered on atom :math:`i` with respect to its own position. -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/s3_compute_5.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/s3_compute_5.rs :language: rust :start-after: [compute] :end-before: [compute] @@ -427,7 +427,7 @@ on atom :math:`i` with respect to its own position. :toggle: Here is the final implementation for the compute function :before-not-html: Here is the final implementation for the compute function - .. literalinclude:: ../../../../rascaline/src/tutorials/moments/moments.rs + .. literalinclude:: ../../../../featomic/src/tutorials/moments/moments.rs :language: rust :start-after: [compute] :end-before: [compute] @@ -442,12 +442,12 @@ needs to be constructed from a calculator name and hyper-parameters in JSON format. When the user calls ``Calculator::new("calculator_name", "{\"hyper_parameters\": -1}")``, rascaline looks for ``"calculator_name"`` in the global calculator +1}")``, featomic looks for ``"calculator_name"`` in the global calculator registry, and tries to create an instance using the hyper-parameters. In order to make our calculator available to all users, we need to add it to this -registry, in ``rascaline/src/calculator.rs``. The registry looks like this: +registry, in ``featomic/src/calculator.rs``. The registry looks like this: -.. literalinclude:: ../../../../rascaline/src/calculator.rs +.. literalinclude:: ../../../../featomic/src/calculator.rs :language: rust :start-after: [calculator-registration] :end-before: [calculator-registration] @@ -461,7 +461,7 @@ You'll need to make sure to bring your new calculator in scope with a `use` item Additionally, you may want to add a convenience class in Python for our new calculator. For this, you can add a class like this to -``python/rascaline/calculators.py``: +``python/featomic/calculators.py``: .. code-block:: python @@ -480,11 +480,11 @@ calculator. For this, you can add a class like this to ############################################################################# # this allows using the calculator like this - from rascaline import GeometricMoments + from featomic import GeometricMoments calculator = GeometricMoments(cutoff=3.5, max_moment=6, gradients=False) # instead of - from rascaline.calculators import CalculatorBase + from featomic.calculators import CalculatorBase calculator = CalculatorBase( "geometric_moments", {"cutoff": 3.5, "max_moment": 6, "gradients": False}, @@ -499,7 +499,7 @@ Testing the new calculator Before we can release our new calculator in the world, we need to make sure it currently behaves as intended, and that we have a way to ensure it continues to -behave as intended as the code changes. To achieve both goals, rascaline uses +behave as intended as the code changes. To achieve both goals, featomic uses unit tests and regression tests. Unit tests are written in the same file as the main part of the code, in a ``tests`` module, and are expected to test high level properties of the code. For example, unit tests allow to check that the @@ -508,13 +508,13 @@ right values are computed when the users requests a subset of samples & features. On the other hand, regression tests check the exact values produced by a given calculator on a specific system; and that these values stay the same as we modify the code, for example when trying to optimize it. These regression -tests live in the ``rascaline/tests`` folder, with one file per test. +tests live in the ``featomic/tests`` folder, with one file per test. This tutorial will focus on unit tests and introduce some utilities for tests that should apply to all calculators. To write regression tests, you should take inspiration from existing tests such as ``spherical-expansion`` test. Each Rust -file in ``rascaline/tests`` is associated with a Python file in -``rascaline/tests/data`` used to generate the values the regression test is +file in ``featomic/tests`` is associated with a Python file in +``featomic/tests/data`` used to generate the values the regression test is checking, so you'll need one of these as well. Testing properties @@ -531,12 +531,12 @@ our geometric moments representation, the first moment (with order 0) should always be the number of neighbor of the current atomic type over the total number of neighbors. A test checking this property would look like this: -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/moments.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/moments.rs :language: rust :start-after: [property-test] :end-before: [property-test] -The ``rascaline::systems::test_utils::test_systems`` function provides a couple +The ``featomic::systems::test_utils::test_systems`` function provides a couple of very simple systems to be used for testing. Testing partial calculations @@ -544,11 +544,11 @@ Testing partial calculations One properties that all calculators must respect is that computing only a subset of samples or feature should give the same values as computing everything. -Rascaline provides a function (``calculators::tests_utils::compute_partial``) to +Featomic provides a function (``calculators::tests_utils::compute_partial``) to check this for you, simplifying the tests a bit. Here is how one can use it with the ``GeometricMoments`` calculator: -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/moments.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/moments.rs :language: rust :start-after: [partial-test] :end-before: [partial-test] @@ -558,10 +558,10 @@ Testing gradients ^^^^^^^^^^^^^^^^^ If a calculator can compute gradients, it is a good idea to check if the -gradient does match the finite differences definition of derivatives. Rascaline +gradient does match the finite differences definition of derivatives. Featomic provides ``calculators::tests_utils::finite_difference`` to help check this. -.. literalinclude:: ../../../../rascaline/src/tutorials/moments/moments.rs +.. literalinclude:: ../../../../featomic/src/tutorials/moments/moments.rs :language: rust :start-after: [finite-differences-test] :end-before: [finite-differences-test] diff --git a/docs/src/devdoc/how-to/profiling.rst b/docs/src/devdoc/how-to/profiling.rst index d366ab2d7..3405da7fd 100644 --- a/docs/src/devdoc/how-to/profiling.rst +++ b/docs/src/devdoc/how-to/profiling.rst @@ -2,7 +2,7 @@ Profiling calculation ===================== It can be interesting to know where a calculation is spending its time. To this -end, rascaline includes self-profiling code that can record and display which +end, featomic includes self-profiling code that can record and display which part of the calculation takes time, and which function called long-running functions. All the example should output something similar to the table below. @@ -64,15 +64,15 @@ You can obtain a dataset for profiling from our :download:`website <../../../sta .. group-tab:: Rust - .. literalinclude:: ../../../../rascaline/examples/profiling.rs + .. literalinclude:: ../../../../featomic/examples/profiling.rs :language: rust .. group-tab:: C++ - .. literalinclude:: ../../../../rascaline-c-api/examples/profiling.cpp + .. literalinclude:: ../../../../featomic/examples/profiling.cpp :language: c++ .. group-tab:: C - .. literalinclude:: ../../../../rascaline-c-api/examples/profiling.c + .. literalinclude:: ../../../../featomic/examples/profiling.c :language: c diff --git a/docs/src/devdoc/index.rst b/docs/src/devdoc/index.rst index 7d48d384e..a2d5adabd 100644 --- a/docs/src/devdoc/index.rst +++ b/docs/src/devdoc/index.rst @@ -8,10 +8,10 @@ This developer documentation is divided into three parts: 1. :ref:`devdoc-get-started` explains how you can start developing code and documentation; 2. The :ref:`devdoc-how-to`, take you through a series of steps on key problems - for developing rascaline; + for developing featomic; 3. In the :ref:`devdoc-explanations` section discusses key topics and concepts at a fairly high level and provides useful explanations to expand your - knowledge about the architecture of rascaline; + knowledge about the architecture of featomic; .. toctree:: :maxdepth: 2 diff --git a/docs/src/explanations/concepts.rst b/docs/src/explanations/concepts.rst index 93d25737b..c253bfd26 100644 --- a/docs/src/explanations/concepts.rst +++ b/docs/src/explanations/concepts.rst @@ -1,21 +1,21 @@ .. _core-concepts: -Core concepts of rascaline +Core concepts of featomic ========================== -Rascaline is a library computing representations of atomic systems for machine +Featomic is a library computing representations of atomic systems for machine learning applications. These representations encode fundamental symmetries of the systems to ensure that the machine learning algorithm is as efficient as possible. Examples of representations include the `Smooth Overlap of Atomic Positions `_ (SOAP), `Behler-Parrinello symmetry functions `_, `Coulomb matrices`_, and many others. This documentation does not describe each method in details, delegating instead to many other good resources on the -subject. This section in particular explains the three core objects rascaline is +subject. This section in particular explains the three core objects featomic is built upon: systems, calculators and descriptors. .. figure:: ../../static/images/core-concepts.* - Schematic representations of the three core concepts in rascaline: systems, + Schematic representations of the three core concepts in featomic: systems, calculators and descriptors. The core operation provided by this library to compute the representation (associated with a given calculator) of one or multiple systems, getting the corresponding data in a descriptor. @@ -27,31 +27,31 @@ built upon: systems, calculators and descriptors. Systems: atoms and molecules ---------------------------- -Systems describe the input data rascaline uses to compute various +Systems describe the input data featomic uses to compute various representations. They contains information about the atomic positions, different atomic types, unit cell and periodicity, and are responsible for computing the neighbors of each atomic center. -Rascaline uses systems in a generic manner, and while it provides a default +Featomic uses systems in a generic manner, and while it provides a default implementation called ``SimpleSystem`` it is able to use data from any source by going through a few lines of adaptor code. This enables using it directly inside molecular simulation engines, re-using the neighbors list calculation done by the engine, when using machine learning force-fields in simulations. Both implementation and data related to systems are thus provided by users of -the rascaline library. +the featomic library. Calculators: computing representations -------------------------------------- -Calculators are provided by rascaline, and compute a single representations. +Calculators are provided by featomic, and compute a single representations. There is a calculator for the :ref:`sorted distances vector ` representation, one for the :ref:`spherical expansion ` representation, one for the :ref:`LODE spherical expansion ` representation, and hopefully soon many others. -All calculators are registered globally in rascaline, and can be constructed +All calculators are registered globally in featomic, and can be constructed with a name and a set of parameters (often called hyper-parameters). These parameters control the features of the final representation: how many are they, and what do they represent. All :ref:`available calculators ` @@ -65,7 +65,7 @@ Descriptors: data storage for atomistic machine learning After using a calculator on one or multiple systems, users will get the numerical representation of their atomic systems in a ``descriptor`` object. -Rascaline uses `metatensor`_ ``TensorMap`` type when returning descriptors. +Featomic uses `metatensor`_ ``TensorMap`` type when returning descriptors. .. _metatensor: https://lab-cosmo.github.io/metatensor/ diff --git a/docs/src/explanations/index.rst b/docs/src/explanations/index.rst index 77eb9ffa0..4015f7d86 100644 --- a/docs/src/explanations/index.rst +++ b/docs/src/explanations/index.rst @@ -4,8 +4,8 @@ Explanations ============ The explanation section discusses topics that broaden your knowledge of -rascaline. The theory behind the calculators and additional useful information -are found here to give you more clarity and understanding of what rascaline is +featomic. The theory behind the calculators and additional useful information +are found here to give you more clarity and understanding of what featomic is all about. .. toctree:: @@ -13,4 +13,4 @@ all about. concepts soap - rotation_adapted \ No newline at end of file + rotation_adapted diff --git a/docs/src/explanations/rotation_adapted.rst b/docs/src/explanations/rotation_adapted.rst index cdab00100..d1b1301af 100644 --- a/docs/src/explanations/rotation_adapted.rst +++ b/docs/src/explanations/rotation_adapted.rst @@ -85,7 +85,7 @@ One can also write the inverse of :ref:`(2) `: .. math:: :name: eq:cg_decoupling - \ket{l_1 m_1} \ket{l_2 m_2} = \sum_{l m} C_{m_1 m_2 m}^{l_1 l_2 l m} \ket{l_1 l_2 l m} + \ket{l_1 m_1} \ket{l_2 m_2} = \sum_{l m} C_{m_1 m_2 m}^{l_1 l_2 l} \ket{l_1 l_2 l m} that express the product of two rotation eigenstates in terms of one. This process is known as decoupling. @@ -111,4 +111,4 @@ density, and combining them with a CG iteration to a coupled basis, as in \frac{\delta_{\sigma, (-1)^{l_1 + l_2 + \lambda}}}{\sqrt{2 \lambda + 1}} \sum_{m} C_{m (\mu-m) \mu}^{l_1 l_2 \lambda} c_{n_1 l_1 m}^{i} c_{n_2 l_2 (\mu - m)}^{i} -where we have assumed real spherical harmonics coefficients. +where we have assumed complex spherical harmonics coefficients. diff --git a/docs/src/get-started/rascaline.rst b/docs/src/get-started/featomic.rst similarity index 77% rename from docs/src/get-started/rascaline.rst rename to docs/src/get-started/featomic.rst index 6ac49d995..284b06c5f 100644 --- a/docs/src/get-started/rascaline.rst +++ b/docs/src/get-started/featomic.rst @@ -1,17 +1,17 @@ -What is rascaline +What is featomic ================= -Rascaline is a library for the efficient computing of representations for +Featomic is a library for the efficient computing of representations for atomistic machine learning also called "descriptors" or "fingerprints". These representation can be used for atomistic machine learning (ML) models including ML potentials, visualization or similarity analysis. There exist several libraries able to compute such structural representations, -such as `DScribe`_, `QUIP`_, and many more. Rascaline tries to distinguish +such as `DScribe`_, `QUIP`_, and many more. Featomic tries to distinguish itself by focussing on speed and memory efficiency of the calculations, with the explicit goal of running molecular simulations with ML potentials. In particular, memory efficiency is achieved by using the `metatensor`_ to store the -structural representation. Additionally, rascaline is not limited to a single +structural representation. Additionally, featomic is not limited to a single representation but supports several: .. include:: ../../../README.rst diff --git a/docs/src/get-started/index.rst b/docs/src/get-started/index.rst index 7c29dfd7e..9f3db5ec5 100644 --- a/docs/src/get-started/index.rst +++ b/docs/src/get-started/index.rst @@ -3,11 +3,11 @@ Getting started =============== -The following sections describes how to install and start with using rascaline. +The following sections describes how to install and start with using featomic. .. toctree:: :maxdepth: 2 - rascaline + featomic installation tutorials diff --git a/docs/src/get-started/installation.rst b/docs/src/get-started/installation.rst index fc6e57cb0..910953c70 100644 --- a/docs/src/get-started/installation.rst +++ b/docs/src/get-started/installation.rst @@ -1,7 +1,7 @@ Installation ============ -You can install rascaline in different ways depending on which language you plan +You can install featomic in different ways depending on which language you plan to use it from. In all cases you will need a Rust compiler, which you can install using `rustup `_ or your OS package manager. @@ -11,7 +11,7 @@ Installing the Python module ---------------------------- For building and using the Python package, clone the repository using `git -`_ and install rascaline using `pip +`_ and install featomic using `pip `_. From source: @@ -21,21 +21,21 @@ From source: # Make sure you are using the latest version of pip pip install --upgrade pip - git clone https://github.com/Luthaf/rascaline - cd rascaline + git clone https://github.com/metatensor/featomic + cd featomic pip install . # alternatively, the same thing in a single command - pip install git+https://github.com/Luthaf/rascaline + pip install git+https://github.com/metatensor/featomic -Rascaline is also provided as prebuilt wheel which avoids the intermediate step +Featomic is also provided as prebuilt wheel which avoids the intermediate step of building the package with a Rust compiler from the source code. .. code-block:: bash pip install --upgrade pip - pip install --extra-index-url https://luthaf.fr/nightly-wheels/ rascaline + pip install --extra-index-url https://luthaf.fr/nightly-wheels/ featomic .. _install-c-lib: @@ -44,12 +44,12 @@ Installing the C/C++ library ---------------------------- This installs a C-compatible shared library that can also be called from C++, as -well as CMake files that can be used with ``find_package(rascaline)``. +well as CMake files that can be used with ``find_package(featomic)``. .. code-block:: bash - git clone https://github.com/Luthaf/rascaline - cd rascaline/rascaline-c-api + git clone https://github.com/metatensor/featomic + cd featomic/featomic mkdir build cd build cmake .. @@ -75,14 +75,11 @@ The build and installation can be configures with a few cmake options, using | BUILD_SHARED_LIBS | Default to installing and using a shared | ON | | | library instead of a static one | | +--------------------------------------+-----------------------------------------------+----------------+ -| RASCALINE_INSTALL_BOTH_STATIC_SHARED | Install both the shared and static version | ON | +| FEATOMIC_INSTALL_BOTH_STATIC_SHARED | Install both the shared and static version | ON | | | of the library | | +--------------------------------------+-----------------------------------------------+----------------+ -| RASCALINE_ENABLE_CHEMFILES | Enable the usage of chemfiles for reading | ON | -| | systems from files | | -+--------------------------------------+-----------------------------------------------+----------------+ -| RASCALINE_FETCH_METATENSOR | Automatically fetch, build and install | OFF | -| | metatensor (a dependency of rascaline) | | +| FEATOMIC_FETCH_METATENSOR | Automatically fetch, build and install | OFF | +| | metatensor (a dependency of featomic) | | +--------------------------------------+-----------------------------------------------+----------------+ Using the Rust library @@ -93,15 +90,7 @@ Add the following to your project ``Cargo.toml`` .. code-block:: toml [dependencies] - rascaline = {git = "https://github.com/Luthaf/rascaline"} - -Rascaline has one optional dependency (chemfiles), which is enabled by default. -If you want to disable it, you can use: - -.. code-block:: toml - - [dependencies] - rascaline = {git = "https://github.com/Luthaf/rascaline", default-features = false} + featomic = {git = "https://github.com/metatensor/featomic"} .. _install-torch-script: @@ -116,15 +105,15 @@ Building from source: .. code-block:: bash - git clone https://github.com/luthaf/rascaline - cd rascaline/python/rascaline-torch + git clone https://github.com/metatensor/featomic + cd featomic/python/featomic_torch pip install . # Make sure you are using the latest version of pip pip install --upgrade pip # alternatively, the same thing in a single command - pip install git+https://github.com/luthaf/rascaline#subdirectory=python/rascaline-torch + pip install git+https://github.com/metatensor/featomic#subdirectory=python/featomic_torch For usage from C++ @@ -132,8 +121,8 @@ For usage from C++ .. code-block:: bash - git clone https://github.com/lab-cosmo/rascaline - cd rascaline/rascaline-torch + git clone https://github.com/lab-cosmo/featomic + cd featomic/featomic-torch mkdir build && cd build cmake .. # configure cmake if needed @@ -151,11 +140,11 @@ dependencies: python -c "import torch; print(torch.utils.cmake_prefix_path)" -- :ref:`the C++ interface of rascaline `, which itself requires +- :ref:`the C++ interface of featomic `, which itself requires the `C++ interface of metatensor`_; - the `TorchScript interface of metatensor`_. We can download and build an appropriate version of it automatically by setting the cmake option - ``-DRASCALINE_TORCH_FETCH_METATENSOR_TORCH=ON`` + ``-DFEATOMIC_TORCH_FETCH_METATENSOR_TORCH=ON`` If any of these dependencies is not in a standard location, you should specify the installation directory when configuring cmake with ``CMAKE_PREFIX_PATH``. @@ -171,7 +160,7 @@ Other useful configuration options are: | CMAKE_PREFIX_PATH | ``;``-separated list of path where CMake will | | | | search for dependencies. | | +----------------------------------------+-----------------------------------------------+----------------+ -| RASCALINE_TORCH_FETCH_METATENSOR_TORCH | Should CMake automatically download and | OFF | +| FEATOMIC_TORCH_FETCH_METATENSOR_TORCH | Should CMake automatically download and | OFF | | | install metatensor-torch? | | +----------------------------------------+-----------------------------------------------+----------------+ diff --git a/docs/src/get-started/tutorials.rst b/docs/src/get-started/tutorials.rst index 5d9b92c3e..65b283ef7 100644 --- a/docs/src/get-started/tutorials.rst +++ b/docs/src/get-started/tutorials.rst @@ -1,13 +1,11 @@ .. _userdoc-tutorials: -Tutorials: using rascaline from Python +Tutorials: using featomic from Python ====================================== -The presented tutorials allow you to perform basic calculations in rascaline, -as well as understand the role of individual hyper parameters in calculations +The presented tutorial allow you to perform basic calculations in featomic .. toctree:: :maxdepth: 2 ../examples/first-calculation - ../examples/understanding-hypers diff --git a/docs/src/how-to/computing-soap.rst b/docs/src/how-to/computing-soap.rst index 0f6703822..bbc63da97 100644 --- a/docs/src/how-to/computing-soap.rst +++ b/docs/src/how-to/computing-soap.rst @@ -36,15 +36,15 @@ You can obtain a testing dataset from our :download:`website <../../static/datas .. group-tab:: Rust - .. literalinclude:: ../../../rascaline/examples/compute-soap.rs + .. literalinclude:: ../../../featomic/examples/compute-soap.rs :language: rust .. group-tab:: C++ - .. literalinclude:: ../../../rascaline-c-api/examples/compute-soap.cpp + .. literalinclude:: ../../../featomic/examples/compute-soap.cpp :language: c++ .. group-tab:: C - .. literalinclude:: ../../../rascaline-c-api/examples/compute-soap.c + .. literalinclude:: ../../../featomic/examples/compute-soap.c :language: c diff --git a/docs/src/how-to/index.rst b/docs/src/how-to/index.rst index f54490aed..e349cc897 100644 --- a/docs/src/how-to/index.rst +++ b/docs/src/how-to/index.rst @@ -4,7 +4,7 @@ How-to guides ============= How-to guides are like recipes when you are cooking. Equipped with basic -knowledge about rascaline, you can find key problems and use-cases here. If you +knowledge about featomic, you can find key problems and use-cases here. If you are a total beginner, you can go to :ref:`userdoc-get-started` section. .. toctree:: @@ -14,6 +14,5 @@ are a total beginner, you can go to :ref:`userdoc-get-started` section. sample-selection property-selection keys-selection - le-basis splined-radial-integral long-range diff --git a/docs/src/how-to/keys-selection.rst b/docs/src/how-to/keys-selection.rst index 19be8130c..140f44026 100644 --- a/docs/src/how-to/keys-selection.rst +++ b/docs/src/how-to/keys-selection.rst @@ -1,7 +1,7 @@ Keys selection ============== -This examples shows how to tell rascaline to compute a set of blocks which +This examples shows how to tell featomic to compute a set of blocks which correspond to predefined keys. This can be used to either reduce the set of computed blocks if we are only interested in some of them (skipping the calculation of the other blocks); or when we need to explicitly add some blocks diff --git a/docs/src/how-to/le-basis.rst b/docs/src/how-to/le-basis.rst deleted file mode 100644 index d37f75ff1..000000000 --- a/docs/src/how-to/le-basis.rst +++ /dev/null @@ -1,25 +0,0 @@ -.. _userdoc-how-to-le-basis: - -Laplacian eigenstate basis -========================== - -This examples shows how to calculate a spherical expansion using a Laplacian -eigenstate basis. - -.. tabs:: - - .. group-tab:: Python - - .. container:: sphx-glr-footer sphx-glr-footer-example - - .. container:: sphx-glr-download sphx-glr-download-python - - :download:`Download Python source code for this example: le-basis.py <../examples/le-basis.py>` - - .. container:: sphx-glr-download sphx-glr-download-jupyter - - :download:`Download Jupyter notebook for this example: le-basis.ipynb <../examples/le-basis.ipynb>` - - .. include:: ../examples/le-basis.rst - :start-after: start-body - :end-before: end-body diff --git a/docs/src/how-to/long-range.rst b/docs/src/how-to/long-range.rst index 327441980..c89b5bdc0 100644 --- a/docs/src/how-to/long-range.rst +++ b/docs/src/how-to/long-range.rst @@ -3,27 +3,29 @@ Long-range only LODE descriptor =============================== -The :py:class:`LodeSphericalExpansion ` allows the -calculation of a descriptor that includes all atoms within the system and projects them -onto a spherical expansion/ fingerprint within a given ``cutoff``. This is very useful -if long-range interactions between atoms are important to describe the physics and -chemistry of a collection of atoms. However, as stated the descriptor contains **ALL** -atoms of the system and sometimes it can be desired to only have a long-range/exterior -only descriptor that only includes the atoms outside a given cutoff. Sometimes there -descriptors are also denoted by far-field descriptors. - -A long range only descriptor can be particular useful when one already has a good -descriptor for the short-range density like (SOAP) and the long-range descriptor (far -field) should contain different information from what the short-range descriptor already -offers. - -Such descriptor can be constructed within `rascaline` as sketched by the image below. +The :py:class:`LodeSphericalExpansion ` allows +the calculation of a descriptor that includes all atoms within the system and +projects them onto a spherical expansion/ fingerprint within a given ``cutoff``. +This is very useful if long-range interactions between atoms are important to +describe the physics and chemistry of a collection of atoms. However, as stated +the descriptor contains **ALL** atoms of the system and sometimes it can be +desired to only have a long-range/exterior only descriptor that only includes +the atoms outside a given cutoff. Sometimes there descriptors are also denoted +by far-field descriptors. + +A long range only descriptor can be particular useful when one already has a +good descriptor for the short-range density like (SOAP) and the long-range +descriptor (far field) should contain different information from what the +short-range descriptor already offers. + +Such descriptor can be constructed within `featomic` as sketched by the image +below. .. figure:: ../../static/images/long-range-descriptor.* :align: center In this example will construct such a descriptor using the :ref:`radial integral -splining ` tools of `rascaline`. +splining ` tools of `featomic`. .. tabs:: diff --git a/docs/src/how-to/property-selection.rst b/docs/src/how-to/property-selection.rst index c4474ef37..44fcbe54d 100644 --- a/docs/src/how-to/property-selection.rst +++ b/docs/src/how-to/property-selection.rst @@ -4,7 +4,7 @@ Property Selection ================== This examples shows how to only compute a subset of properties for each sample -with a given rascaline representation. In particular, we will use the SOAP power +with a given featomic representation. In particular, we will use the SOAP power spectrum representation, and select the most significant features within a single block using farthest point sampling (FPS). We will run the calculation for all atomic systems stored in a file, the path to which should be given as diff --git a/docs/src/how-to/sample-selection.rst b/docs/src/how-to/sample-selection.rst index 07037257e..afe451462 100644 --- a/docs/src/how-to/sample-selection.rst +++ b/docs/src/how-to/sample-selection.rst @@ -10,7 +10,7 @@ argument. This can be useful if we are only interested in certain systems in a large dataset, or if we need to determine the effect of a certain type of atoms on some system properties. In the following, we will look at the tools with which -sample selection can be done in rascaline. +sample selection can be done in featomic. The first part of this example repeats the :ref:`userdoc-how-to-computing-soap`, so we suggest that you read it initially. diff --git a/docs/src/index.rst b/docs/src/index.rst index 85ecceee2..86ae002ed 100644 --- a/docs/src/index.rst +++ b/docs/src/index.rst @@ -1,7 +1,7 @@ -Overview of Rascaline's Documentation +Overview of Featomic's Documentation ===================================== -This documentation covers everything you need to know about rascaline. +This documentation covers everything you need to know about featomic. It comprises of the following five broad sections: - :ref:`userdoc-get-started` @@ -10,7 +10,7 @@ It comprises of the following five broad sections: - :ref:`userdoc-explanations` - :ref:`devdoc` -If you are new to rascaline we recommend starting with the +If you are new to featomic we recommend starting with the :ref:`userdoc-get-started` section. If you want to contribute to the development of the library please have a look at our :ref:`developer documentation `. @@ -20,32 +20,32 @@ Getting started --------------- If you are an absolute beginner, we recommend you to start with the get started -pages to familiarize yourself with rascaline and the rascaline ecosystem. +pages to familiarize yourself with featomic and the featomic ecosystem. How-to guides ------------- This section comprises of guides that will take you through series of steps -involved in addressing key problems and use-cases in rascaline. It requires -intermediate to advanced knowledge of how rascaline works. If you are an +involved in addressing key problems and use-cases in featomic. It requires +intermediate to advanced knowledge of how featomic works. If you are an absolute beginner, it is recommended you start from the :ref:`userdoc-get-started` section before going to the How to Guides. Reference guides ---------------- -The Reference Guide contains technical references for rascaline's implemented +The Reference Guide contains technical references for featomic's implemented calculators as well as the APIs. It describes the various functionalities -provided by rascaline. You can always refer to this section to learn more about -classes, functions, modules, and other aspects of rascaline's machinery you come +provided by featomic. You can always refer to this section to learn more about +classes, functions, modules, and other aspects of featomic's machinery you come across. Explanations ------------ The explanation section discusses key topics and concepts at a fairly high level -and provides useful explanations to expand your knowledge of rascaline. It -requires at least basic to intermediate knowledge of rascaline If you are an +and provides useful explanations to expand your knowledge of featomic. It +requires at least basic to intermediate knowledge of featomic If you are an absolute beginner, we recommend you start from the :ref:`userdoc-get-started` section of the documentation. @@ -53,7 +53,7 @@ Developer documentation ----------------------- The developer guide introduces the aspects of how contributing to the code base -or the documentation of rascaline. +or the documentation of featomic. .. toctree:: diff --git a/docs/src/references/api/c/calculators.rst b/docs/src/references/api/c/calculators.rst index da87fe8e2..edfced9e3 100644 --- a/docs/src/references/api/c/calculators.rst +++ b/docs/src/references/api/c/calculators.rst @@ -1,35 +1,35 @@ Dealing with calculators ======================== -.. doxygentypedef:: rascal_calculator_t +.. doxygentypedef:: featomic_calculator_t -The following functions operate on :c:type:`rascal_calculator_t`: +The following functions operate on :c:type:`featomic_calculator_t`: -- :c:func:`rascal_calculator`: create new calculators -- :c:func:`rascal_calculator_free`: free allocated calculators -- :c:func:`rascal_calculator_compute`: run the actual calculation -- :c:func:`rascal_calculator_name` get the name of a calculator -- :c:func:`rascal_calculator_parameters`: get the hyper-parameters of a calculator -- :c:func:`rascal_calculator_cutoffs`: get the cutoffs of a calculator +- :c:func:`featomic_calculator`: create new calculators +- :c:func:`featomic_calculator_free`: free allocated calculators +- :c:func:`featomic_calculator_compute`: run the actual calculation +- :c:func:`featomic_calculator_name` get the name of a calculator +- :c:func:`featomic_calculator_parameters`: get the hyper-parameters of a calculator +- :c:func:`featomic_calculator_cutoffs`: get the cutoffs of a calculator --------------------------------------------------------------------- -.. doxygenfunction:: rascal_calculator +.. doxygenfunction:: featomic_calculator -.. doxygenfunction:: rascal_calculator_free +.. doxygenfunction:: featomic_calculator_free -.. doxygenfunction:: rascal_calculator_compute +.. doxygenfunction:: featomic_calculator_compute -.. doxygenfunction:: rascal_calculator_name +.. doxygenfunction:: featomic_calculator_name -.. doxygenfunction:: rascal_calculator_parameters +.. doxygenfunction:: featomic_calculator_parameters -.. doxygenfunction:: rascal_calculator_cutoffs +.. doxygenfunction:: featomic_calculator_cutoffs --------------------------------------------------------------------- -.. doxygenstruct:: rascal_calculation_options_t +.. doxygenstruct:: featomic_calculation_options_t :members: -.. doxygenstruct:: rascal_labels_selection_t +.. doxygenstruct:: featomic_labels_selection_t :members: diff --git a/docs/src/references/api/c/index.rst b/docs/src/references/api/c/index.rst index b095c5bd2..735e7994f 100644 --- a/docs/src/references/api/c/index.rst +++ b/docs/src/references/api/c/index.rst @@ -3,29 +3,29 @@ C API reference =============== -Rascaline offers a C API that can be called from any language able to call C +Featomic offers a C API that can be called from any language able to call C functions (in particular, this includes Python, Fortran with ``iso_c_env``, C++, and most languages used nowadays). Convenient wrappers of the C API are also provided for :ref:`Python ` users. -The C API is implemented in Rust, in the ``rascaline-c-api`` crate. You can use -these functions in your own code by :ref:`installing the corresponding shared -library and header `, and then including ``rascaline.h`` and -linking with ``-lrascaline``. Alternatively, we provide a cmake package config -file, allowing you to do use rascaline like this (after installation): +You can use these functions in your own code by :ref:`installing the +corresponding shared library and header `, and then including +``featomic.h`` and linking with ``-lfeatomic``. Alternatively, we provide a +cmake package config file, allowing you to do use featomic like this (after +installation): .. code-block:: cmake - find_package(rascaline) + find_package(featomic) # add executables/libraries add_executable(MyExecutable my_sources.c) add_library(MyLibrary my_sources.c) - # Link to rascaline, this makes the header accessible - target_link_libraries(MyExecutable rascaline) + # Link to featomic, this makes the header accessible + target_link_libraries(MyExecutable featomic) -The functions and types provided in ``rascaline.h`` can be grouped in three main groups: +The functions and types provided in ``featomic.h`` can be grouped in three main groups: .. toctree:: :maxdepth: 1 diff --git a/docs/src/references/api/c/misc.rst b/docs/src/references/api/c/misc.rst index da503136b..6113037ae 100644 --- a/docs/src/references/api/c/misc.rst +++ b/docs/src/references/api/c/misc.rst @@ -4,48 +4,46 @@ Miscellaneous Error handling -------------- -.. doxygenfunction:: rascal_last_error +.. doxygenfunction:: featomic_last_error -.. doxygentypedef:: rascal_status_t +.. doxygentypedef:: featomic_status_t -.. doxygendefine:: RASCAL_SUCCESS +.. doxygendefine:: FEATOMIC_SUCCESS -.. doxygendefine:: RASCAL_INVALID_PARAMETER_ERROR +.. doxygendefine:: FEATOMIC_INVALID_PARAMETER_ERROR -.. doxygendefine:: RASCAL_JSON_ERROR +.. doxygendefine:: FEATOMIC_JSON_ERROR -.. doxygendefine:: RASCAL_UTF8_ERROR +.. doxygendefine:: FEATOMIC_UTF8_ERROR -.. doxygendefine:: RASCAL_CHEMFILES_ERROR +.. doxygendefine:: FEATOMIC_SYSTEM_ERROR -.. doxygendefine:: RASCAL_SYSTEM_ERROR - -.. doxygendefine:: RASCAL_INTERNAL_ERROR +.. doxygendefine:: FEATOMIC_INTERNAL_ERROR .. _c-api-logging: Logging ------- -.. doxygenfunction:: rascal_set_logging_callback +.. doxygenfunction:: featomic_set_logging_callback -.. doxygentypedef:: rascal_logging_callback_t +.. doxygentypedef:: featomic_logging_callback_t -.. doxygendefine:: RASCAL_LOG_LEVEL_ERROR +.. doxygendefine:: FEATOMIC_LOG_LEVEL_ERROR -.. doxygendefine:: RASCAL_LOG_LEVEL_WARN +.. doxygendefine:: FEATOMIC_LOG_LEVEL_WARN -.. doxygendefine:: RASCAL_LOG_LEVEL_INFO +.. doxygendefine:: FEATOMIC_LOG_LEVEL_INFO -.. doxygendefine:: RASCAL_LOG_LEVEL_DEBUG +.. doxygendefine:: FEATOMIC_LOG_LEVEL_DEBUG -.. doxygendefine:: RASCAL_LOG_LEVEL_TRACE +.. doxygendefine:: FEATOMIC_LOG_LEVEL_TRACE Profiling --------- -.. doxygenfunction:: rascal_profiling_enable +.. doxygenfunction:: featomic_profiling_enable -.. doxygenfunction:: rascal_profiling_clear +.. doxygenfunction:: featomic_profiling_clear -.. doxygenfunction:: rascal_profiling_get +.. doxygenfunction:: featomic_profiling_get diff --git a/docs/src/references/api/c/systems.rst b/docs/src/references/api/c/systems.rst index 7223fe5e0..6e1fecfec 100644 --- a/docs/src/references/api/c/systems.rst +++ b/docs/src/references/api/c/systems.rst @@ -2,21 +2,15 @@ Defining systems ================ There are two ways you can define systems to pass to -:c:func:`rascal_calculator_compute`: the easy way is to use -:c:func:`rascal_basic_systems_read` to read all systems defined in a file, and +:c:func:`featomic_calculator_compute`: the easy way is to use +:c:func:`featomic_basic_systems_read` to read all systems defined in a file, and run the calculation on all these systems. The more complex but also more -flexible way is to create a :c:struct:`rascal_system_t` manually, implementing +flexible way is to create a :c:struct:`featomic_system_t` manually, implementing all required functions; and then passing one or more systems to -:c:func:`rascal_calculator_compute`. +:c:func:`featomic_calculator_compute`. -.. doxygenstruct:: rascal_system_t +.. doxygenstruct:: featomic_system_t :members: -.. doxygenstruct:: rascal_pair_t +.. doxygenstruct:: featomic_pair_t :members: - ---------------------------------------------------------------------- - -.. doxygenfunction:: rascal_basic_systems_read - -.. doxygenfunction:: rascal_basic_systems_free diff --git a/docs/src/references/api/cxx/calculators.rst b/docs/src/references/api/cxx/calculators.rst index 94843df34..d3c66483d 100644 --- a/docs/src/references/api/cxx/calculators.rst +++ b/docs/src/references/api/cxx/calculators.rst @@ -1,11 +1,11 @@ Dealing with calculators ======================== -.. doxygenclass:: rascaline::Calculator +.. doxygenclass:: featomic::Calculator :members: -.. doxygenclass:: rascaline::CalculationOptions +.. doxygenclass:: featomic::CalculationOptions :members: -.. doxygenclass:: rascaline::LabelsSelection +.. doxygenclass:: featomic::LabelsSelection :members: diff --git a/docs/src/references/api/cxx/index.rst b/docs/src/references/api/cxx/index.rst index 602cfc50e..02e566824 100644 --- a/docs/src/references/api/cxx/index.rst +++ b/docs/src/references/api/cxx/index.rst @@ -3,25 +3,25 @@ C++ API reference ================= -Rascaline offers a C++ API, built on top of the :ref:`C API `. +Featomic offers a C++ API, built on top of the :ref:`C API `. You can the provided classes and functions in your own code by :ref:`installing the corresponding shared library and header `, and then including -``rascaline.hpp`` and linking with ``-lrascaline``. Alternatively, we provide a -cmake package config file, allowing you to do use rascaline like this (after +``featomic.hpp`` and linking with ``-lfeatomic``. Alternatively, we provide a +cmake package config file, allowing you to do use featomic like this (after installation): .. code-block:: cmake - find_package(rascaline) + find_package(featomic) # add executables/libraries add_executable(MyExecutable my_sources.cxx) add_library(MyLibrary my_sources.cxx) - # Link to rascaline, this makes the header accessible - target_link_libraries(MyExecutable rascaline) + # Link to featomic, this makes the header accessible + target_link_libraries(MyExecutable featomic) -The functions and types provided in ``rascaline.hpp`` can be grouped in three main groups: +The functions and types provided in ``featomic.hpp`` can be grouped in three main groups: .. toctree:: :maxdepth: 1 diff --git a/docs/src/references/api/cxx/misc.rst b/docs/src/references/api/cxx/misc.rst index a8271594d..da347dd14 100644 --- a/docs/src/references/api/cxx/misc.rst +++ b/docs/src/references/api/cxx/misc.rst @@ -3,8 +3,8 @@ Miscellaneous Logging should be handled using the :ref:`C API ` functions. -.. doxygenclass:: rascaline::RascalineError +.. doxygenclass:: featomic::FeatomicError :members: -.. doxygenclass:: rascaline::Profiler +.. doxygenclass:: featomic::Profiler :members: diff --git a/docs/src/references/api/cxx/systems.rst b/docs/src/references/api/cxx/systems.rst index 110624e4a..4ad9f6079 100644 --- a/docs/src/references/api/cxx/systems.rst +++ b/docs/src/references/api/cxx/systems.rst @@ -1,16 +1,9 @@ Defining systems ================ -There are two ways you can define systems to pass to a -:cpp:class:`rascaline::Calculator`. The easy way is to use -:cpp:class:`rascaline::BasicSystems` to read all systems defined in a file, and -run the calculation on all these systems. The more complex but also more -flexible way is to define a new child class of :cpp:class:`rascaline::System` -implementing all required functions; and then passing a vector of pointers to -the child class instances to your :cpp:class:`rascaline::Calculator`. -.. doxygenclass:: rascaline::System +.. doxygenclass:: featomic::System :members: -.. doxygenclass:: rascaline::BasicSystems +.. doxygenclass:: featomic::SimpleSystem :members: diff --git a/docs/src/references/api/index.rst b/docs/src/references/api/index.rst index 3548e5c30..9f5e2ba96 100644 --- a/docs/src/references/api/index.rst +++ b/docs/src/references/api/index.rst @@ -2,7 +2,7 @@ API documentation ----------------- The API documentation contains all the information about functions and classes -you'll need to use rascaline in your own code. This section is separated +you'll need to use featomic in your own code. This section is separated by programming languages. .. toctree:: diff --git a/docs/src/references/api/python/basis.rst b/docs/src/references/api/python/basis.rst new file mode 100644 index 000000000..198b086df --- /dev/null +++ b/docs/src/references/api/python/basis.rst @@ -0,0 +1,31 @@ +Basis functions +=============== + +.. autoclass:: featomic.basis.TensorProduct + :show-inheritance: + +.. autoclass:: featomic.basis.Explicit + :show-inheritance: + +.. autoclass:: featomic.basis.LaplacianEigenstate + :show-inheritance: + +.. autoclass:: featomic.basis.ExpansionBasis + :members: + +.. _python-radial-basis: + +Radial basis functions +---------------------- + +.. autoclass:: featomic.basis.Gto + :show-inheritance: + +.. autoclass:: featomic.basis.Monomials + :show-inheritance: + +.. autoclass:: featomic.basis.SphericalBessel + :show-inheritance: + +.. autoclass:: featomic.basis.RadialBasis + :members: diff --git a/docs/src/references/api/python/calculators.rst b/docs/src/references/api/python/calculators.rst index 56114a93e..da5b89457 100644 --- a/docs/src/references/api/python/calculators.rst +++ b/docs/src/references/api/python/calculators.rst @@ -1,46 +1,46 @@ Available Calculators ===================== -.. autoclass:: rascaline.calculators.CalculatorBase +.. autoclass:: featomic.calculators.CalculatorBase :members: -------------------------------------------------------------------------------- -.. autoclass:: rascaline.AtomicComposition +.. autoclass:: featomic.AtomicComposition :members: :show-inheritance: -.. autoclass:: rascaline.NeighborList +.. autoclass:: featomic.NeighborList :members: :show-inheritance: -.. autoclass:: rascaline.SortedDistances +.. autoclass:: featomic.SortedDistances :members: :show-inheritance: -.. autoclass:: rascaline.SphericalExpansion +.. autoclass:: featomic.SphericalExpansion :members: :show-inheritance: -.. autoclass:: rascaline.SphericalExpansionByPair +.. autoclass:: featomic.SphericalExpansionByPair :members: :show-inheritance: -.. autoclass:: rascaline.SoapRadialSpectrum +.. autoclass:: featomic.SoapRadialSpectrum :members: :show-inheritance: -.. autoclass:: rascaline.SoapPowerSpectrum +.. autoclass:: featomic.SoapPowerSpectrum :members: :show-inheritance: -.. autoclass:: rascaline.LodeSphericalExpansion +.. autoclass:: featomic.LodeSphericalExpansion :members: :show-inheritance: diff --git a/docs/src/references/api/python/clebsch-gordan.rst b/docs/src/references/api/python/clebsch-gordan.rst new file mode 100644 index 000000000..1fc3b1edc --- /dev/null +++ b/docs/src/references/api/python/clebsch-gordan.rst @@ -0,0 +1,22 @@ +Clebsch-Gordan products +======================= + +.. autoclass:: featomic.clebsch_gordan.EquivariantPowerSpectrum + :members: + +.. autoclass:: featomic.clebsch_gordan.PowerSpectrum + :members: + +.. autoclass:: featomic.clebsch_gordan.DensityCorrelations + :members: + +.. autofunction:: featomic.clebsch_gordan.cartesian_to_spherical + + +Low-level functionalities +------------------------- + +.. autoclass:: featomic.clebsch_gordan.ClebschGordanProduct + :members: + +.. autofunction:: featomic.clebsch_gordan.calculate_cg_coefficients diff --git a/docs/src/references/api/python/cutoff.rst b/docs/src/references/api/python/cutoff.rst new file mode 100644 index 000000000..cafb23e94 --- /dev/null +++ b/docs/src/references/api/python/cutoff.rst @@ -0,0 +1,14 @@ +Local environments +================== + +.. autoclass:: featomic.cutoff.Cutoff + :members: + +.. autoclass:: featomic.cutoff.ShiftedCosine + :show-inheritance: + +.. autoclass:: featomic.cutoff.Step + :show-inheritance: + +.. autoclass:: featomic.cutoff.SmoothingFunction + :members: diff --git a/docs/src/references/api/python/density.rst b/docs/src/references/api/python/density.rst new file mode 100644 index 000000000..b374c96b9 --- /dev/null +++ b/docs/src/references/api/python/density.rst @@ -0,0 +1,24 @@ +.. _python-atomic-density: + +Atomic density +============== + +.. autoclass:: featomic.density.DiracDelta + :show-inheritance: + +.. autoclass:: featomic.density.Gaussian + :show-inheritance: + +.. autoclass:: featomic.density.SmearedPowerLaw + :show-inheritance: + +.. autoclass:: featomic.density.AtomicDensity + :members: + +------ + +.. autoclass:: featomic.density.Willatt2018 + :show-inheritance: + +.. autoclass:: featomic.density.RadialScaling + :members: diff --git a/docs/src/references/api/python/index.rst b/docs/src/references/api/python/index.rst index b2e76a538..6975e3a30 100644 --- a/docs/src/references/api/python/index.rst +++ b/docs/src/references/api/python/index.rst @@ -3,15 +3,21 @@ Python API reference ==================== -Most users will find the Python interface to rascaline to be the most convenient +Most users will find the Python interface to featomic to be the most convenient to use. This interface is built on top of the C API, and can be :ref:`installed independently `. The functions and classes provided in -``rascaline`` can be grouped in three main groups: +``featomic`` can be grouped in three main groups: .. toctree:: :maxdepth: 1 systems calculators - utils/index misc + + cutoff + density + basis + splines + + clebsch-gordan diff --git a/docs/src/references/api/python/misc.rst b/docs/src/references/api/python/misc.rst index 5475d1cc8..f59a65a1b 100644 --- a/docs/src/references/api/python/misc.rst +++ b/docs/src/references/api/python/misc.rst @@ -1,15 +1,19 @@ Miscellaneous ============= -.. autoclass:: rascaline.RascalError +.. autoclass:: featomic.FeatomicError :members: :undoc-members: -.. autofunction:: rascaline.set_logging_callback +.. autofunction:: featomic.set_logging_callback -.. autofunction:: rascaline.log.default_logging_callback +.. autofunction:: featomic.log.default_logging_callback -.. autoclass:: rascaline.Profiler +.. autoclass:: featomic.Profiler :members: :undoc-members: + +.. autofunction:: featomic.convert_hypers + +.. autofunction:: featomic.hypers_to_json diff --git a/docs/src/references/api/python/splines.rst b/docs/src/references/api/python/splines.rst new file mode 100644 index 000000000..432d0548e --- /dev/null +++ b/docs/src/references/api/python/splines.rst @@ -0,0 +1,21 @@ +.. _python-utils-splines: + + +Splined radial integrals +======================== + +The classes presented here can take arbitrary radial basis function and density; +and compute the radial integral that enters many density-based representations +such as SOAP and LODE. This enables using arbitrary, user-defined basis +functions and density with the existing calculators. Both classes require +`scipy`_ to be installed in order to perform the numerical integrals. + + +.. autoclass:: featomic.splines.SoapSpliner + :members: + +.. autoclass:: featomic.splines.LodeSpliner + :members: + + +.. _`scipy`: https://scipy.org diff --git a/docs/src/references/api/python/systems.rst b/docs/src/references/api/python/systems.rst index 5ddb24e30..458dcf7ec 100644 --- a/docs/src/references/api/python/systems.rst +++ b/docs/src/references/api/python/systems.rst @@ -1,13 +1,13 @@ Available system implementation =============================== -.. autoclass:: rascaline.IntoSystem +.. autoclass:: featomic.IntoSystem -.. autofunction:: rascaline.systems.wrap_system +.. autofunction:: featomic.systems.wrap_system -.. autoclass:: rascaline.systems.AseSystem +.. autoclass:: featomic.systems.AseSystem -.. autoclass:: rascaline.systems.ChemfilesSystem +.. autoclass:: featomic.systems.ChemfilesSystem -.. autoclass:: rascaline.SystemBase +.. autoclass:: featomic.SystemBase :members: diff --git a/docs/src/references/api/python/utils/atomic-density.rst b/docs/src/references/api/python/utils/atomic-density.rst deleted file mode 100644 index 4c0fc5591..000000000 --- a/docs/src/references/api/python/utils/atomic-density.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: rascaline.utils.splines.atomic_density diff --git a/docs/src/references/api/python/utils/clebsch-gordan.rst b/docs/src/references/api/python/utils/clebsch-gordan.rst deleted file mode 100644 index 61f1b2f16..000000000 --- a/docs/src/references/api/python/utils/clebsch-gordan.rst +++ /dev/null @@ -1,17 +0,0 @@ -Clebsch-Gordan products -======================= - -.. autoclass:: rascaline.utils.DensityCorrelations - :members: - - -.. autofunction:: rascaline.utils.cartesian_to_spherical - - -Low-level functionalities -------------------------- - -.. autoclass:: rascaline.utils.ClebschGordanProduct - :members: - -.. autofunction:: rascaline.utils.calculate_cg_coefficients diff --git a/docs/src/references/api/python/utils/index.rst b/docs/src/references/api/python/utils/index.rst deleted file mode 100644 index d713fdcb9..000000000 --- a/docs/src/references/api/python/utils/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -Utils -===== - -Utility functions and classes that extend the core usage of rascaline. - - -.. toctree:: - :maxdepth: 1 - - atomic-density - radial-basis - power-spectrum - splines - clebsch-gordan diff --git a/docs/src/references/api/python/utils/power-spectrum.rst b/docs/src/references/api/python/utils/power-spectrum.rst deleted file mode 100644 index 488f1559e..000000000 --- a/docs/src/references/api/python/utils/power-spectrum.rst +++ /dev/null @@ -1,6 +0,0 @@ -PowerSpectrum -============= - -.. autoclass:: rascaline.utils.PowerSpectrum - :members: - :show-inheritance: diff --git a/docs/src/references/api/python/utils/radial-basis.rst b/docs/src/references/api/python/utils/radial-basis.rst deleted file mode 100644 index fa8bacf62..000000000 --- a/docs/src/references/api/python/utils/radial-basis.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: rascaline.utils.splines.radial_basis diff --git a/docs/src/references/api/python/utils/splines.rst b/docs/src/references/api/python/utils/splines.rst deleted file mode 100644 index 23c48c54b..000000000 --- a/docs/src/references/api/python/utils/splines.rst +++ /dev/null @@ -1,3 +0,0 @@ -.. _python-utils-splines: - -.. automodule:: rascaline.utils.splines.splines diff --git a/docs/src/references/api/rust.rst b/docs/src/references/api/rust.rst index 3c0d1981e..2cc2985e2 100644 --- a/docs/src/references/api/rust.rst +++ b/docs/src/references/api/rust.rst @@ -2,4 +2,4 @@ Rust API reference ================== For the Rust documention please use the -`generated rustdoc documentation `_. +`generated rustdoc documentation `_. diff --git a/docs/src/references/api/torch/calculators.rst b/docs/src/references/api/torch/calculators.rst index b536ef073..cb7e2434d 100644 --- a/docs/src/references/api/torch/calculators.rst +++ b/docs/src/references/api/torch/calculators.rst @@ -1,48 +1,48 @@ Calculators =========== -.. autoclass:: rascaline.torch.CalculatorModule +.. autoclass:: featomic.torch.CalculatorModule :members: -.. autofunction:: rascaline.torch.register_autograd +.. autofunction:: featomic.torch.register_autograd -------------------------------------------------------------------------------- -.. autoclass:: rascaline.torch.AtomicComposition +.. autoclass:: featomic.torch.AtomicComposition :members: :show-inheritance: -.. autoclass:: rascaline.torch.NeighborList +.. autoclass:: featomic.torch.NeighborList :members: :show-inheritance: -.. autoclass:: rascaline.torch.SortedDistances +.. autoclass:: featomic.torch.SortedDistances :members: :show-inheritance: -.. autoclass:: rascaline.torch.SphericalExpansion +.. autoclass:: featomic.torch.SphericalExpansion :members: :show-inheritance: -.. autoclass:: rascaline.torch.SphericalExpansionByPair +.. autoclass:: featomic.torch.SphericalExpansionByPair :members: :show-inheritance: -.. autoclass:: rascaline.torch.SoapRadialSpectrum +.. autoclass:: featomic.torch.SoapRadialSpectrum :members: :show-inheritance: -.. autoclass:: rascaline.torch.SoapPowerSpectrum +.. autoclass:: featomic.torch.SoapPowerSpectrum :members: :show-inheritance: -.. autoclass:: rascaline.torch.LodeSphericalExpansion +.. autoclass:: featomic.torch.LodeSphericalExpansion :members: :show-inheritance: diff --git a/docs/src/references/api/torch/clebsch-gordan.rst b/docs/src/references/api/torch/clebsch-gordan.rst new file mode 100644 index 000000000..fdbfad297 --- /dev/null +++ b/docs/src/references/api/torch/clebsch-gordan.rst @@ -0,0 +1,19 @@ +Clebsch-Gordan products +======================= + +.. autoclass:: featomic.torch.clebsch_gordan.PowerSpectrum + :members: + +.. autoclass:: featomic.torch.clebsch_gordan.DensityCorrelations + :members: + +.. autofunction:: featomic.torch.clebsch_gordan.cartesian_to_spherical + + +Low-level functionalities +------------------------- + +.. autoclass:: featomic.torch.clebsch_gordan.ClebschGordanProduct + :members: + +.. autofunction:: featomic.torch.clebsch_gordan.calculate_cg_coefficients diff --git a/docs/src/references/api/torch/cxx/calculators.rst b/docs/src/references/api/torch/cxx/calculators.rst index 3da1641dd..e7310dc6d 100644 --- a/docs/src/references/api/torch/cxx/calculators.rst +++ b/docs/src/references/api/torch/cxx/calculators.rst @@ -1,10 +1,10 @@ Calculators =========== -.. doxygentypedef:: rascaline_torch::TorchCalculator +.. doxygentypedef:: featomic_torch::TorchCalculator -.. doxygenclass:: rascaline_torch::CalculatorHolder +.. doxygenclass:: featomic_torch::CalculatorHolder :members: -.. doxygenfunction:: rascaline_torch::register_autograd +.. doxygenfunction:: featomic_torch::register_autograd diff --git a/docs/src/references/api/torch/index.rst b/docs/src/references/api/torch/index.rst index 401dc70b7..17752b68c 100644 --- a/docs/src/references/api/torch/index.rst +++ b/docs/src/references/api/torch/index.rst @@ -3,12 +3,12 @@ TorchScript API reference ========================= -.. py:currentmodule:: rascaline.torch +.. py:currentmodule:: featomic.torch -We provide a PyTorch C++ extension to make rascaline compatible with Torch and +We provide a PyTorch C++ extension to make featomic compatible with Torch and TorchScript in three ways: -- registering rascaline calculators as special nodes in Torch's computational +- registering featomic calculators as special nodes in Torch's computational graph, allowing to use backward propagation of derivatives to compute gradients of arbitrary quantities with respect to atomic positions and cell (e.g. forces and stress when the quantity is the energy of a system); @@ -20,18 +20,18 @@ TorchScript in three ways: Please refer to the :ref:`installation instructions ` to know how to install the Python and C++ sides of this library. The core classes -of rascaline are documented below for an usage from Python: +of featomic are documented below for an usage from Python: .. toctree:: :maxdepth: 1 systems calculators - utils/index + clebsch-gordan -------------------------------------------------------------------------------- -If you want to use rascaline's TorchScript API from C++, you might be interested +If you want to use featomic's TorchScript API from C++, you might be interested in the following documentation: .. toctree:: diff --git a/docs/src/references/api/torch/systems.rst b/docs/src/references/api/torch/systems.rst index 1b328eb22..5d3a5fdd1 100644 --- a/docs/src/references/api/torch/systems.rst +++ b/docs/src/references/api/torch/systems.rst @@ -1,10 +1,10 @@ System ====== -Instead of a custom ``System`` class, ``rascaline-torch`` uses the class defined +Instead of a custom ``System`` class, ``featomic-torch`` uses the class defined by metatensor's atomistic models facilities: -:py:class:`metatensor.torch.atomistic.System`. Rascaline provides converters +:py:class:`metatensor.torch.atomistic.System`. Featomic provides converters from all the supported system providers (i.e. everything in -:py:class:`rascaline.IntoSystem`) to the TorchScript compatible ``System``. +:py:class:`featomic.IntoSystem`) to the TorchScript compatible ``System``. -.. autofunction:: rascaline.torch.systems_to_torch +.. autofunction:: featomic.torch.systems_to_torch diff --git a/docs/src/references/api/torch/utils/clebsch-gordan.rst b/docs/src/references/api/torch/utils/clebsch-gordan.rst deleted file mode 100644 index a8cb5299a..000000000 --- a/docs/src/references/api/torch/utils/clebsch-gordan.rst +++ /dev/null @@ -1,5 +0,0 @@ -Clebsch-Gordan products -======================= - -.. autoclass:: rascaline.torch.utils.DensityCorrelations - :members: diff --git a/docs/src/references/api/torch/utils/index.rst b/docs/src/references/api/torch/utils/index.rst deleted file mode 100644 index a2cf425eb..000000000 --- a/docs/src/references/api/torch/utils/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -Utils -===== - -Utility functions and classes that extend the core usage of rascaline-torch - - -.. toctree:: - :maxdepth: 1 - - power-spectrum - clebsch-gordan diff --git a/docs/src/references/api/torch/utils/power-spectrum.rst b/docs/src/references/api/torch/utils/power-spectrum.rst deleted file mode 100644 index 12633c552..000000000 --- a/docs/src/references/api/torch/utils/power-spectrum.rst +++ /dev/null @@ -1,6 +0,0 @@ -PowerSpectrum -============= - -.. autoclass:: rascaline.torch.utils.PowerSpectrum - :members: - :show-inheritance: diff --git a/docs/src/references/calculators/atomic-composition.rst b/docs/src/references/calculators/atomic-composition.rst index 697009b1b..4ea81cb57 100644 --- a/docs/src/references/calculators/atomic-composition.rst +++ b/docs/src/references/calculators/atomic-composition.rst @@ -5,4 +5,4 @@ Atomic Composition This calculator is registered with the ``atomic_composition`` name. -.. rascaline-json-schema:: build/json-schemas/AtomicComposition.json +.. featomic-json-schema:: build/json-schemas/AtomicComposition.json diff --git a/docs/src/references/calculators/index.rst b/docs/src/references/calculators/index.rst index 65ac43ca0..0e649b256 100644 --- a/docs/src/references/calculators/index.rst +++ b/docs/src/references/calculators/index.rst @@ -4,7 +4,7 @@ Calculator reference -------------------- Below is a list of all calculators available. Calculators are the core of -rascaline and are algorithms for transforming Cartesian coordinates into +featomic and are algorithms for transforming Cartesian coordinates into representations suitable for machine learning. Each calculators has a different approach of this transformation but some belong to the same family. To learn more about these connections and the theory you may consider our @@ -12,14 +12,14 @@ more about these connections and the theory you may consider our Each calculators is registered globally with a name (specified in the corresponding documentation page) that can be used to construct this calculator -with ``Calculator::new`` in Rust, ``rascal_calculator`` in C or -``rascaline::Calculator`` in C++. The hyper-parameters of the calculator must be +with ``Calculator::new`` in Rust, ``featomic_calculator`` in C or +``featomic::Calculator`` in C++. The hyper-parameters of the calculator must be given as a JSON formatted string. The possible fields in the JSON are documented as a `JSON schema`_, and rendered in the pages below. After the initialization of each calculator the computation of the representations is performed using the -:py:func:`rascaline.calculators.CalculatorBase.compute()` method. +:py:func:`featomic.calculators.CalculatorBase.compute()` method. .. _JSON schema: https://json-schema.org/ diff --git a/docs/src/references/calculators/lode-spherical-expansion.rst b/docs/src/references/calculators/lode-spherical-expansion.rst index c5aaa8c80..86aa5e294 100644 --- a/docs/src/references/calculators/lode-spherical-expansion.rst +++ b/docs/src/references/calculators/lode-spherical-expansion.rst @@ -5,4 +5,4 @@ LODE spherical expansion This calculator is registered with the ``lode_spherical_expansion`` name. -.. rascaline-json-schema:: build/json-schemas/LodeSphericalExpansion.json +.. featomic-json-schema:: build/json-schemas/LodeSphericalExpansion.json diff --git a/docs/src/references/calculators/neighbor-list.rst b/docs/src/references/calculators/neighbor-list.rst index 8f7020917..4c05b475b 100644 --- a/docs/src/references/calculators/neighbor-list.rst +++ b/docs/src/references/calculators/neighbor-list.rst @@ -3,4 +3,4 @@ Neighbor List This calculator is registered with the ``neighbor_list`` name. -.. rascaline-json-schema:: build/json-schemas/NeighborList.json +.. featomic-json-schema:: build/json-schemas/NeighborList.json diff --git a/docs/src/references/calculators/soap-power-spectrum.rst b/docs/src/references/calculators/soap-power-spectrum.rst index b94e9abf2..389bbccf6 100644 --- a/docs/src/references/calculators/soap-power-spectrum.rst +++ b/docs/src/references/calculators/soap-power-spectrum.rst @@ -5,4 +5,4 @@ SOAP power spectrum This calculator is registered with the ``soap_power_spectrum`` name. -.. rascaline-json-schema:: build/json-schemas/SoapPowerSpectrum.json +.. featomic-json-schema:: build/json-schemas/SoapPowerSpectrum.json diff --git a/docs/src/references/calculators/soap-radial-spectrum.rst b/docs/src/references/calculators/soap-radial-spectrum.rst index de9e75575..6cc6d9d5b 100644 --- a/docs/src/references/calculators/soap-radial-spectrum.rst +++ b/docs/src/references/calculators/soap-radial-spectrum.rst @@ -5,4 +5,4 @@ SOAP radial spectrum This calculator is registered with the ``soap_radial_spectrum`` name. -.. rascaline-json-schema:: build/json-schemas/SoapRadialSpectrum.json +.. featomic-json-schema:: build/json-schemas/SoapRadialSpectrum.json diff --git a/docs/src/references/calculators/sorted-distances.rst b/docs/src/references/calculators/sorted-distances.rst index 66f40a3ac..e47bd19b9 100644 --- a/docs/src/references/calculators/sorted-distances.rst +++ b/docs/src/references/calculators/sorted-distances.rst @@ -7,4 +7,4 @@ Sorted distance vector This calculator is registered with the ``sorted_distances`` name. -.. rascaline-json-schema:: build/json-schemas/SortedDistances.json +.. featomic-json-schema:: build/json-schemas/SortedDistances.json diff --git a/docs/src/references/calculators/spherical-expansion-by-pair.rst b/docs/src/references/calculators/spherical-expansion-by-pair.rst index 0225c7703..6ae6f0d49 100644 --- a/docs/src/references/calculators/spherical-expansion-by-pair.rst +++ b/docs/src/references/calculators/spherical-expansion-by-pair.rst @@ -5,4 +5,4 @@ Spherical expansion by pair This calculator is registered with the ``spherical_expansion_by_pair`` name. -.. rascaline-json-schema:: build/json-schemas/SphericalExpansionByPair.json +.. featomic-json-schema:: build/json-schemas/SphericalExpansionByPair.json diff --git a/docs/src/references/calculators/spherical-expansion.rst b/docs/src/references/calculators/spherical-expansion.rst index 5d73034b2..cba7e9eb7 100644 --- a/docs/src/references/calculators/spherical-expansion.rst +++ b/docs/src/references/calculators/spherical-expansion.rst @@ -5,4 +5,4 @@ Spherical expansion This calculator is registered with the ``spherical_expansion`` name. -.. rascaline-json-schema:: build/json-schemas/SphericalExpansion.json +.. featomic-json-schema:: build/json-schemas/SphericalExpansion.json diff --git a/docs/static/rascaline.css b/docs/static/featomic.css similarity index 100% rename from docs/static/rascaline.css rename to docs/static/featomic.css diff --git a/docs/static/images/featomic-python.pdf b/docs/static/images/featomic-python.pdf new file mode 100644 index 000000000..abd9c2be7 Binary files /dev/null and b/docs/static/images/featomic-python.pdf differ diff --git a/docs/static/images/featomic-python.svg b/docs/static/images/featomic-python.svg new file mode 100644 index 000000000..df0ea2b22 --- /dev/null +++ b/docs/static/images/featomic-python.svg @@ -0,0 +1,69 @@ + + + + + + + libfeatomic.so + + + + featomic-c-api + + + + + + cbindgen + + + + + + featomic.h + + + + + + pycparser + + + + + + + featomic + + + Python module + + + + + + + + + _c_api.py + + + + + + + + + + + calls + + + generates + + + + + + + diff --git a/docs/static/images/rascaline-python.pdf b/docs/static/images/rascaline-python.pdf deleted file mode 100644 index 17cad47fe..000000000 Binary files a/docs/static/images/rascaline-python.pdf and /dev/null differ diff --git a/docs/static/images/rascaline-python.svg b/docs/static/images/rascaline-python.svg deleted file mode 100644 index 629c35d52..000000000 --- a/docs/static/images/rascaline-python.svg +++ /dev/null @@ -1 +0,0 @@ -librascaline.sorascaline-c-apicbindgenrascaline.hpycparserrascalinePython module_rascaline.pycallsgenerates \ No newline at end of file diff --git a/featomic-torch/CHANGELOG.md b/featomic-torch/CHANGELOG.md new file mode 100644 index 000000000..884341288 --- /dev/null +++ b/featomic-torch/CHANGELOG.md @@ -0,0 +1,39 @@ +# Changelog + +All notable changes to featomic are documented here, following the [keep +a changelog](https://keepachangelog.com/en/1.1.0/) format. This project follows +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased](https://github.com/metatensor/featomic/) + + + +## [Version 0.6.0](https://github.com/metatensor/featomic/releases/tag/featomic-torch-v0.6.0) - 2025-01-07 + +### Added + +- C++ and Python TorchScript bindings to `featomic`, making all calculators + accessible from TorchScript models. + +- Integration of the TorchScript calculators with + [metatensor-torch-atomistic](https://docs.metatensor.org/latest/atomistic/index.html), + using the `System` class from this package as a system provider, and + integrating with neighbor lists provided by the simulation engine through + metatensor. + +- Automatic integration with (Py)Torch automatic differentiation system. If any + of the inputs requires gradients, then `featomic-torch` will compute them, + store them, and integrate them with a custom backward function on the + calculator output. + +- Re-export of Python tools for Clebsch-Gordan tensor products from `featomic`, + in a TorchScript-compatible way. diff --git a/featomic-torch/CMakeLists.txt b/featomic-torch/CMakeLists.txt new file mode 100644 index 000000000..6a8b67a39 --- /dev/null +++ b/featomic-torch/CMakeLists.txt @@ -0,0 +1,261 @@ +cmake_minimum_required(VERSION 3.16) + +if (POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) # Timestamp for FetchContent +endif() + +if (POLICY CMP0077) + cmake_policy(SET CMP0077 NEW) # use variables to set OPTIONS +endif() + + +if(NOT "${LAST_CMAKE_VERSION}" VERSION_EQUAL ${CMAKE_VERSION}) + set(LAST_CMAKE_VERSION ${CMAKE_VERSION} CACHE INTERNAL "Last version of cmake used to configure") + if (${CMAKE_CURRENT_SOURCE_DIR} STREQUAL ${CMAKE_SOURCE_DIR}) + message(STATUS "Running CMake version ${CMAKE_VERSION}") + endif() +endif() + + +file(READ ${CMAKE_CURRENT_SOURCE_DIR}/VERSION FEATOMIC_TORCH_VERSION) +string(STRIP ${FEATOMIC_TORCH_VERSION} FEATOMIC_TORCH_VERSION) + +include(cmake/dev-versions.cmake) +create_development_version("${FEATOMIC_TORCH_VERSION}" FEATOMIC_TORCH_FULL_VERSION "featomic-torch-v") +message(STATUS "Building featomic-torch v${FEATOMIC_TORCH_FULL_VERSION}") + +# strip any -dev/-rc suffix on the version since project(VERSION) does not support it +string(REGEX REPLACE "([0-9]*)\\.([0-9]*)\\.([0-9]*).*" "\\1.\\2.\\3" FEATOMIC_TORCH_VERSION ${FEATOMIC_TORCH_FULL_VERSION}) +project(featomic_torch + VERSION ${FEATOMIC_TORCH_VERSION} + LANGUAGES CXX +) +set(PROJECT_VERSION ${FEATOMIC_TORCH_FULL_VERSION}) + + +option(FEATOMIC_TORCH_TESTS "Build featomic-torch C++ tests" OFF) +option(FEATOMIC_TORCH_FETCH_METATENSOR_TORCH "Download and build the metatensor_torch library before building featomic_torch" OFF) + +set(BIN_INSTALL_DIR "bin" CACHE PATH "Path relative to CMAKE_INSTALL_PREFIX where to install binaries/DLL") +set(LIB_INSTALL_DIR "lib" CACHE PATH "Path relative to CMAKE_INSTALL_PREFIX where to install libraries") +set(INCLUDE_INSTALL_DIR "include" CACHE PATH "Path relative to CMAKE_INSTALL_PREFIX where to install headers") + +# Set a default build type if none was specified +if (${CMAKE_CURRENT_SOURCE_DIR} STREQUAL ${CMAKE_SOURCE_DIR}) + if("${CMAKE_BUILD_TYPE}" STREQUAL "" AND "${CMAKE_CONFIGURATION_TYPES}" STREQUAL "") + message(STATUS "Setting build type to 'release' as none was specified.") + set( + CMAKE_BUILD_TYPE "release" + CACHE STRING + "Choose the type of build, options are: none(CMAKE_CXX_FLAGS or CMAKE_C_FLAGS used) debug release relwithdebinfo minsizerel." + FORCE + ) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS release debug relwithdebinfo minsizerel none) + endif() +endif() + +function(check_compatible_versions _dependency_ _actual_ _requested_) + if(${_actual_} MATCHES "^([0-9]+)\\.([0-9]+)") + set(_actual_major_ "${CMAKE_MATCH_1}") + set(_actual_minor_ "${CMAKE_MATCH_2}") + else() + message(FATAL_ERROR "Failed to parse actual version: ${_actual_}") + endif() + + if(${_requested_} MATCHES "^([0-9]+)\\.([0-9]+)") + set(_requested_major_ "${CMAKE_MATCH_1}") + set(_requested_minor_ "${CMAKE_MATCH_2}") + else() + message(FATAL_ERROR "Failed to parse requested version: ${_requested_}") + endif() + + if (${_requested_major_} EQUAL 0 AND ${_actual_minor_} EQUAL ${_requested_minor_}) + # major version is 0 and same minor version, everything is fine + elseif (${_actual_major_} EQUAL ${_requested_major_}) + # same major version, everything is fine + else() + # not compatible + message(FATAL_ERROR "Incompatible versions for ${_dependency_}: we need v${_requested_}, but we got v${_actual_}") + endif() +endfunction() + + +set(REQUIRED_FEATOMIC_VERSION "0.6.0") +if (NOT "$ENV{FEATOMIC_NO_LOCAL_DEPS}" STREQUAL "1") + # If building a dev version, we also need to update the + # REQUIRED_FEATOMIC_VERSION in the same way we update the + # featomic-torch version + create_development_version("${REQUIRED_FEATOMIC_VERSION}" FEATOMIC_FULL_VERSION "featomic-v") +else() + set(FEATOMIC_FULL_VERSION ${REQUIRED_FEATOMIC_VERSION}) +endif() +string(REGEX REPLACE "([0-9]*)\\.([0-9]*).*" "\\1.\\2" REQUIRED_FEATOMIC_VERSION ${FEATOMIC_FULL_VERSION}) + +# Either featomic is built as part of the same CMake project, or we try to +# find the corresponding CMake package +if (TARGET featomic) + get_target_property(FEATOMIC_BUILD_VERSION featomic BUILD_VERSION) + check_compatible_versions("featomic" ${FEATOMIC_BUILD_VERSION} ${REQUIRED_FEATOMIC_VERSION}) +else() + find_package(featomic ${REQUIRED_FEATOMIC_VERSION} CONFIG REQUIRED) + + get_target_property(FEATOMIC_LOCATION featomic IMPORTED_LOCATION) + get_filename_component(FEATOMIC_LOCATION ${FEATOMIC_LOCATION} DIRECTORY) + message(STATUS "Using local featomic from ${FEATOMIC_LOCATION}") +endif() + + +# FindCUDNN.cmake distributed with PyTorch is a bit broken, so we have a +# fixed version in `cmake/FindCUDNN.cmake` +set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake;${CMAKE_MODULE_PATH}") + +find_package(Torch 1.12 REQUIRED) + +# ============================================================================ # +# Setup metatensor_torch + +# METATENSOR_FETCH_VERSION is the exact version we will fetch from github if +# FEATOMIC_TORCH_FETCH_METATENSOR_TORCH=ON, and REQUIRED_METATENSOR_TORCH_VERSION +# is the minimal version we require when using `find_package` to find the library. +# +# When updating METATENSOR_FETCH_VERSION, you will also have to update the +# SHA256 sum of the file in `FetchContent_Declare`. +set(METATENSOR_FETCH_VERSION "0.6.1") +set(REQUIRED_METATENSOR_TORCH_VERSION "0.6") +if (FEATOMIC_TORCH_FETCH_METATENSOR_TORCH) + message(STATUS "Fetching metatensor-torch from github") + + set(URL_ROOT "https://github.com/lab-cosmo/metatensor/releases/download") + include(FetchContent) + FetchContent_Declare( + metatensor_torch + URL ${URL_ROOT}/metatensor-torch-v${METATENSOR_FETCH_VERSION}/metatensor-torch-cxx-${METATENSOR_FETCH_VERSION}.tar.gz + URL_HASH SHA256=0941da4bc6d25ee73b597774d3c8c6edf6a44f134139bd93b33834eae52ac4dd + ) + + if (CMAKE_VERSION VERSION_GREATER 3.18) + FetchContent_MakeAvailable(metatensor_torch) + else() + if (NOT metatensor_POPULATED) + FetchContent_Populate(metatensor_torch) + endif() + + add_subdirectory(${metatensor_torch_SOURCE_DIR} ${metatensor_torch_BINARY_DIR}) + endif() +else() + find_package(metatensor_torch ${REQUIRED_METATENSOR_TORCH_VERSION} REQUIRED CONFIG) + get_target_property(METATENSOR_TORCH_LOCATION metatensor_torch IMPORTED_LOCATION) + get_filename_component(METATENSOR_TORCH_LOCATION ${METATENSOR_TORCH_LOCATION} DIRECTORY) + message(STATUS "Using local metatensor-torch from ${METATENSOR_TORCH_LOCATION}") +endif() + + +set(FEATOMIC_TORCH_HEADERS + "include/featomic/torch/system.hpp" + "include/featomic/torch/autograd.hpp" + "include/featomic/torch/calculator.hpp" + "include/featomic/torch.hpp" +) + +set(FEATOMIC_TORCH_SOURCE + "src/system.cpp" + "src/autograd.cpp" + "src/openmp.cpp" + "src/calculator.cpp" + "src/register.cpp" +) + +add_library(featomic_torch SHARED + ${FEATOMIC_TORCH_HEADERS} + ${FEATOMIC_TORCH_SOURCE} +) + +target_link_libraries(featomic_torch PUBLIC torch metatensor_torch featomic) +target_compile_features(featomic_torch PUBLIC cxx_std_17) +target_include_directories(featomic_torch PUBLIC + $ + $ + $ +) + +# Create a header defining FEATOMIC_TORCH_EXPORT for to export classes/functions +# in DLL on Windows. +set_target_properties(featomic_torch PROPERTIES + # hide non-exported symbols by default, this mimics Windows behavior on Unix + CXX_VISIBILITY_PRESET hidden +) + +if (FEATOMIC_TORCH_FETCH_METATENSOR_TORCH) + # If we install metatensor_torch together with featomic_torch, we need to + # set the RPATH to $ORIGIN to make sure featomic_torch can find + # metatensor_torch. + set_target_properties(featomic_torch PROPERTIES INSTALL_RPATH "$ORIGIN") +endif() + +include(GenerateExportHeader) +generate_export_header(featomic_torch + BASE_NAME FEATOMIC_TORCH + EXPORT_FILE_NAME ${CMAKE_CURRENT_BINARY_DIR}/include/featomic/torch/exports.h +) +target_compile_definitions(featomic_torch PRIVATE featomic_torch_EXPORTS) + +find_package(OpenMP) +if (OpenMP_CXX_FOUND) + # Torch bundles its own copy of the OpenMP runtime library, and if we + # compile and link against the system version as well this can lead to + # crashes during initialization on macOS. + # + # So on this plaftorm we instead compile the code with OpenMP flags, and + # leave the corresponding symbols undefined in `featomic_torch`, hopping + # that when Torch is loaded we'll get these symbols in the global namespace. + # + # On other platforms, this seems to be less of an issue, maybe because torch + # adds a hash to the library name it bundles (i.e. `libgomp-de42aff.so`) + if (APPLE) + string(REPLACE " " ";" omp_cxx_flags_list ${OpenMP_CXX_FLAGS}) + target_compile_options(featomic_torch PRIVATE ${omp_cxx_flags_list}) + target_include_directories(featomic_torch PRIVATE SYSTEM ${OpenMP_CXX_INCLUDE_DIRS}) + target_link_libraries(featomic_torch PRIVATE -Wl,-undefined,dynamic_lookup) + else() + target_link_libraries(featomic_torch PRIVATE OpenMP::OpenMP_CXX) + endif() +endif() + +if (FEATOMIC_TORCH_TESTS) + enable_testing() + add_subdirectory(tests) +endif() + +#------------------------------------------------------------------------------# +# Installation configuration +#------------------------------------------------------------------------------# +include(CMakePackageConfigHelpers) +write_basic_package_version_file( + featomic_torch-config-version.cmake + VERSION ${FEATOMIC_TORCH_VERSION} + COMPATIBILITY SameMinorVersion +) + +install(TARGETS featomic_torch + EXPORT featomic_torch-targets + ARCHIVE DESTINATION ${LIB_INSTALL_DIR} + LIBRARY DESTINATION ${LIB_INSTALL_DIR} + RUNTIME DESTINATION ${BIN_INSTALL_DIR} +) +install(EXPORT featomic_torch-targets + DESTINATION ${LIB_INSTALL_DIR}/cmake/featomic_torch +) + +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/cmake/featomic_torch-config.in.cmake + ${CMAKE_CURRENT_BINARY_DIR}/featomic_torch-config.cmake + @ONLY +) +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/featomic_torch-config-version.cmake + ${CMAKE_CURRENT_BINARY_DIR}/featomic_torch-config.cmake + DESTINATION ${LIB_INSTALL_DIR}/cmake/featomic_torch +) + +install(DIRECTORY "include/featomic" DESTINATION ${INCLUDE_INSTALL_DIR}) +install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/include/featomic DESTINATION ${INCLUDE_INSTALL_DIR}) diff --git a/rascaline-torch/Cargo.toml b/featomic-torch/Cargo.toml similarity index 90% rename from rascaline-torch/Cargo.toml rename to featomic-torch/Cargo.toml index 6a21d42da..1a13d9b64 100644 --- a/rascaline-torch/Cargo.toml +++ b/featomic-torch/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "rascaline-torch" +name = "featomic-torch" version = "0.0.0" edition = "2021" publish = false diff --git a/featomic-torch/VERSION b/featomic-torch/VERSION new file mode 100644 index 000000000..a918a2aa1 --- /dev/null +++ b/featomic-torch/VERSION @@ -0,0 +1 @@ +0.6.0 diff --git a/rascaline-torch/cmake/FindCUDNN.cmake b/featomic-torch/cmake/FindCUDNN.cmake similarity index 100% rename from rascaline-torch/cmake/FindCUDNN.cmake rename to featomic-torch/cmake/FindCUDNN.cmake diff --git a/featomic-torch/cmake/dev-versions.cmake b/featomic-torch/cmake/dev-versions.cmake new file mode 100644 index 000000000..fd350df96 --- /dev/null +++ b/featomic-torch/cmake/dev-versions.cmake @@ -0,0 +1,96 @@ +# Parse a `_version_` number, and store its components in `_major_` `_minor_` +# `_patch_` and `_rc_` +function(parse_version _version_ _major_ _minor_ _patch_ _rc_) + string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)(-rc)?([0-9]+)?" _ "${_version_}") + + if(${CMAKE_MATCH_COUNT} EQUAL 3) + set(${_rc_} "" PARENT_SCOPE) + elseif(${CMAKE_MATCH_COUNT} EQUAL 5) + set(${_rc_} ${CMAKE_MATCH_5} PARENT_SCOPE) + else() + message(FATAL_ERROR "invalid version string ${_version_}") + endif() + + set(${_major_} ${CMAKE_MATCH_1} PARENT_SCOPE) + set(${_minor_} ${CMAKE_MATCH_2} PARENT_SCOPE) + set(${_patch_} ${CMAKE_MATCH_3} PARENT_SCOPE) +endfunction() + +if (CMAKE_VERSION VERSION_LESS "3.17") + # CMAKE_CURRENT_FUNCTION_LIST_DIR was added in CMake 3.17 + set(CMAKE_CURRENT_FUNCTION_LIST_DIR "${CMAKE_CURRENT_LIST_DIR}") +endif() + +# Get the time of the last modification since the last tag/release, and a hash +# of the latest commit/full state of a dirty repository +function(git_version_info _tag_prefix_ _output_n_commits_ _output_git_hash_) + set(_script_ "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../../scripts/git-version-info.py") + + if (EXISTS "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/git_version_info") + # When building from a tarball, the script is executed and the result + # put in this file + file(STRINGS "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/git_version_info" _file_content_) + list(GET _file_content_ 0 _n_commits_) + list(GET _file_content_ 1 _git_hash_) + + elseif (EXISTS "${_script_}") + # When building from a checkout, we'll need to run the script + find_package(Python COMPONENTS Interpreter REQUIRED) + execute_process( + COMMAND "${Python_EXECUTABLE}" "${_script_}" "${_tag_prefix_}" + RESULT_VARIABLE _status_ + OUTPUT_VARIABLE _stdout_ + ERROR_VARIABLE _stderr_ + WORKING_DIRECTORY ${CMAKE_CURRENT_FUNCTION_LIST_DIR} + ) + + if (NOT ${_status_} EQUAL 0) + message(WARNING + "git-version-info.py failed, version number might be wrong:\nstdout: ${_stdout_}\nstderr: ${_stderr_}") + set(${_output_} 0 PARENT_SCOPE) + return() + endif() + + if (NOT "${_stderr_}" STREQUAL "") + message(WARNING "git-version-info.py gave some errors, version number might be wrong:\nstdout: ${_stdout_}\nstderr: ${_stderr_}") + endif() + + string(REPLACE "\n" ";" _lines_ ${_stdout_}) + list(GET _lines_ 0 _n_commits_) + list(GET _lines_ 1 _git_hash_) + else() + message(FATAL_ERROR "could not update git version information") + endif() + + string(STRIP ${_n_commits_} _n_commits_) + set(${_output_n_commits_} ${_n_commits_} PARENT_SCOPE) + + string(STRIP ${_git_hash_} _git_hash_) + set(${_output_git_hash_} ${_git_hash_} PARENT_SCOPE) +endfunction() + + +# Take the version declared in the package, and increase the right number if we +# are actually installing a developement version from after the latest git tag +function(create_development_version _version_ _output_ _tag_prefix_) + git_version_info("${_tag_prefix_}" _n_commits_ _git_hash_) + + parse_version(${_version_} _major_ _minor_ _patch_ _rc_) + if(${_n_commits_} STREQUAL "0") + # we are building a release, leave the version number as-is + if("${_rc_}" STREQUAL "") + set(${_output_} "${_major_}.${_minor_}.${_patch_}" PARENT_SCOPE) + else() + set(${_output_} "${_major_}.${_minor_}.${_patch_}-rc${_rc_}" PARENT_SCOPE) + endif() + else() + # we are building a development version, increase the right part of the version + if("${_rc_}" STREQUAL "") + math(EXPR _minor_ "${_minor_} + 1") + set(${_output_} "${_major_}.${_minor_}.0-dev${_n_commits_}+${_git_hash_}" PARENT_SCOPE) + else() + math(EXPR _rc_ "${_rc_} + 1") + set(${_output_} "${_major_}.${_minor_}.${_patch_}-rc${_rc_}-dev${_n_commits_}+${_git_hash_}" PARENT_SCOPE) + endif() + endif() +endfunction() diff --git a/featomic-torch/cmake/featomic_torch-config.in.cmake b/featomic-torch/cmake/featomic_torch-config.in.cmake new file mode 100644 index 000000000..26e3f0878 --- /dev/null +++ b/featomic-torch/cmake/featomic_torch-config.in.cmake @@ -0,0 +1,28 @@ +include(CMakeFindDependencyMacro) + +# use the same version for featomic as the main CMakeLists.txt +set(REQUIRED_FEATOMIC_VERSION @REQUIRED_FEATOMIC_VERSION@) +find_package(featomic ${REQUIRED_FEATOMIC_VERSION} CONFIG REQUIRED) + +# use the same version for metatensor_torch as the main CMakeLists.txt +set(REQUIRED_METATENSOR_TORCH_VERSION @REQUIRED_METATENSOR_TORCH_VERSION@) +find_package(metatensor_torch ${REQUIRED_METATENSOR_TORCH_VERSION} CONFIG REQUIRED) + +# We can only load metatensorfeatomic_torch with the same minor version of Torch +# that was used to compile it (and is stored in BUILD_TORCH_VERSION) +set(BUILD_TORCH_VERSION @Torch_VERSION@) +set(BUILD_TORCH_MAJOR @Torch_VERSION_MAJOR@) +set(BUILD_TORCH_MINOR @Torch_VERSION_MINOR@) + +find_package(Torch ${BUILD_TORCH_VERSION} REQUIRED) + +if (NOT "${BUILD_TORCH_MAJOR}" STREQUAL "${Torch_VERSION_MAJOR}") + message(FATAL_ERROR "found incompatible torch version: featomic-torch was built against v${BUILD_TORCH_VERSION} but we found v${Torch_VERSION}") +endif() + +if (NOT "${BUILD_TORCH_MINOR}" STREQUAL "${Torch_VERSION_MINOR}") + message(FATAL_ERROR "found incompatible torch version: featomic-torch was built against v${BUILD_TORCH_VERSION} but we found v${Torch_VERSION}") +endif() + + +include(${CMAKE_CURRENT_LIST_DIR}/featomic_torch-targets.cmake) diff --git a/featomic-torch/include/featomic/torch.hpp b/featomic-torch/include/featomic/torch.hpp new file mode 100644 index 000000000..1a01132d3 --- /dev/null +++ b/featomic-torch/include/featomic/torch.hpp @@ -0,0 +1,10 @@ +#ifndef FEATOMIC_TORCH_HPP +#define FEATOMIC_TORCH_HPP + +#include "featomic/torch/exports.h" // IWYU pragma: export + +#include "featomic/torch/system.hpp" // IWYU pragma: export +#include "featomic/torch/calculator.hpp" // IWYU pragma: export + + +#endif diff --git a/rascaline-torch/include/rascaline/torch/autograd.hpp b/featomic-torch/include/featomic/torch/autograd.hpp similarity index 80% rename from rascaline-torch/include/rascaline/torch/autograd.hpp rename to featomic-torch/include/featomic/torch/autograd.hpp index 67d1e305f..14673d16a 100644 --- a/rascaline-torch/include/rascaline/torch/autograd.hpp +++ b/featomic-torch/include/featomic/torch/autograd.hpp @@ -1,23 +1,23 @@ -// IWYU pragma: private; include "rascaline/torch.hpp" +// IWYU pragma: private; include "featomic/torch.hpp" -#ifndef RASCALINE_TORCH_AUTOGRAD_HPP -#define RASCALINE_TORCH_AUTOGRAD_HPP +#ifndef FEATOMIC_TORCH_AUTOGRAD_HPP +#define FEATOMIC_TORCH_AUTOGRAD_HPP #include #include #include -#include "rascaline/torch/exports.h" +#include "featomic/torch/exports.h" -namespace rascaline_torch { +namespace featomic_torch { -/// Custom torch::autograd::Function integrating rascaline with torch autograd. +/// Custom torch::autograd::Function integrating featomic with torch autograd. /// /// This is a bit more complex than your typical autograd because there is some -/// impedance mismatch between rascaline and torch. Most of it should be taken +/// impedance mismatch between featomic and torch. Most of it should be taken /// care of by the `compute` function below. -class RASCALINE_TORCH_EXPORT RascalineAutograd: public torch::autograd::Function { +class FEATOMIC_TORCH_EXPORT FeatomicAutograd: public torch::autograd::Function { public: /// Register a pseudo node in Torch's computational graph going from /// `all_positions` and `all_cell` to the values in `block`; using the diff --git a/rascaline-torch/include/rascaline/torch/calculator.hpp b/featomic-torch/include/featomic/torch/calculator.hpp similarity index 81% rename from rascaline-torch/include/rascaline/torch/calculator.hpp rename to featomic-torch/include/featomic/torch/calculator.hpp index b56269889..332a0b109 100644 --- a/rascaline-torch/include/rascaline/torch/calculator.hpp +++ b/featomic-torch/include/featomic/torch/calculator.hpp @@ -1,16 +1,16 @@ -#ifndef RASCALINE_TORCH_CALCULATOR_HPP -#define RASCALINE_TORCH_CALCULATOR_HPP +#ifndef FEATOMIC_TORCH_CALCULATOR_HPP +#define FEATOMIC_TORCH_CALCULATOR_HPP #include -#include +#include #include #include -#include "rascaline/torch/exports.h" +#include "featomic/torch/exports.h" -namespace rascaline_torch { -class RascalineAutograd; +namespace featomic_torch { +class FeatomicAutograd; class CalculatorHolder; using TorchCalculator = torch::intrusive_ptr; @@ -19,7 +19,7 @@ class CalculatorOptionsHolder; using TorchCalculatorOptions = torch::intrusive_ptr; /// Options for a single calculation -class RASCALINE_TORCH_EXPORT CalculatorOptionsHolder: public torch::CustomClassHolder { +class FEATOMIC_TORCH_EXPORT CalculatorOptionsHolder: public torch::CustomClassHolder { public: /// get the current selected samples torch::IValue selected_samples() const { @@ -32,8 +32,8 @@ class RASCALINE_TORCH_EXPORT CalculatorOptionsHolder: public torch::CustomClassH /// `metatensor_torch::TorchTensorMap`. void set_selected_samples(torch::IValue selection); - /// Get the selected samples in the format used by rascaline - rascaline::LabelsSelection selected_samples_rascaline() const; + /// Get the selected samples in the format used by featomic + featomic::LabelsSelection selected_samples_featomic() const; /// get the current selected properties torch::IValue selected_properties() const { @@ -46,8 +46,8 @@ class RASCALINE_TORCH_EXPORT CalculatorOptionsHolder: public torch::CustomClassH /// `metatensor_torch::TorchTensorMap`. void set_selected_properties(torch::IValue selection); - /// Get the selected properties in the format used by rascaline - rascaline::LabelsSelection selected_properties_rascaline() const; + /// Get the selected properties in the format used by featomic + featomic::LabelsSelection selected_properties_featomic() const; /// get the current selected keys torch::IValue selected_keys() const { @@ -69,9 +69,9 @@ class RASCALINE_TORCH_EXPORT CalculatorOptionsHolder: public torch::CustomClassH torch::IValue selected_keys_ = torch::IValue(); }; -/// Custom class holder to store, serialize and load rascaline calculators +/// Custom class holder to store, serialize and load featomic calculators /// inside Torch(Script) modules. -class RASCALINE_TORCH_EXPORT CalculatorHolder: public torch::CustomClassHolder { +class FEATOMIC_TORCH_EXPORT CalculatorHolder: public torch::CustomClassHolder { public: /// Create a new calculator with the given `name` and JSON `parameters` CalculatorHolder(std::string name, std::string parameters): @@ -107,7 +107,7 @@ class RASCALINE_TORCH_EXPORT CalculatorHolder: public torch::CustomClassHolder { private: std::string c_name_; - rascaline::Calculator calculator_; + featomic::Calculator calculator_; }; @@ -123,7 +123,7 @@ class RASCALINE_TORCH_EXPORT CalculatorHolder: public torch::CustomClassHolder { /// contain `"cell"` gradients. /// /// `forward_gradients` controls which gradients are left inside the TensorMap. -metatensor_torch::TorchTensorMap RASCALINE_TORCH_EXPORT register_autograd( +metatensor_torch::TorchTensorMap FEATOMIC_TORCH_EXPORT register_autograd( std::vector systems, metatensor_torch::TorchTensorMap precomputed, std::vector forward_gradients diff --git a/rascaline-torch/include/rascaline/torch/system.hpp b/featomic-torch/include/featomic/torch/system.hpp similarity index 77% rename from rascaline-torch/include/rascaline/torch/system.hpp rename to featomic-torch/include/featomic/torch/system.hpp index fb06a265d..f7c5170df 100644 --- a/rascaline-torch/include/rascaline/torch/system.hpp +++ b/featomic-torch/include/featomic/torch/system.hpp @@ -1,24 +1,24 @@ -#ifndef RASCALINE_TORCH_SYSTEM_HPP -#define RASCALINE_TORCH_SYSTEM_HPP +#ifndef FEATOMIC_TORCH_SYSTEM_HPP +#define FEATOMIC_TORCH_SYSTEM_HPP #include #include #include -#include +#include #include -#include "rascaline/torch/exports.h" +#include "featomic/torch/exports.h" -namespace rascaline_torch { +namespace featomic_torch { -/// Implementation of `rascaline::System` using `metatensor_torch::System` as +/// Implementation of `featomic::System` using `metatensor_torch::System` as /// backing memory for all the data. /// /// This can either be used with the Rust neighbor list implementation; or a set /// of pre-computed neighbor lists can be added to the system. -class RASCALINE_TORCH_EXPORT SystemAdapter final: public rascaline::System { +class FEATOMIC_TORCH_EXPORT SystemAdapter final: public featomic::System { public: /// Create a `SystemAdapter` wrapping an existing `metatensor_torch::System` SystemAdapter(metatensor_torch::System system); @@ -36,7 +36,7 @@ class RASCALINE_TORCH_EXPORT SystemAdapter final: public rascaline::System { SystemAdapter& operator=(SystemAdapter&&) = default; /*========================================================================*/ - /* Functions to implement rascaline::System */ + /* Functions to implement featomic::System */ /*========================================================================*/ /// @private @@ -68,16 +68,16 @@ class RASCALINE_TORCH_EXPORT SystemAdapter final: public rascaline::System { void compute_neighbors(double cutoff) override; /// @private - const std::vector& pairs() const override; + const std::vector& pairs() const override; /// @private - const std::vector& pairs_containing(uintptr_t atom) const override; + const std::vector& pairs_containing(uintptr_t atom) const override; /*========================================================================*/ /* Functions to re-use pre-computed pairs */ /*========================================================================*/ - /// Should we copy data to rascaline internal data structure and compute the + /// Should we copy data to featomic internal data structure and compute the /// neighbor list there? This is set to `true` by default, or `false` if /// a neighbor list has been added with `set_precomputed_pairs`. bool use_native_system() const; @@ -95,11 +95,11 @@ class RASCALINE_TORCH_EXPORT SystemAdapter final: public rascaline::System { struct PrecomputedPairs { - std::vector pairs_; - std::vector> pairs_by_atom_; + std::vector pairs_; + std::vector> pairs_by_atom_; }; - void set_precomputed_pairs(double cutoff, std::vector pairs); + void set_precomputed_pairs(double cutoff, std::vector pairs); // all precomputed pairs we know about std::map precomputed_pairs_; diff --git a/rascaline-torch/lib.rs b/featomic-torch/lib.rs similarity index 100% rename from rascaline-torch/lib.rs rename to featomic-torch/lib.rs diff --git a/rascaline-torch/src/autograd.cpp b/featomic-torch/src/autograd.cpp similarity index 96% rename from rascaline-torch/src/autograd.cpp rename to featomic-torch/src/autograd.cpp index fc4f3055f..2ab1a75dc 100644 --- a/rascaline-torch/src/autograd.cpp +++ b/featomic-torch/src/autograd.cpp @@ -1,12 +1,12 @@ #include #include "metatensor/torch/tensor.hpp" -#include "rascaline/torch/autograd.hpp" +#include "featomic/torch/autograd.hpp" #include "./openmp.hpp" using namespace metatensor_torch; -using namespace rascaline_torch; +using namespace featomic_torch; // # NOTATION // @@ -18,12 +18,12 @@ using namespace rascaline_torch; // - r is the atomic positions; // - H is the cell matrix; -/// Implementation of the positions part of `RascalineAutograd::backward` as +/// Implementation of the positions part of `FeatomicAutograd::backward` as /// another custom autograd function, to allow for double backward. template struct PositionsGrad: torch::autograd::Function> { /// This operate one block at the time since we need to pass `dA_dX` (which - /// comes from `RascalineAutograd::backward` `grad_outputs`) as a + /// comes from `FeatomicAutograd::backward` `grad_outputs`) as a /// `torch::Tensor` to be able to register a `grad_fn` with it. static std::vector forward( torch::autograd::AutogradContext *ctx, @@ -93,10 +93,10 @@ static std::vector extract_gradient_blocks( } /******************************************************************************/ -/* RascalineAutograd */ +/* FeatomicAutograd */ /******************************************************************************/ -std::vector RascalineAutograd::forward( +std::vector FeatomicAutograd::forward( torch::autograd::AutogradContext *ctx, torch::Tensor all_positions, torch::Tensor all_cells, @@ -132,7 +132,7 @@ std::vector RascalineAutograd::forward( return {block->values()}; } -std::vector RascalineAutograd::backward( +std::vector FeatomicAutograd::backward( torch::autograd::AutogradContext *ctx, std::vector grad_outputs ) { @@ -173,7 +173,7 @@ std::vector RascalineAutograd::backward( positions_grad = output[0]; } else { - C10_THROW_ERROR(TypeError, "rascaline only supports float64 and float32 data"); + C10_THROW_ERROR(TypeError, "featomic only supports float64 and float32 data"); } } @@ -217,7 +217,7 @@ std::vector RascalineAutograd::backward( cell_grad = output[0]; } else { - C10_THROW_ERROR(TypeError, "rascaline only supports float64 and float32 data"); + C10_THROW_ERROR(TypeError, "featomic only supports float64 and float32 data"); } } @@ -367,7 +367,7 @@ std::vector PositionsGrad::backward( TORCH_WARN_ONCE( "second derivatives with respect to positions are not implemented " "and will not be accumulated during backward() calls. If you need " - "second derivatives, please open an issue on rascaline repository." + "second derivatives, please open an issue on featomic repository." ); } @@ -549,7 +549,7 @@ std::vector CellGrad::backward( TORCH_WARN_ONCE( "second derivatives with respect to cell matrix are not implemented " "and will not be accumulated during backward() calls. If you need " - "second derivatives, please open an issue on rascaline repository." + "second derivatives, please open an issue on featomic repository." ); } diff --git a/rascaline-torch/src/calculator.cpp b/featomic-torch/src/calculator.cpp similarity index 86% rename from rascaline-torch/src/calculator.cpp rename to featomic-torch/src/calculator.cpp index 36c803674..b33daa36e 100644 --- a/rascaline-torch/src/calculator.cpp +++ b/featomic-torch/src/calculator.cpp @@ -1,37 +1,37 @@ #include #include -#include +#include -#include "rascaline/torch/calculator.hpp" -#include "rascaline/torch/autograd.hpp" -#include "rascaline/torch/system.hpp" +#include "featomic/torch/calculator.hpp" +#include "featomic/torch/autograd.hpp" +#include "featomic/torch/system.hpp" using namespace metatensor_torch; -using namespace rascaline_torch; +using namespace featomic_torch; -class DisableRascalineCellGradientWarning { +class DisableFeatomicCellGradientWarning { public: - DisableRascalineCellGradientWarning() { - this->modify_env = std::getenv("RASCALINE_NO_WARN_CELL_GRADIENTS") == nullptr; + DisableFeatomicCellGradientWarning() { + this->modify_env = std::getenv("FEATOMIC_NO_WARN_CELL_GRADIENTS") == nullptr; if (this->modify_env) { #ifdef WIN32 - _putenv_s("RASCALINE_NO_WARN_CELL_GRADIENTS", "1"); + _putenv_s("FEATOMIC_NO_WARN_CELL_GRADIENTS", "1"); #else - setenv("RASCALINE_NO_WARN_CELL_GRADIENTS", "1", static_cast(false)); + setenv("FEATOMIC_NO_WARN_CELL_GRADIENTS", "1", static_cast(false)); #endif } } - ~DisableRascalineCellGradientWarning() { + ~DisableFeatomicCellGradientWarning() { if (this->modify_env) { #ifdef WIN32 - _putenv_s("RASCALINE_NO_WARN_CELL_GRADIENTS", ""); + _putenv_s("FEATOMIC_NO_WARN_CELL_GRADIENTS", ""); #else - unsetenv("RASCALINE_NO_WARN_CELL_GRADIENTS"); + unsetenv("FEATOMIC_NO_WARN_CELL_GRADIENTS"); #endif } } @@ -39,7 +39,7 @@ class DisableRascalineCellGradientWarning { bool modify_env; }; -// move a block created by rascaline to torch +// move a block created by featomic to torch static TorchTensorBlock block_to_torch( std::shared_ptr tensor, metatensor::TensorBlock block @@ -196,14 +196,14 @@ metatensor_torch::TorchTensorMap CalculatorHolder::compute( if (!device.is_cpu()) { TORCH_WARN_ONCE( - "Systems data is on device ", device, " but rascaline only supports ", + "Systems data is on device ", device, " but featomic only supports ", "calculations on CPU. All the data will be moved to CPU and then " "back on device on your behalf" ); } if (dtype != torch::kFloat32 && dtype != torch::kFloat64) { - C10_THROW_ERROR(TypeError, "rascaline only supports float64 and float32 data"); + C10_THROW_ERROR(TypeError, "featomic only supports float64 and float32 data"); } auto all_positions = stack_all_positions(systems); @@ -214,7 +214,7 @@ metatensor_torch::TorchTensorMap CalculatorHolder::compute( if (torch_options.get() == nullptr) { torch_options = torch::make_intrusive(); } - auto options = rascaline::CalculationOptions(); + auto options = featomic::CalculationOptions(); // which gradients should we compute? We have to compute some gradient // either if positions/cell has `requires_grad` set to `true`, or if the @@ -250,27 +250,27 @@ metatensor_torch::TorchTensorMap CalculatorHolder::compute( } // convert the systems - auto rascaline_systems = std::vector(); - rascaline_systems.reserve(systems.size()); + auto featomic_systems = std::vector(); + featomic_systems.reserve(systems.size()); for (auto& system: systems) { - rascaline_systems.emplace_back(system); + featomic_systems.emplace_back(system); } - options.use_native_system = all_systems_use_native(rascaline_systems); + options.use_native_system = all_systems_use_native(featomic_systems); if (torch_options->selected_keys().isCustomClass()) { options.selected_keys = torch_options->selected_keys().toCustomClass()->as_metatensor(); } - options.selected_samples = torch_options->selected_samples_rascaline(); - options.selected_properties = torch_options->selected_properties_rascaline(); + options.selected_samples = torch_options->selected_samples_featomic(); + options.selected_properties = torch_options->selected_properties_featomic(); // ============ run the calculation and move data to torch ============== // auto raw_descriptor = std::shared_ptr(); { // do not warn when using "cell" gradients - auto guard = DisableRascalineCellGradientWarning(); + auto guard = DisableFeatomicCellGradientWarning(); raw_descriptor= std::make_shared( - calculator_.compute(rascaline_systems, options) + calculator_.compute(featomic_systems, options) ); } @@ -293,8 +293,8 @@ metatensor_torch::TorchTensorMap CalculatorHolder::compute( // ============ register the autograd nodes for each block ============== // for (int64_t block_i=0; block_ikeys()->count(); block_i++) { auto block = TensorMapHolder::block_by_id(torch_descriptor, block_i); - // see `RascalineAutograd::forward` for an explanation of what's happening - auto _ = RascalineAutograd::apply( + // see `FeatomicAutograd::forward` for an explanation of what's happening + auto _ = FeatomicAutograd::apply( all_positions, all_cells, systems_start_ivalue, @@ -311,7 +311,7 @@ metatensor_torch::TorchTensorMap CalculatorHolder::compute( } -metatensor_torch::TorchTensorMap rascaline_torch::register_autograd( +metatensor_torch::TorchTensorMap featomic_torch::register_autograd( std::vector systems, metatensor_torch::TorchTensorMap precomputed, std::vector forward_gradients @@ -368,7 +368,7 @@ metatensor_torch::TorchTensorMap rascaline_torch::register_autograd( for (int64_t block_i=0; block_ikeys()->count(); block_i++) { auto block = TensorMapHolder::block_by_id(precomputed, block_i); - auto _ = RascalineAutograd::apply( + auto _ = FeatomicAutograd::apply( all_positions, all_cells, systems_start_ivalue, @@ -435,17 +435,17 @@ static void check_selection_type( } } -static rascaline::LabelsSelection selection_to_rascaline(const torch::IValue& selection, std::string field) { +static featomic::LabelsSelection selection_to_featomic(const torch::IValue& selection, std::string field) { if (selection.isNone()) { - return rascaline::LabelsSelection::all(); + return featomic::LabelsSelection::all(); } else if (selection.isCustomClass()) { try { auto subset = selection.toCustomClass(); - return rascaline::LabelsSelection::subset(subset->as_metatensor()); + return featomic::LabelsSelection::subset(subset->as_metatensor()); } catch (const c10::Error&) { try { auto predefined = selection.toCustomClass(); - return rascaline::LabelsSelection::predefined(predefined->as_metatensor()); + return featomic::LabelsSelection::predefined(predefined->as_metatensor()); } catch (const c10::Error&) { C10_THROW_ERROR(TypeError, "internal error: invalid type for `" + field + "`, got " @@ -466,8 +466,8 @@ void CalculatorOptionsHolder::set_selected_samples(torch::IValue selection) { selected_samples_ = std::move(selection); } -rascaline::LabelsSelection CalculatorOptionsHolder::selected_samples_rascaline() const { - return selection_to_rascaline(selected_samples_, "selected_samples"); +featomic::LabelsSelection CalculatorOptionsHolder::selected_samples_featomic() const { + return selection_to_featomic(selected_samples_, "selected_samples"); } void CalculatorOptionsHolder::set_selected_properties(torch::IValue selection) { @@ -475,8 +475,8 @@ void CalculatorOptionsHolder::set_selected_properties(torch::IValue selection) { selected_properties_ = std::move(selection); } -rascaline::LabelsSelection CalculatorOptionsHolder::selected_properties_rascaline() const { - return selection_to_rascaline(selected_properties_, "selected_properties"); +featomic::LabelsSelection CalculatorOptionsHolder::selected_properties_featomic() const { + return selection_to_featomic(selected_properties_, "selected_properties"); } void CalculatorOptionsHolder::set_selected_keys(torch::IValue selection) { diff --git a/rascaline-torch/src/openmp.cpp b/featomic-torch/src/openmp.cpp similarity index 95% rename from rascaline-torch/src/openmp.cpp rename to featomic-torch/src/openmp.cpp index f2527f045..ed6a643e8 100644 --- a/rascaline-torch/src/openmp.cpp +++ b/featomic-torch/src/openmp.cpp @@ -5,7 +5,7 @@ #include -using namespace rascaline_torch; +using namespace featomic_torch; #ifndef _OPENMP diff --git a/rascaline-torch/src/openmp.hpp b/featomic-torch/src/openmp.hpp similarity index 84% rename from rascaline-torch/src/openmp.hpp rename to featomic-torch/src/openmp.hpp index f4d1acf2d..3340f4a72 100644 --- a/rascaline-torch/src/openmp.hpp +++ b/featomic-torch/src/openmp.hpp @@ -1,5 +1,5 @@ -#ifndef RASCALINE_TORCH_OPENMP_HPP -#define RASCALINE_TORCH_OPENMP_HPP +#ifndef FEATOMIC_TORCH_OPENMP_HPP +#define FEATOMIC_TORCH_OPENMP_HPP #include @@ -14,7 +14,7 @@ int omp_get_thread_num(); #endif -namespace rascaline_torch { +namespace featomic_torch { class ThreadLocalTensor { public: diff --git a/rascaline-torch/src/register.cpp b/featomic-torch/src/register.cpp similarity index 95% rename from rascaline-torch/src/register.cpp rename to featomic-torch/src/register.cpp index ea9c64670..2b3246d77 100644 --- a/rascaline-torch/src/register.cpp +++ b/featomic-torch/src/register.cpp @@ -1,9 +1,9 @@ #include -#include "rascaline/torch.hpp" -using namespace rascaline_torch; +#include "featomic/torch.hpp" +using namespace featomic_torch; -TORCH_LIBRARY(rascaline, module) { +TORCH_LIBRARY(featomic, module) { // There is no way to access the docstrings from Python, so we don't bother // setting them to something useful here. const std::string DOCSTRING; diff --git a/rascaline-torch/src/system.cpp b/featomic-torch/src/system.cpp similarity index 89% rename from rascaline-torch/src/system.cpp rename to featomic-torch/src/system.cpp index 706ee609c..512e8925e 100644 --- a/rascaline-torch/src/system.cpp +++ b/featomic-torch/src/system.cpp @@ -1,19 +1,19 @@ #include #include -#include "rascaline/torch/system.hpp" +#include "featomic/torch/system.hpp" -using namespace rascaline_torch; +using namespace featomic_torch; SystemAdapter::SystemAdapter(metatensor_torch::System system): system_(std::move(system)) { this->types_ = system_->types().to(torch::kCPU).contiguous(); this->positions_ = system_->positions().to(torch::kCPU).to(torch::kDouble).contiguous(); this->cell_ = system_->cell().to(torch::kCPU).to(torch::kDouble).contiguous(); - // convert all neighbors list that where requested by rascaline + // convert all neighbors list that where requested by featomic for (const auto& options: system_->known_neighbor_lists()) { for (const auto& requestor: options->requestors()) { - if (requestor == "rascaline") { + if (requestor == "featomic") { auto cutoff = options->cutoff(); auto neighbors = system_->get_neighbor_list(options); auto samples_values = neighbors->samples()->values().to(torch::kCPU).contiguous(); @@ -24,13 +24,13 @@ SystemAdapter::SystemAdapter(metatensor_torch::System system): system_(std::move auto n_pairs = samples.size(0); - auto pairs = std::vector(); + auto pairs = std::vector(); for (int64_t i=0; i(samples[i][0]); pair.second = static_cast(samples[i][1]); @@ -59,8 +59,8 @@ SystemAdapter::SystemAdapter(metatensor_torch::System system): system_(std::move } } -void SystemAdapter::set_precomputed_pairs(double cutoff, std::vector pairs) { - auto pairs_by_center = std::vector>(); +void SystemAdapter::set_precomputed_pairs(double cutoff, std::vector pairs) { + auto pairs_by_center = std::vector>(); pairs_by_center.resize(this->size()); for (const auto& pair: pairs) { @@ -117,7 +117,7 @@ void SystemAdapter::compute_neighbors(double cutoff) { last_cutoff_ = cutoff; } -const std::vector& SystemAdapter::pairs() const { +const std::vector& SystemAdapter::pairs() const { if (this->use_native_system() || last_cutoff_ == -1.0) { C10_THROW_ERROR(ValueError, "this system only support 'use_native_systems=true'" @@ -129,7 +129,7 @@ const std::vector& SystemAdapter::pairs() const { return it->second.pairs_; } -const std::vector& SystemAdapter::pairs_containing(uintptr_t atom) const { +const std::vector& SystemAdapter::pairs_containing(uintptr_t atom) const { if (this->use_native_system() || last_cutoff_ == -1.0) { C10_THROW_ERROR(ValueError, "this system only support 'use_native_systems=true'" diff --git a/rascaline-torch/tests/CMakeLists.txt b/featomic-torch/tests/CMakeLists.txt similarity index 85% rename from rascaline-torch/tests/CMakeLists.txt rename to featomic-torch/tests/CMakeLists.txt index 874738c63..b0d9c11b9 100644 --- a/rascaline-torch/tests/CMakeLists.txt +++ b/featomic-torch/tests/CMakeLists.txt @@ -1,5 +1,5 @@ -# re-use catch from rascaline-c-api C++ tests -add_subdirectory(../../rascaline-c-api/tests/catch catch) +# re-use catch from featomic C++ tests +add_subdirectory(../../featomic/tests/utils/catch catch) # make sure we compile catch with the flags that torch requires. In particular, # torch sets -D_GLIBCXX_USE_CXX11_ABI=0 on Linux, which changes some of the @@ -8,7 +8,7 @@ target_link_libraries(catch torch) find_program(VALGRIND valgrind) if (VALGRIND) - if (NOT "$ENV{RASCALINE_DISABLE_VALGRIND}" EQUAL "1") + if (NOT "$ENV{FEATOMIC_DISABLE_VALGRIND}" EQUAL "1") message(STATUS "Running tests using valgrind") set(TEST_COMMAND "${VALGRIND}" "--tool=memcheck" "--dsymutil=yes" "--error-exitcode=125" @@ -35,7 +35,7 @@ file(GLOB ALL_TESTS *.cpp) foreach(_file_ ${ALL_TESTS}) get_filename_component(_name_ ${_file_} NAME_WE) add_executable(torch-${_name_} ${_file_}) - target_link_libraries(torch-${_name_} rascaline_torch catch) + target_link_libraries(torch-${_name_} featomic_torch catch) add_test( NAME torch-${_name_} @@ -47,7 +47,7 @@ foreach(_file_ ${ALL_TESTS}) # (and any other DLL) STRING(REPLACE ";" "\\;" PATH_STRING "$ENV{PATH}") set_tests_properties(torch-${_name_} PROPERTIES - ENVIRONMENT "PATH=${PATH_STRING}\;$\;$\;$\;$\;$" + ENVIRONMENT "PATH=${PATH_STRING}\;$\;$\;$\;$\;$" ) endif() endforeach() diff --git a/rascaline-torch/tests/calculator.cpp b/featomic-torch/tests/calculator.cpp similarity index 95% rename from rascaline-torch/tests/calculator.cpp rename to featomic-torch/tests/calculator.cpp index c86e1818a..e63bb6694 100644 --- a/rascaline-torch/tests/calculator.cpp +++ b/featomic-torch/tests/calculator.cpp @@ -1,14 +1,14 @@ #include -#include +#include #include #include "metatensor/torch/labels.hpp" -#include "rascaline/torch.hpp" +#include "featomic/torch.hpp" #include -using namespace rascaline_torch; +using namespace featomic_torch; using namespace metatensor_torch; static metatensor_torch::System test_system(bool positions_grad, bool cell_grad); @@ -167,7 +167,7 @@ TEST_CASE("Calculator") { auto grad_fn = values.grad_fn(); REQUIRE(grad_fn); - CHECK_THAT(grad_fn->name(), Catch::Matchers::Contains("rascaline_torch::RascalineAutograd")); + CHECK_THAT(grad_fn->name(), Catch::Matchers::Contains("featomic_torch::FeatomicAutograd")); // forward gradients auto gradient = TensorBlockHolder::gradient(block, "positions"); @@ -199,7 +199,7 @@ TEST_CASE("Calculator") { grad_fn = values.grad_fn(); REQUIRE(grad_fn); - CHECK_THAT(grad_fn->name(), Catch::Matchers::Contains("rascaline_torch::RascalineAutograd")); + CHECK_THAT(grad_fn->name(), Catch::Matchers::Contains("featomic_torch::FeatomicAutograd")); // forward gradients gradient = TensorBlockHolder::gradient(block, "positions"); @@ -231,7 +231,7 @@ TEST_CASE("Calculator") { auto grad_fn = values.grad_fn(); REQUIRE(grad_fn); - CHECK_THAT(grad_fn->name(), Catch::Matchers::Contains("rascaline_torch::RascalineAutograd")); + CHECK_THAT(grad_fn->name(), Catch::Matchers::Contains("featomic_torch::FeatomicAutograd")); // no forward gradients CHECK(block->gradients_list().empty()); @@ -244,7 +244,7 @@ TEST_CASE("Calculator") { grad_fn = values.grad_fn(); REQUIRE(grad_fn); - CHECK_THAT(grad_fn->name(), Catch::Matchers::Contains("rascaline_torch::RascalineAutograd")); + CHECK_THAT(grad_fn->name(), Catch::Matchers::Contains("featomic_torch::FeatomicAutograd")); // no forward gradients CHECK(block->gradients_list().empty()); diff --git a/rascaline-torch/tests/check-torch-install.rs b/featomic-torch/tests/check-torch-install.rs similarity index 65% rename from rascaline-torch/tests/check-torch-install.rs rename to featomic-torch/tests/check-torch-install.rs index 0409d15ad..6de2dc0bc 100644 --- a/rascaline-torch/tests/check-torch-install.rs +++ b/featomic-torch/tests/check-torch-install.rs @@ -12,14 +12,14 @@ fn check_torch_install() { const CARGO_TARGET_TMPDIR: &str = env!("CARGO_TARGET_TMPDIR"); // ====================================================================== // - // build and install rascaline-torch with cmake + // build and install featomic-torch with cmake let mut build_dir = PathBuf::from(CARGO_TARGET_TMPDIR); build_dir.push("torch-install"); let deps_dir = build_dir.join("deps"); - let rascaline_dep = deps_dir.join("rascaline"); - std::fs::create_dir_all(&rascaline_dep).expect("failed to create rascaline dep dir"); - let rascaline_cmake_prefix = utils::setup_rascaline(rascaline_dep); + let featomic_dep = deps_dir.join("featomic"); + std::fs::create_dir_all(&featomic_dep).expect("failed to create featomic dep dir"); + let featomic_cmake_prefix = utils::setup_featomic(featomic_dep); let torch_dep = deps_dir.join("torch"); std::fs::create_dir_all(&torch_dep).expect("failed to create torch dep dir"); @@ -27,41 +27,41 @@ fn check_torch_install() { let cargo_manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); - // configure cmake for rascaline-torch - let rascaline_torch_dep = deps_dir.join("rascaline-torch"); - let install_prefix = rascaline_torch_dep.join("usr"); - std::fs::create_dir_all(&rascaline_torch_dep).expect("failed to create rascaline-torch dep dir"); - let mut cmake_config = utils::cmake_config(&cargo_manifest_dir, &rascaline_torch_dep); + // configure cmake for featomic-torch + let featomic_torch_dep = deps_dir.join("featomic-torch"); + let install_prefix = featomic_torch_dep.join("usr"); + std::fs::create_dir_all(&featomic_torch_dep).expect("failed to create featomic-torch dep dir"); + let mut cmake_config = utils::cmake_config(&cargo_manifest_dir, &featomic_torch_dep); cmake_config.arg(format!( "-DCMAKE_PREFIX_PATH={};{}", - rascaline_cmake_prefix.display(), + featomic_cmake_prefix.display(), pytorch_cmake_prefix.display() )); cmake_config.arg(format!("-DCMAKE_INSTALL_PREFIX={}", install_prefix.display())); - cmake_config.arg("-DRASCALINE_TORCH_FETCH_METATENSOR_TORCH=ON"); + cmake_config.arg("-DFEATOMIC_TORCH_FETCH_METATENSOR_TORCH=ON"); - // The two properties below handle the RPATH for rascaline_torch, setting it - // in such a way that we can always load librascaline.so and libtorch.so - // from the location they are found at when compiling rascaline-torch. See + // The two properties below handle the RPATH for featomic_torch, setting it + // in such a way that we can always load libfeatomic.so and libtorch.so + // from the location they are found at when compiling featomic-torch. See // https://gitlab.kitware.com/cmake/community/-/wikis/doc/cmake/RPATH-handling // for more information on CMake RPATH handling cmake_config.arg("-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON"); cmake_config.arg("-DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON"); let status = cmake_config.status().expect("could not run cmake"); - assert!(status.success(), "failed to run rascaline_torch cmake configuration"); + assert!(status.success(), "failed to run featomic_torch cmake configuration"); - // build and install rascaline-torch - let mut cmake_build = utils::cmake_build(&rascaline_torch_dep); + // build and install featomic-torch + let mut cmake_build = utils::cmake_build(&featomic_torch_dep); cmake_build.arg("--target"); cmake_build.arg("install"); let status = cmake_build.status().expect("could not run cmake"); - assert!(status.success(), "failed to run rascaline_torch cmake build"); + assert!(status.success(), "failed to run featomic_torch cmake build"); // ====================================================================== // - // try to use the installed rascaline-torch from cmake + // try to use the installed featomic-torch from cmake let mut source_dir = PathBuf::from(&cargo_manifest_dir); source_dir.extend(["tests", "cmake-project"]); @@ -70,7 +70,7 @@ fn check_torch_install() { let mut cmake_config = utils::cmake_config(&source_dir, &build_dir); cmake_config.arg(format!( "-DCMAKE_PREFIX_PATH={};{};{}", - rascaline_cmake_prefix.display(), + featomic_cmake_prefix.display(), pytorch_cmake_prefix.display(), install_prefix.display(), )); @@ -78,7 +78,7 @@ fn check_torch_install() { let status = cmake_config.status().expect("could not run cmake"); assert!(status.success(), "failed to run test project cmake configuration"); - // build the code, linking to rascaline-torch + // build the code, linking to featomic-torch let mut cmake_build = utils::cmake_build(&build_dir); let status = cmake_build.status().expect("could not run cmake"); assert!(status.success(), "failed to run test project cmake build"); diff --git a/featomic-torch/tests/cmake-project/CMakeLists.txt b/featomic-torch/tests/cmake-project/CMakeLists.txt new file mode 100644 index 000000000..2dc9443ea --- /dev/null +++ b/featomic-torch/tests/cmake-project/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.16) + +project(featomic-torch-test-cmake-project CXX) + + +# We need to update the REQUIRED_FEATOMIC_VERSION in the same way we update the +# featomic version for dev builds +include(../../cmake/dev-versions.cmake) +set(REQUIRED_FEATOMIC_TORCH_VERSION "0.6.0") +create_development_version("${REQUIRED_FEATOMIC_TORCH_VERSION}" FEATOMIC_TORCH_FULL_VERSION "featomic-torch-v") +string(REGEX REPLACE "([0-9]*)\\.([0-9]*).*" "\\1.\\2" REQUIRED_FEATOMIC_TORCH_VERSION ${FEATOMIC_TORCH_FULL_VERSION}) +find_package(featomic_torch ${REQUIRED_FEATOMIC_TORCH_VERSION} REQUIRED) + +add_executable(torch-main src/main.cpp) +target_link_libraries(torch-main featomic_torch) + +enable_testing() +add_test(NAME torch-main COMMAND torch-main) + +if(WIN32) + # We need to set the path to allow access to metatensor.dll + STRING(REPLACE ";" "\\;" PATH_STRING "$ENV{PATH}") + set_tests_properties(torch-main PROPERTIES + ENVIRONMENT "PATH=${PATH_STRING}\;$\;$\;$" + ) +endif() diff --git a/featomic-torch/tests/cmake-project/README.md b/featomic-torch/tests/cmake-project/README.md new file mode 100644 index 000000000..c2e5071c0 --- /dev/null +++ b/featomic-torch/tests/cmake-project/README.md @@ -0,0 +1,3 @@ +# Sample CMake project using featomic-torch + +This is a basic cmake project linking to featomic-torch from C++ code. diff --git a/rascaline-torch/tests/cmake-project/src/main.cpp b/featomic-torch/tests/cmake-project/src/main.cpp similarity index 90% rename from rascaline-torch/tests/cmake-project/src/main.cpp rename to featomic-torch/tests/cmake-project/src/main.cpp index 02f1e32c8..442e07975 100644 --- a/rascaline-torch/tests/cmake-project/src/main.cpp +++ b/featomic-torch/tests/cmake-project/src/main.cpp @@ -1,9 +1,9 @@ #include #include -#include +#include -using namespace rascaline_torch; +using namespace featomic_torch; int main() { auto system = torch::make_intrusive( diff --git a/rascaline-torch/tests/run-torch-tests.rs b/featomic-torch/tests/run-torch-tests.rs similarity index 77% rename from rascaline-torch/tests/run-torch-tests.rs rename to featomic-torch/tests/run-torch-tests.rs index 8b3d7c7f8..9caf8b53c 100644 --- a/rascaline-torch/tests/run-torch-tests.rs +++ b/featomic-torch/tests/run-torch-tests.rs @@ -18,26 +18,26 @@ fn run_torch_tests() { build_dir.push("torch-tests"); let deps_dir = build_dir.join("deps"); - let rascaline_dep = deps_dir.join("rascaline"); - std::fs::create_dir_all(&rascaline_dep).expect("failed to create rascaline dep dir"); - let rascaline_cmake_prefix = utils::setup_rascaline(rascaline_dep); + let featomic_dep = deps_dir.join("featomic"); + std::fs::create_dir_all(&featomic_dep).expect("failed to create featomic dep dir"); + let featomic_cmake_prefix = utils::setup_featomic(featomic_dep); let torch_dep = deps_dir.join("torch"); std::fs::create_dir_all(&torch_dep).expect("failed to create torch dep dir"); let pytorch_cmake_prefix = utils::setup_pytorch(torch_dep); // ====================================================================== // - // build the rascaline-torch C++ tests and run them + // build the featomic-torch C++ tests and run them let mut source_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); - source_dir.extend(["..", "rascaline-torch"]); + source_dir.extend(["..", "featomic-torch"]); // configure cmake for the tests let mut cmake_config = utils::cmake_config(&source_dir, &build_dir); - cmake_config.arg("-DRASCALINE_TORCH_TESTS=ON"); - cmake_config.arg("-DRASCALINE_TORCH_FETCH_METATENSOR_TORCH=ON"); + cmake_config.arg("-DFEATOMIC_TORCH_TESTS=ON"); + cmake_config.arg("-DFEATOMIC_TORCH_FETCH_METATENSOR_TORCH=ON"); cmake_config.arg(format!( "-DCMAKE_PREFIX_PATH={};{}", - rascaline_cmake_prefix.display(), + featomic_cmake_prefix.display(), pytorch_cmake_prefix.display() )); let status = cmake_config.status().expect("could not run cmake"); diff --git a/rascaline-torch/tests/utils/mod.rs b/featomic-torch/tests/utils/mod.rs similarity index 79% rename from rascaline-torch/tests/utils/mod.rs rename to featomic-torch/tests/utils/mod.rs index cb6302687..8924e3b3e 100644 --- a/rascaline-torch/tests/utils/mod.rs +++ b/featomic-torch/tests/utils/mod.rs @@ -1,4 +1,4 @@ -//! utility functions to run rascaline-torch tests from Cargo +//! utility functions to run featomic-torch tests from Cargo //! (used in `run-torch-tests.rs` and `torch-install-check.rs`) #![allow(clippy::needless_return)] @@ -7,7 +7,7 @@ use std::path::PathBuf; use std::process::Command; -#[path = "../../../rascaline-c-api/tests/utils/mod.rs"] +#[path = "../../../featomic/tests/utils/mod.rs"] mod core_utils; pub use core_utils::cmake_build; @@ -84,7 +84,7 @@ pub fn setup_pytorch(build_dir: PathBuf) -> PathBuf { .expect("failed to run python"); assert!(status.success(), "failed to run `python -m pip install --upgrade pip`"); - let torch_version = std::env::var("RASCALINE_TORCH_TEST_VERSION").unwrap_or("2.3.*".into()); + let torch_version = std::env::var("FEATOMIC_TORCH_TEST_VERSION").unwrap_or("2.3.*".into()); let status = Command::new(&python) .arg("-m") .arg("pip") @@ -111,29 +111,28 @@ pub fn setup_pytorch(build_dir: PathBuf) -> PathBuf { return prefix; } -/// Build rascaline in `build_dir`, and return the installation prefix -pub fn setup_rascaline(build_dir: PathBuf) -> PathBuf { - let mut rascaline_source_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); - rascaline_source_dir.extend(["..", "rascaline-c-api"]); +/// Build featomic in `build_dir`, and return the installation prefix +pub fn setup_featomic(build_dir: PathBuf) -> PathBuf { + let mut featomic_source_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + featomic_source_dir.extend(["..", "featomic"]); - // configure cmake for rascaline - let mut cmake_config = cmake_config(&rascaline_source_dir, &build_dir); + // configure cmake for featomic + let mut cmake_config = cmake_config(&featomic_source_dir, &build_dir); let install_prefix = build_dir.join("usr"); cmake_config.arg(format!("-DCMAKE_INSTALL_PREFIX={}", install_prefix.display())); - cmake_config.arg("-DRASCALINE_FETCH_METATENSOR=ON"); - cmake_config.arg("-DRASCALINE_ENABLE_CHEMFILES=OFF"); + cmake_config.arg("-DFEATOMIC_FETCH_METATENSOR=ON"); let status = cmake_config.status().expect("could not run cmake"); - assert!(status.success(), "failed to run rascaline cmake configuration"); + assert!(status.success(), "failed to run featomic cmake configuration"); - // build and install rascaline + // build and install featomic let mut cmake_build = cmake_build(&build_dir); cmake_build.arg("--target"); cmake_build.arg("install"); let status = cmake_build.status().expect("could not run cmake"); - assert!(status.success(), "failed to run rascaline cmake build"); + assert!(status.success(), "failed to run featomic cmake build"); return install_prefix; } diff --git a/rascaline-torch/tests/valgrind.supp b/featomic-torch/tests/valgrind.supp similarity index 100% rename from rascaline-torch/tests/valgrind.supp rename to featomic-torch/tests/valgrind.supp diff --git a/featomic/CHANGELOG.md b/featomic/CHANGELOG.md new file mode 100644 index 000000000..769b6ffa6 --- /dev/null +++ b/featomic/CHANGELOG.md @@ -0,0 +1,62 @@ +# Changelog + +All notable changes to featomic are documented here, following the [keep +a changelog](https://keepachangelog.com/en/1.1.0/) format. This project follows +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased](https://github.com/metatensor/featomic/) + + + +## [Version 0.6.0](https://github.com/metatensor/featomic/releases/tag/featomic-v0.6.0) - 2024-12-20 + +### Added + +- Multiple atomistic features calculators with a native implementation: + - SOAP spherical expansion, radial spectrum, power spectrum and spherical + expansion for pairs of atoms; + - LODE spherical expansion; + - Neighbor list; + - Sorted distances vector; + - Atomic composition. + +- All the calculator outputs are stored in + [metatensor's](https://docs.metatensor.org/) `TensorMap` objects. This allow + to both store the features in a very sparse format, saving memory; and to + store different irreducible representations (for SO(3) equivariant atomsitic + features) + +- Most of the calculators can compute gradients with respect to `positions`, + `cell` or `stress`, storing them in the `gradient()` of metatensor's + `TensorBlock`. + +- All the native calculators are exposed through a C API, and accessible from + multiple languages: Rust, C++ and Python. + +- Interface to mutliple system providers, and a way to define custom system + providers in user code. The following system providers are supported from + Python: ASE (https://wiki.fysik.dtu.dk/ase/); chemfiles + (https://chemfiles.org/); and PySCF (https://pyscf.org/) + +- Python-only calculators, based on Clebsch-Gordan tensor products to combine + equivariant featurizations. This includes + - PowerSpectrum, able to combine two different spherical expansions + - `EquivariantPowerSpectrum`, the same but producing features both invariant + and covariant with respect to rotations + - `DensityCorrelations` to compute arbitrary body-order density correlations; + - `ClebschGordanProduct`, the core building block that does a single + Clebsch-Gordan tensor product. + +- Python tools to define custom atomic density and radial basis functions, and + then compute splines for the radial integral apearing in SOAP and LODE + spherical expansions. This enables using these native calculators with + user-defined atomic densities and basis functions. diff --git a/rascaline-c-api/CMakeLists.txt b/featomic/CMakeLists.txt similarity index 59% rename from rascaline-c-api/CMakeLists.txt rename to featomic/CMakeLists.txt index 74a2e2db7..454fc8305 100644 --- a/rascaline-c-api/CMakeLists.txt +++ b/featomic/CMakeLists.txt @@ -1,15 +1,15 @@ -# Basic CMake integration for rascaline. +# Basic CMake integration for featomic. cmake_minimum_required(VERSION 3.16) -# Is rascaline the main project configured by the user? Or is this being used +# Is featomic the main project configured by the user? Or is this being used # as a submodule/subdirectory? if (${CMAKE_CURRENT_SOURCE_DIR} STREQUAL ${CMAKE_SOURCE_DIR}) - set(RASCALINE_MAIN_PROJECT ON) + set(FEATOMIC_MAIN_PROJECT ON) else() - set(RASCALINE_MAIN_PROJECT OFF) + set(FEATOMIC_MAIN_PROJECT OFF) endif() -if(${RASCALINE_MAIN_PROJECT} AND NOT "${CACHED_LAST_CMAKE_VERSION}" VERSION_EQUAL ${CMAKE_VERSION}) +if(${FEATOMIC_MAIN_PROJECT} AND NOT "${CACHED_LAST_CMAKE_VERSION}" VERSION_EQUAL ${CMAKE_VERSION}) # We use CACHED_LAST_CMAKE_VERSION to only print the cmake version # once in the configuration log set(CACHED_LAST_CMAKE_VERSION ${CMAKE_VERSION} CACHE INTERNAL "Last version of cmake used to configure") @@ -21,36 +21,42 @@ if (POLICY CMP0135) endif() if (POLICY CMP0077) - # use variables to set OPTIONS - cmake_policy(SET CMP0077 NEW) + cmake_policy(SET CMP0077 NEW) # use variables to set OPTIONS endif() file(STRINGS "Cargo.toml" CARGO_TOML_CONTENT) foreach(line ${CARGO_TOML_CONTENT}) - string(REGEX REPLACE "version = \"([0-9]+\\.[0-9]+\\.[0-9]+)\".*" "\\1" RASCALINE_VERSION ${line}) + string(REGEX REPLACE "version = \"([0-9]+\\.[0-9]+\\.[0-9]+.*)\"" "\\1" FEATOMIC_VERSION ${line}) if (NOT ${CMAKE_MATCH_COUNT} EQUAL 0) - # stop on the first regex match, this should be rascaline version + # stop on the first regex match, this should be featomic version break() endif() endforeach() -project(rascaline - VERSION ${RASCALINE_VERSION} - LANGUAGES C CXX +include(cmake/dev-versions.cmake) +create_development_version("${FEATOMIC_VERSION}" FEATOMIC_FULL_VERSION "featomic-v") +message(STATUS "Building featomic v${FEATOMIC_FULL_VERSION}") + +# strip any -dev/-rc suffix on the version since project(VERSION) does not support it +string(REGEX REPLACE "([0-9]*)\\.([0-9]*)\\.([0-9]*).*" "\\1.\\2.\\3" FEATOMIC_VERSION ${FEATOMIC_FULL_VERSION}) +project(featomic + VERSION ${FEATOMIC_VERSION} + LANGUAGES C CXX # we need to declare a language to access CMAKE_SIZEOF_VOID_P later ) +set(PROJECT_VERSION ${FEATOMIC_FULL_VERSION}) # We follow the standard CMake convention of using BUILD_SHARED_LIBS to provide # either a shared or static library as a default target. But since cargo always # builds both versions by default, we also install both versions by default. -# `RASCALINE_INSTALL_BOTH_STATIC_SHARED=OFF` allow to disable this behavior, and +# `FEATOMIC_INSTALL_BOTH_STATIC_SHARED=OFF` allow to disable this behavior, and # only install the file corresponding to `BUILD_SHARED_LIBS=ON/OFF`. # -# BUILD_SHARED_LIBS controls the `rascaline` cmake target, making it an alias of -# either `rascaline::static` or `rascaline::shared`. This is mainly relevant -# when using rascaline from another cmake project, either as a submodule or from -# an installed library (see cmake/rascaline-config.cmake) +# BUILD_SHARED_LIBS controls the `featomic` cmake target, making it an alias of +# either `featomic::static` or `featomic::shared`. This is mainly relevant +# when using featomic from another cmake project, either as a submodule or from +# an installed library (see cmake/featomic-config.cmake) option(BUILD_SHARED_LIBS "Use a shared library by default instead of a static one" ON) -option(RASCALINE_INSTALL_BOTH_STATIC_SHARED "Install both shared and static libraries" ON) +option(FEATOMIC_INSTALL_BOTH_STATIC_SHARED "Install both shared and static libraries" ON) set(BIN_INSTALL_DIR "bin" CACHE PATH "Path relative to CMAKE_INSTALL_PREFIX where to install binaries/DLL") set(LIB_INSTALL_DIR "lib" CACHE PATH "Path relative to CMAKE_INSTALL_PREFIX where to install libraries") @@ -60,13 +66,12 @@ set(RUST_BUILD_TARGET "" CACHE STRING "Cross-compilation target for rust code. L set(EXTRA_RUST_FLAGS "" CACHE STRING "Flags used to build rust code") mark_as_advanced(RUST_BUILD_TARGET EXTRA_RUST_FLAGS) -option(RASCALINE_ENABLE_CHEMFILES "Enable the usage of chemfiles for reading structures from files" OFF) -option(RASCALINE_FETCH_METATENSOR "Download and build the metatensor C API before building rascaline" OFF) +option(FEATOMIC_FETCH_METATENSOR "Download and build the metatensor C API before building featomic" OFF) set(CMAKE_MACOSX_RPATH ON) set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${LIB_INSTALL_DIR}") -if (${RASCALINE_MAIN_PROJECT}) +if (${FEATOMIC_MAIN_PROJECT}) if("${CMAKE_BUILD_TYPE}" STREQUAL "" AND "${CMAKE_CONFIGURATION_TYPES}" STREQUAL "") message(STATUS "Setting build type to 'release' as none was specified.") set(CMAKE_BUILD_TYPE "release" @@ -77,9 +82,9 @@ if (${RASCALINE_MAIN_PROJECT}) endif() endif() -if(${RASCALINE_MAIN_PROJECT} AND NOT "${CACHED_LAST_CMAKE_BUILD_TYPE}" STREQUAL ${CMAKE_BUILD_TYPE}) +if(${FEATOMIC_MAIN_PROJECT} AND NOT "${CACHED_LAST_CMAKE_BUILD_TYPE}" STREQUAL ${CMAKE_BUILD_TYPE}) set(CACHED_LAST_CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE} CACHE INTERNAL "Last build type used in configuration") - message(STATUS "Building rascaline in ${CMAKE_BUILD_TYPE} mode") + message(STATUS "Building featomic in ${CMAKE_BUILD_TYPE} mode") endif() find_program(CARGO_EXE "cargo" DOC "path to cargo (Rust build system)") @@ -117,16 +122,21 @@ endif() # ============================================================================ # # determine Cargo flags +set(CARGO_BUILD_ARG "") + +if (EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/Cargo.lock) + set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--locked") +endif() + # TODO: support multiple configuration generators (MSVC, ...) string(TOLOWER ${CMAKE_BUILD_TYPE} BUILD_TYPE) if ("${BUILD_TYPE}" STREQUAL "debug") - set(CARGO_BUILD_ARG "") set(CARGO_BUILD_TYPE "debug") elseif("${BUILD_TYPE}" STREQUAL "release") - set(CARGO_BUILD_ARG "--release") + set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--release") set(CARGO_BUILD_TYPE "release") elseif("${BUILD_TYPE}" STREQUAL "relwithdebinfo") - set(CARGO_BUILD_ARG "--release") + set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--release") set(CARGO_BUILD_TYPE "release") else() message(FATAL_ERROR "unsuported build type: ${CMAKE_BUILD_TYPE}") @@ -134,6 +144,7 @@ endif() set(CARGO_TARGET_DIR ${CMAKE_CURRENT_BINARY_DIR}/target) set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--target-dir=${CARGO_TARGET_DIR}") +set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--features=c-api") # Handle cross compilation with RUST_BUILD_TARGET if ("${RUST_BUILD_TARGET}" STREQUAL "") @@ -143,14 +154,14 @@ if ("${RUST_BUILD_TARGET}" STREQUAL "") message(FATAL_ERROR "failed to determine host target, output was: ${CARGO_VERSION_RAW}") endif() - if (${RASCALINE_MAIN_PROJECT}) + if (${FEATOMIC_MAIN_PROJECT}) message(STATUS "Compiling to host (${RUST_HOST_TARGET})") endif() set(CARGO_OUTPUT_DIR "${CARGO_TARGET_DIR}/${CARGO_BUILD_TYPE}") set(RUST_BUILD_TARGET ${RUST_HOST_TARGET}) else() - if (${RASCALINE_MAIN_PROJECT}) + if (${FEATOMIC_MAIN_PROJECT}) message(STATUS "Cross-compiling to ${RUST_BUILD_TARGET}") endif() @@ -158,12 +169,8 @@ else() set(CARGO_OUTPUT_DIR "${CARGO_TARGET_DIR}/${RUST_BUILD_TARGET}/${CARGO_BUILD_TYPE}") endif() -if (NOT ${RASCALINE_ENABLE_CHEMFILES}) - set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--no-default-features") -endif() - # Get the list of libraries linked by default by cargo/rustc to add when linking -# to rascaline::static +# to featomic::static if (CARGO_VERSION_CHANGED) include(cmake/tempdir.cmake) get_tempdir(TMPDIR) @@ -183,7 +190,7 @@ if (CARGO_VERSION_CHANGED) file(APPEND "${TMPDIR}/_cargo_required_libs/Cargo.toml" "[lib]\ncrate-type=[\"staticlib\"]") execute_process( - COMMAND ${CARGO_EXE} rustc --verbose --color never --target=${RUST_BUILD_TARGET} -- --print=native-static-libs + COMMAND ${CARGO_EXE} rustc --color never --target=${RUST_BUILD_TARGET} -- --print=native-static-libs WORKING_DIRECTORY "${TMPDIR}/_cargo_required_libs" RESULT_VARIABLE cargo_build_result ERROR_VARIABLE cargo_build_error_message @@ -211,7 +218,7 @@ if (CARGO_VERSION_CHANGED) list(REMOVE_DUPLICATES stripped_lib_list) set(CARGO_DEFAULT_LIBRARIES "${stripped_lib_list}" CACHE INTERNAL "list of implicitly linked libraries") - if (${RASCALINE_MAIN_PROJECT}) + if (${FEATOMIC_MAIN_PROJECT}) message(STATUS "Cargo default link libraries are: ${CARGO_DEFAULT_LIBRARIES}") endif() else() @@ -222,10 +229,16 @@ endif() # ============================================================================ # # Setup metatensor +# METATENSOR_FETCH_VERSION is the exact version we will fetch from github if +# FEATOMIC_FETCH_METATENSOR=ON, and METATENSOR_REQUIRED_VERSION is the minimal +# version we require when using `find_package` to find the library. +# +# When updating METATENSOR_FETCH_VERSION, you will also have to update the +# SHA256 sum of the file in `FetchContent_Declare`. set(METATENSOR_FETCH_VERSION "0.1.11") set(METATENSOR_REQUIRED_VERSION "0.1") -if (RASCALINE_FETCH_METATENSOR) - message(STATUS "Fetching metatensor @ ${METATENSOR_FETCH_VERSION} from github") +if (FEATOMIC_FETCH_METATENSOR) + message(STATUS "Fetching metatensor-core from github") include(FetchContent) set(URL_ROOT "https://github.com/lab-cosmo/metatensor/releases/download") @@ -245,52 +258,53 @@ if (RASCALINE_FETCH_METATENSOR) add_subdirectory(${metatensor_SOURCE_DIR} ${metatensor_BINARY_DIR}) endif() - # metatensor will be installed in the same place as rascaline, so set + # metatensor will be installed in the same place as featomic, so set # the RPATH to ${ORIGIN} to load the file from there set(METATENSOR_RPATH "$$\\{ORIGIN\\}") else() find_package(metatensor ${METATENSOR_REQUIRED_VERSION} REQUIRED CONFIG) - - # in case rascaline gets installed in a different place than metatensor, + # in case featomic gets installed in a different place than metatensor, # set the RPATH to the directory where we found metatensor get_target_property(METATENSOR_LOCATION metatensor::shared IMPORTED_LOCATION) get_filename_component(METATENSOR_LOCATION ${METATENSOR_LOCATION} DIRECTORY) set(METATENSOR_RPATH "${METATENSOR_LOCATION}") + + message(STATUS "Using local metatensor from ${METATENSOR_LOCATION}") endif() # ============================================================================ # -# Setup rascaline libraries +# Setup featomic libraries file(GLOB_RECURSE ALL_RUST_SOURCES ${PROJECT_SOURCE_DIR}/../Cargo.toml - ${PROJECT_SOURCE_DIR}/../rascaline/Cargo.toml - ${PROJECT_SOURCE_DIR}/../rascaline/src/**.rs + ${PROJECT_SOURCE_DIR}/../featomic/Cargo.toml + ${PROJECT_SOURCE_DIR}/../featomic/src/**.rs ${PROJECT_SOURCE_DIR}/Cargo.toml ${PROJECT_SOURCE_DIR}/build.rs ${PROJECT_SOURCE_DIR}/src/**.rs ) -add_library(rascaline::shared SHARED IMPORTED GLOBAL) -set(RASCALINE_SHARED_LOCATION "${CARGO_OUTPUT_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}rascaline${CMAKE_SHARED_LIBRARY_SUFFIX}") -set(RASCALINE_IMPLIB_LOCATION "${RASCALINE_SHARED_LOCATION}.lib") +add_library(featomic::shared SHARED IMPORTED GLOBAL) +set(FEATOMIC_SHARED_LOCATION "${CARGO_OUTPUT_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}featomic${CMAKE_SHARED_LIBRARY_SUFFIX}") +set(FEATOMIC_IMPLIB_LOCATION "${FEATOMIC_SHARED_LOCATION}.lib") -add_library(rascaline::static STATIC IMPORTED GLOBAL) -set(RASCALINE_STATIC_LOCATION "${CARGO_OUTPUT_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}rascaline${CMAKE_STATIC_LIBRARY_SUFFIX}") +add_library(featomic::static STATIC IMPORTED GLOBAL) +set(FEATOMIC_STATIC_LOCATION "${CARGO_OUTPUT_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}featomic${CMAKE_STATIC_LIBRARY_SUFFIX}") -get_filename_component(RASCALINE_SHARED_LIB_NAME ${RASCALINE_SHARED_LOCATION} NAME) -get_filename_component(RASCALINE_IMPLIB_NAME ${RASCALINE_IMPLIB_LOCATION} NAME) -get_filename_component(RASCALINE_STATIC_LIB_NAME ${RASCALINE_STATIC_LOCATION} NAME) +get_filename_component(FEATOMIC_SHARED_LIB_NAME ${FEATOMIC_SHARED_LOCATION} NAME) +get_filename_component(FEATOMIC_IMPLIB_NAME ${FEATOMIC_IMPLIB_LOCATION} NAME) +get_filename_component(FEATOMIC_STATIC_LIB_NAME ${FEATOMIC_STATIC_LOCATION} NAME) # We need to add some metadata to the shared library to enable linking to it # without using an absolute path. if (UNIX) if (APPLE) - # set the install name to `@rpath/librascaline.dylib` - set(CARGO_RUSTC_ARGS "-Clink-arg=-Wl,-install_name,@rpath/${RASCALINE_SHARED_LIB_NAME}") + # set the install name to `@rpath/libfeatomic.dylib` + set(CARGO_RUSTC_ARGS "-Clink-arg=-Wl,-install_name,@rpath/${FEATOMIC_SHARED_LIB_NAME}") else() # LINUX - # set the SONAME to librascaline.so, and point the RPATH to metatensor - set(CARGO_RUSTC_ARGS "-Clink-arg=-Wl,-soname,${RASCALINE_SHARED_LIB_NAME},-rpath=${METATENSOR_RPATH}") + # set the SONAME to libfeatomic.so, and point the RPATH to metatensor + set(CARGO_RUSTC_ARGS "-Clink-arg=-Wl,-soname,${FEATOMIC_SHARED_LIB_NAME},-rpath=${METATENSOR_RPATH}") endif() else() set(CARGO_RUSTC_ARGS "") @@ -309,58 +323,67 @@ if (NOT "$ENV{RUSTC_WRAPPER}" STREQUAL "") list(APPEND CARGO_ENV "RUSTC_WRAPPER=$ENV{RUSTC_WRAPPER}") endif() -add_custom_target(cargo-build-rascaline ALL +if (FEATOMIC_INSTALL_BOTH_STATIC_SHARED) + set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--crate-type=cdylib;--crate-type=staticlib") + set(FILES_CREATED_BY_CARGO "${FEATOMIC_SHARED_LIB_NAME} and ${FEATOMIC_STATIC_LIB_NAME}") +else() + if (BUILD_SHARED_LIBS) + set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--crate-type=cdylib") + set(FILES_CREATED_BY_CARGO "${FEATOMIC_SHARED_LIB_NAME}") + else() + set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--crate-type=staticlib") + set(FILES_CREATED_BY_CARGO "${FEATOMIC_STATIC_LIB_NAME}") + endif() +endif() + +add_custom_target(cargo-build-featomic ALL COMMAND ${CMAKE_COMMAND} -E env ${CARGO_ENV} cargo rustc ${CARGO_BUILD_ARG} -- ${CARGO_RUSTC_ARGS} WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} DEPENDS ${ALL_RUST_SOURCES} - COMMENT "Building ${RASCALINE_SHARED_LIB_NAME} and ${RASCALINE_STATIC_LIB_NAME} with cargo" - BYPRODUCTS ${RASCALINE_STATIC_LOCATION} ${RASCALINE_SHARED_LOCATION} ${RASCALINE_IMPLIB_LOCATION} + COMMENT "Building ${FILES_CREATED_BY_CARGO} with cargo" + BYPRODUCTS ${FEATOMIC_STATIC_LOCATION} ${FEATOMIC_SHARED_LOCATION} ${FEATOMIC_IMPLIB_LOCATION} ) -add_dependencies(rascaline::shared cargo-build-rascaline) -add_dependencies(rascaline::static cargo-build-rascaline) +add_dependencies(featomic::shared cargo-build-featomic) +add_dependencies(featomic::static cargo-build-featomic) -set(RASCALINE_HEADERS - "${PROJECT_SOURCE_DIR}/include/rascaline.h" - "${PROJECT_SOURCE_DIR}/include/rascaline.hpp" +set(FEATOMIC_HEADERS + "${PROJECT_SOURCE_DIR}/include/featomic.h" + "${PROJECT_SOURCE_DIR}/include/featomic.hpp" ) -set(RASCALINE_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include/) - -set_target_properties(rascaline::shared PROPERTIES - IMPORTED_LOCATION ${RASCALINE_SHARED_LOCATION} - INTERFACE_INCLUDE_DIRECTORIES ${RASCALINE_INCLUDE_DIR} - # the library will need to be linked as C++ code - # since it might contains chemfiles - IMPORTED_LINK_INTERFACE_LANGUAGES CXX +set(FEATOMIC_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include/) + +set_target_properties(featomic::shared PROPERTIES + IMPORTED_LOCATION ${FEATOMIC_SHARED_LOCATION} + INTERFACE_INCLUDE_DIRECTORIES ${FEATOMIC_INCLUDE_DIR} + BUILD_VERSION "${FEATOMIC_FULL_VERSION}" ) -target_compile_features(rascaline::shared INTERFACE cxx_std_17) +target_compile_features(featomic::shared INTERFACE cxx_std_17) if (WIN32) - set_target_properties(rascaline::shared PROPERTIES - IMPORTED_IMPLIB ${RASCALINE_IMPLIB_LOCATION} + set_target_properties(featomic::shared PROPERTIES + IMPORTED_IMPLIB ${FEATOMIC_IMPLIB_LOCATION} ) endif() -set_target_properties(rascaline::static PROPERTIES - IMPORTED_LOCATION ${RASCALINE_STATIC_LOCATION} - INTERFACE_INCLUDE_DIRECTORIES ${RASCALINE_INCLUDE_DIR} +set_target_properties(featomic::static PROPERTIES + IMPORTED_LOCATION ${FEATOMIC_STATIC_LOCATION} + INTERFACE_INCLUDE_DIRECTORIES ${FEATOMIC_INCLUDE_DIR} INTERFACE_LINK_LIBRARIES "${CARGO_DEFAULT_LIBRARIES}" - # the library will need to be linked as C++ code - # since it might contains chemfiles - IMPORTED_LINK_INTERFACE_LANGUAGES CXX + BUILD_VERSION "${FEATOMIC_FULL_VERSION}" ) if (BUILD_SHARED_LIBS) - add_library(rascaline ALIAS rascaline::shared) + add_library(featomic ALIAS featomic::shared) else() - add_library(rascaline ALIAS rascaline::static) + add_library(featomic ALIAS featomic::static) endif() -add_dependencies(cargo-build-rascaline metatensor) -target_link_libraries(rascaline::shared INTERFACE metatensor::shared) -target_link_libraries(rascaline::static INTERFACE metatensor::shared) +add_dependencies(cargo-build-featomic metatensor) +target_link_libraries(featomic::shared INTERFACE metatensor::shared) +target_link_libraries(featomic::static INTERFACE metatensor::shared) #------------------------------------------------------------------------------# # Installation configuration @@ -368,36 +391,36 @@ target_link_libraries(rascaline::static INTERFACE metatensor::shared) include(CMakePackageConfigHelpers) configure_package_config_file( - "${PROJECT_SOURCE_DIR}/cmake/rascaline-config.in.cmake" - "${PROJECT_BINARY_DIR}/rascaline-config.cmake" - INSTALL_DESTINATION ${LIB_INSTALL_DIR}/cmake/rascaline + "${PROJECT_SOURCE_DIR}/cmake/featomic-config.in.cmake" + "${PROJECT_BINARY_DIR}/featomic-config.cmake" + INSTALL_DESTINATION ${LIB_INSTALL_DIR}/cmake/featomic ) configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/cmake/rascaline-config-version.in.cmake" - "${CMAKE_CURRENT_BINARY_DIR}/rascaline-config-version.cmake" + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/featomic-config-version.in.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/featomic-config-version.cmake" @ONLY ) -install(FILES ${RASCALINE_HEADERS} DESTINATION ${INCLUDE_INSTALL_DIR}) +install(FILES ${FEATOMIC_HEADERS} DESTINATION ${INCLUDE_INSTALL_DIR}) -if (RASCALINE_INSTALL_BOTH_STATIC_SHARED OR BUILD_SHARED_LIBS) +if (FEATOMIC_INSTALL_BOTH_STATIC_SHARED OR BUILD_SHARED_LIBS) if (WIN32) # DLL files should go in /bin - install(FILES ${RASCALINE_SHARED_LOCATION} DESTINATION ${BIN_INSTALL_DIR}) + install(FILES ${FEATOMIC_SHARED_LOCATION} DESTINATION ${BIN_INSTALL_DIR}) # .lib files should go in /lib - install(FILES ${RASCALINE_IMPLIB_LOCATION} DESTINATION ${LIB_INSTALL_DIR}) + install(FILES ${FEATOMIC_IMPLIB_LOCATION} DESTINATION ${LIB_INSTALL_DIR}) else() - install(FILES ${RASCALINE_SHARED_LOCATION} DESTINATION ${LIB_INSTALL_DIR}) + install(FILES ${FEATOMIC_SHARED_LOCATION} DESTINATION ${LIB_INSTALL_DIR}) endif() endif() -if (RASCALINE_INSTALL_BOTH_STATIC_SHARED OR NOT BUILD_SHARED_LIBS) - install(FILES ${RASCALINE_STATIC_LOCATION} DESTINATION ${LIB_INSTALL_DIR}) +if (FEATOMIC_INSTALL_BOTH_STATIC_SHARED OR NOT BUILD_SHARED_LIBS) + install(FILES ${FEATOMIC_STATIC_LOCATION} DESTINATION ${LIB_INSTALL_DIR}) endif() install(FILES - ${PROJECT_BINARY_DIR}/rascaline-config-version.cmake - ${PROJECT_BINARY_DIR}/rascaline-config.cmake - DESTINATION ${LIB_INSTALL_DIR}/cmake/rascaline + ${PROJECT_BINARY_DIR}/featomic-config-version.cmake + ${PROJECT_BINARY_DIR}/featomic-config.cmake + DESTINATION ${LIB_INSTALL_DIR}/cmake/featomic ) diff --git a/featomic/Cargo.toml b/featomic/Cargo.toml new file mode 100644 index 000000000..6a3f7942b --- /dev/null +++ b/featomic/Cargo.toml @@ -0,0 +1,91 @@ +[package] +name = "featomic" +version = "0.6.0" +authors = ["Guillaume Fraux "] +edition = "2021" +rust-version = "1.74" + +description = "Library to compute representations for atomistic machine learning" +# readme = "TODO" +documentation = "https://metatensor.github.io/featomic/" +repository = "https://github.com/metatensor/featomic" +license = "BSD-3-Clause" + +[lib] +bench = false + +[features] +default = [] +# Include the code for the featomic C API +c-api = ["time-graph/table", "time-graph/json", "log/std"] +# use a static library for metatensor instead of a shared one +metatensor-static = ["metatensor/static"] + +[package.metadata."docs.rs"] +all-features = true + +[dependencies] +metatensor = {version = "0.2", features = ["rayon"]} + +ndarray = {version = "0.16", features = ["rayon", "serde", "approx"]} +num-traits = "0.2" +rayon = "1.5" + +log = "0.4" +once_cell = "1" +indexmap = "2" +thread_local = "1.1" +time-graph = "0.3.0" + +serde = { version = "1", features = ["derive"] } +serde_json = "1" +schemars = "=1.0.0-alpha.15" + +chemfiles = {version = "0.10", optional = true} + +approx = "0.5" + +[build-dependencies] +cbindgen = { version = "0.27", default-features = false } +fs_extra = "1" +metatensor = "0.2" + +[dev-dependencies] +criterion = "0.5" +which = "5" +glob = "0.3" +ndarray-npy = "0.9" +flate2 = "1.0.20" +time-graph = {version = "0.3.0", features = ["table", "json"]} + + +[[bench]] +name = "spherical-harmonics" +harness = false + +[[bench]] +name = "lode-spherical-expansion" +harness = false +required-features = ["chemfiles"] + +[[bench]] +name = "soap-spherical-expansion" +harness = false +required-features = ["chemfiles"] + +[[bench]] +name = "soap-power-spectrum" +harness = false +required-features = ["chemfiles"] + +[[example]] +name = "compute-soap" +required-features = ["chemfiles"] + +[[example]] +name = "profiling" +required-features = ["chemfiles"] + + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin)'] } diff --git a/rascaline/benches/data/molecular_crystals.xyz b/featomic/benches/data/molecular_crystals.xyz similarity index 100% rename from rascaline/benches/data/molecular_crystals.xyz rename to featomic/benches/data/molecular_crystals.xyz diff --git a/rascaline/benches/data/silicon_bulk.xyz b/featomic/benches/data/silicon_bulk.xyz similarity index 100% rename from rascaline/benches/data/silicon_bulk.xyz rename to featomic/benches/data/silicon_bulk.xyz diff --git a/rascaline/benches/lode-spherical-expansion.rs b/featomic/benches/lode-spherical-expansion.rs similarity index 61% rename from rascaline/benches/lode-spherical-expansion.rs rename to featomic/benches/lode-spherical-expansion.rs index 60bcd2d39..e45e9c021 100644 --- a/rascaline/benches/lode-spherical-expansion.rs +++ b/featomic/benches/lode-spherical-expansion.rs @@ -1,16 +1,20 @@ #![allow(clippy::needless_return)] -use rascaline::{Calculator, System, CalculationOptions}; +use featomic::{Calculator, System, CalculationOptions}; +use chemfiles::{Frame, Trajectory}; use criterion::{BenchmarkGroup, Criterion, measurement::WallTime, SamplingMode}; use criterion::{criterion_group, criterion_main}; -fn load_systems(path: &str) -> Vec> { - let systems = rascaline::systems::read_from_file(format!("benches/data/{}", path)) - .expect("failed to read file"); +fn read_systems_from_file(path: &str) -> Vec { + let mut trajectory = Trajectory::open(path, 'r').expect("could not open the trajectory"); + let mut frame = Frame::new(); + let mut systems = Vec::new(); + for step in 0..trajectory.nsteps() { + trajectory.read_step(step, &mut frame).expect("failed to read single frame"); + systems.push((&frame).into()); + } - return systems.into_iter() - .map(|s| Box::new(s) as Box) - .collect() + systems } fn run_spherical_expansion(mut group: BenchmarkGroup, @@ -18,7 +22,8 @@ fn run_spherical_expansion(mut group: BenchmarkGroup, gradients: bool, test_mode: bool, ) { - let mut systems = load_systems(path); + + let mut systems = read_systems_from_file(&format!("benches/data/{}", path)); if test_mode { // Reduce the time/RAM required to test the benchmarks code. @@ -26,26 +31,32 @@ fn run_spherical_expansion(mut group: BenchmarkGroup, systems.truncate(1); } - let cutoff = 4.0; + let gto_radius = 4.0; let mut n_centers = 0; for system in &mut systems { n_centers += system.size().unwrap(); - system.compute_neighbors(cutoff).unwrap(); } - for atomic_gaussian_width in &[1.5, 1.0, 0.5] { + for smearing in &[1.5, 1.0, 0.5] { let parameters = format!(r#"{{ - "max_radial": 6, - "max_angular": 6, - "cutoff": {cutoff}, - "atomic_gaussian_width": {atomic_gaussian_width}, - "center_atom_weight": 1.0, - "radial_basis": {{ "Gto": {{}} }}, - "potential_exponent": 1 + "density": {{ + "type": "SmearedPowerLaw", + "smearing": {smearing}, + "exponent": 1 + }}, + "basis": {{ + "type": "TensorProduct", + "max_angular": 6, + "radial": {{ + "type": "Gto", + "max_radial": 6, + "radius": {gto_radius} + }} + }} }}"#); let mut calculator = Calculator::new("lode_spherical_expansion", parameters).unwrap(); - group.bench_function(format!("gaussian_width = {}", atomic_gaussian_width), |b| b.iter_custom(|repeat| { + group.bench_function(format!("smearing = {}", smearing), |b| b.iter_custom(|repeat| { let start = std::time::Instant::now(); let options = CalculationOptions { diff --git a/rascaline/benches/soap-power-spectrum.rs b/featomic/benches/soap-power-spectrum.rs similarity index 67% rename from rascaline/benches/soap-power-spectrum.rs rename to featomic/benches/soap-power-spectrum.rs index ae0a34772..f86a42310 100644 --- a/rascaline/benches/soap-power-spectrum.rs +++ b/featomic/benches/soap-power-spectrum.rs @@ -1,18 +1,21 @@ #![allow(clippy::needless_return)] - -use rascaline::{Calculator, System, CalculationOptions}; +use featomic::{Calculator, System, CalculationOptions}; +use chemfiles::{Frame, Trajectory}; use criterion::{BenchmarkGroup, Criterion, measurement::WallTime, SamplingMode}; use criterion::{criterion_group, criterion_main}; -fn load_systems(path: &str) -> Vec> { - let systems = rascaline::systems::read_from_file(format!("benches/data/{}", path)) - .expect("failed to read file"); +fn read_systems_from_file(path: &str) -> Vec { + let mut trajectory = Trajectory::open(path, 'r').expect("could not open the trajectory"); + let mut frame = Frame::new(); + let mut systems = Vec::new(); + for step in 0..trajectory.nsteps() { + trajectory.read_step(step, &mut frame).expect("failed to read single frame"); + systems.push((&frame).into()); + } - return systems.into_iter() - .map(|s| Box::new(s) as Box) - .collect() + systems } fn run_soap_power_spectrum( @@ -21,7 +24,7 @@ fn run_soap_power_spectrum( gradients: bool, test_mode: bool, ) { - let mut systems = load_systems(path); + let mut systems = read_systems_from_file(&format!("benches/data/{}", path)); if test_mode { // Reduce the time/RAM required to test the benchmarks code. @@ -36,9 +39,9 @@ fn run_soap_power_spectrum( system.compute_neighbors(cutoff).unwrap(); } - for &(max_radial, max_angular) in &[(2, 1), (8, 7), (15, 14)] { + for &max_basis in &[1, 7, 14] { // keep the memory requirements under control - if max_radial == 15 { + if max_basis == 14 { systems.truncate(10); n_centers = 0; for system in &mut systems { @@ -47,17 +50,30 @@ fn run_soap_power_spectrum( } let parameters = format!(r#"{{ - "max_radial": {max_radial}, - "max_angular": {max_angular}, - "cutoff": {cutoff}, - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "radial_basis": {{ "Gto": {{}} }}, - "cutoff_function": {{ "ShiftedCosine": {{ "width": 0.5 }} }} + "cutoff": {{ + "radius": {cutoff}, + "smoothing": {{ + "type": "ShiftedCosine", + "width": 0.5 + }} + }}, + "density": {{ + "type": "Gaussian", + "width": 0.3 + }}, + "basis": {{ + "type": "TensorProduct", + "max_angular": {max_basis}, + "radial": {{ + "type": "Gto", + "max_radial": {max_basis} + }} + }} }}"#); + let mut calculator = Calculator::new("soap_power_spectrum", parameters).unwrap(); - group.bench_function(format!("n_max = {}, l_max = {}", max_radial, max_angular), |b| b.iter_custom(|repeat| { + group.bench_function(format!("max_radial = max_angular = {}", max_basis), |b| b.iter_custom(|repeat| { let start = std::time::Instant::now(); let options = CalculationOptions { diff --git a/rascaline/benches/soap-spherical-expansion.rs b/featomic/benches/soap-spherical-expansion.rs similarity index 67% rename from rascaline/benches/soap-spherical-expansion.rs rename to featomic/benches/soap-spherical-expansion.rs index a460ca622..d1a63eadf 100644 --- a/rascaline/benches/soap-spherical-expansion.rs +++ b/featomic/benches/soap-spherical-expansion.rs @@ -1,24 +1,29 @@ #![allow(clippy::needless_return)] -use rascaline::{Calculator, System, CalculationOptions}; +use featomic::{Calculator, System, CalculationOptions}; +use chemfiles::{Frame, Trajectory}; use criterion::{BenchmarkGroup, Criterion, measurement::WallTime, SamplingMode}; use criterion::{criterion_group, criterion_main}; -fn load_systems(path: &str) -> Vec> { - let systems = rascaline::systems::read_from_file(format!("benches/data/{}", path)) - .expect("failed to read file"); +fn read_systems_from_file(path: &str) -> Vec { + let mut trajectory = Trajectory::open(path, 'r').expect("could not open the trajectory"); + let mut frame = Frame::new(); + let mut systems = Vec::new(); + for step in 0..trajectory.nsteps() { + trajectory.read_step(step, &mut frame).expect("failed to read single frame"); + systems.push((&frame).into()); + } - return systems.into_iter() - .map(|s| Box::new(s) as Box) - .collect() + systems } + fn run_spherical_expansion(mut group: BenchmarkGroup, path: &str, gradients: bool, test_mode: bool, ) { - let mut systems = load_systems(path); + let mut systems = read_systems_from_file(&format!("benches/data/{}", path)); if test_mode { // Reduce the time/RAM required to test the benchmarks code. @@ -33,9 +38,9 @@ fn run_spherical_expansion(mut group: BenchmarkGroup, system.compute_neighbors(cutoff).unwrap(); } - for &(max_radial, max_angular) in &[(2, 1), (8, 7), (15, 14)] { + for &max_basis in &[1, 7, 14] { // keep the memory requirements under control - if max_radial == 15 { + if max_basis == 14 { systems.truncate(10); n_centers = 0; for system in &mut systems { @@ -44,17 +49,29 @@ fn run_spherical_expansion(mut group: BenchmarkGroup, } let parameters = format!(r#"{{ - "max_radial": {max_radial}, - "max_angular": {max_angular}, - "cutoff": {cutoff}, - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "radial_basis": {{ "Gto": {{}} }}, - "cutoff_function": {{ "ShiftedCosine": {{ "width": 0.5 }} }} + "cutoff": {{ + "radius": {cutoff}, + "smoothing": {{ + "type": "ShiftedCosine", + "width": 0.5 + }} + }}, + "density": {{ + "type": "Gaussian", + "width": 0.3 + }}, + "basis": {{ + "type": "TensorProduct", + "max_angular": {max_basis}, + "radial": {{ + "type": "Gto", + "max_radial": {max_basis} + }} + }} }}"#); let mut calculator = Calculator::new("spherical_expansion", parameters).unwrap(); - group.bench_function(format!("n_max = {}, l_max = {}", max_radial, max_angular), |b| b.iter_custom(|repeat| { + group.bench_function(format!("max_radial = max_angular = {}", max_basis), |b| b.iter_custom(|repeat| { let start = std::time::Instant::now(); let options = CalculationOptions { diff --git a/rascaline/benches/spherical-harmonics.rs b/featomic/benches/spherical-harmonics.rs similarity index 97% rename from rascaline/benches/spherical-harmonics.rs rename to featomic/benches/spherical-harmonics.rs index 4eea7837a..f9f570902 100644 --- a/rascaline/benches/spherical-harmonics.rs +++ b/featomic/benches/spherical-harmonics.rs @@ -1,5 +1,5 @@ -use rascaline::Vector3D; -use rascaline::math::{SphericalHarmonics, SphericalHarmonicsArray}; +use featomic::Vector3D; +use featomic::math::{SphericalHarmonics, SphericalHarmonicsArray}; use criterion::{Criterion, black_box, criterion_group, criterion_main}; diff --git a/featomic/build.rs b/featomic/build.rs new file mode 100644 index 000000000..93dc80c12 --- /dev/null +++ b/featomic/build.rs @@ -0,0 +1,53 @@ +#![allow(clippy::field_reassign_with_default)] + +use std::path::PathBuf; + +fn main() { + if cfg!(feature = "c-api") { + let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + + let generated_comment = "\ +/* ============ Automatically generated file, DOT NOT EDIT. ============ * + * * + * This file is automatically generated from the featomic sources, * + * using cbindgen. If you want to make change to this file (including * + * documentation), make the corresponding changes in the rust sources * + * in `featomic/src/c_api/` * + * =========================================================================== */"; + + let mut config: cbindgen::Config = Default::default(); + config.language = cbindgen::Language::C; + config.cpp_compat = true; + config.no_includes = true; + config.sys_includes = vec![ + "stdbool.h".into(), + "stdint.h".into(), + "metatensor.h".into() + ]; + config.include_guard = Some("FEATOMIC_H".into()); + config.include_version = false; + config.documentation = true; + config.documentation_style = cbindgen::DocumentationStyle::Doxy; + config.header = Some(generated_comment.into()); + + let result = cbindgen::Builder::new() + .with_crate(crate_dir) + .with_config(config) + .generate() + .map(|data| { + let mut path = PathBuf::from("include"); + path.push("featomic.h"); + data.write_to_file(&path); + }); + + if result.is_ok() { + println!("cargo:rerun-if-changed=src/c_api"); + } else { + // if featomic header generation failed, we always re-run the build script + } + } + + println!( + "cargo:rustc-env=TARGET={}", std::env::var("TARGET").expect("missing TARGET env variable") + ); +} diff --git a/featomic/cmake/dev-versions.cmake b/featomic/cmake/dev-versions.cmake new file mode 100644 index 000000000..fd350df96 --- /dev/null +++ b/featomic/cmake/dev-versions.cmake @@ -0,0 +1,96 @@ +# Parse a `_version_` number, and store its components in `_major_` `_minor_` +# `_patch_` and `_rc_` +function(parse_version _version_ _major_ _minor_ _patch_ _rc_) + string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)(-rc)?([0-9]+)?" _ "${_version_}") + + if(${CMAKE_MATCH_COUNT} EQUAL 3) + set(${_rc_} "" PARENT_SCOPE) + elseif(${CMAKE_MATCH_COUNT} EQUAL 5) + set(${_rc_} ${CMAKE_MATCH_5} PARENT_SCOPE) + else() + message(FATAL_ERROR "invalid version string ${_version_}") + endif() + + set(${_major_} ${CMAKE_MATCH_1} PARENT_SCOPE) + set(${_minor_} ${CMAKE_MATCH_2} PARENT_SCOPE) + set(${_patch_} ${CMAKE_MATCH_3} PARENT_SCOPE) +endfunction() + +if (CMAKE_VERSION VERSION_LESS "3.17") + # CMAKE_CURRENT_FUNCTION_LIST_DIR was added in CMake 3.17 + set(CMAKE_CURRENT_FUNCTION_LIST_DIR "${CMAKE_CURRENT_LIST_DIR}") +endif() + +# Get the time of the last modification since the last tag/release, and a hash +# of the latest commit/full state of a dirty repository +function(git_version_info _tag_prefix_ _output_n_commits_ _output_git_hash_) + set(_script_ "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../../scripts/git-version-info.py") + + if (EXISTS "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/git_version_info") + # When building from a tarball, the script is executed and the result + # put in this file + file(STRINGS "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/git_version_info" _file_content_) + list(GET _file_content_ 0 _n_commits_) + list(GET _file_content_ 1 _git_hash_) + + elseif (EXISTS "${_script_}") + # When building from a checkout, we'll need to run the script + find_package(Python COMPONENTS Interpreter REQUIRED) + execute_process( + COMMAND "${Python_EXECUTABLE}" "${_script_}" "${_tag_prefix_}" + RESULT_VARIABLE _status_ + OUTPUT_VARIABLE _stdout_ + ERROR_VARIABLE _stderr_ + WORKING_DIRECTORY ${CMAKE_CURRENT_FUNCTION_LIST_DIR} + ) + + if (NOT ${_status_} EQUAL 0) + message(WARNING + "git-version-info.py failed, version number might be wrong:\nstdout: ${_stdout_}\nstderr: ${_stderr_}") + set(${_output_} 0 PARENT_SCOPE) + return() + endif() + + if (NOT "${_stderr_}" STREQUAL "") + message(WARNING "git-version-info.py gave some errors, version number might be wrong:\nstdout: ${_stdout_}\nstderr: ${_stderr_}") + endif() + + string(REPLACE "\n" ";" _lines_ ${_stdout_}) + list(GET _lines_ 0 _n_commits_) + list(GET _lines_ 1 _git_hash_) + else() + message(FATAL_ERROR "could not update git version information") + endif() + + string(STRIP ${_n_commits_} _n_commits_) + set(${_output_n_commits_} ${_n_commits_} PARENT_SCOPE) + + string(STRIP ${_git_hash_} _git_hash_) + set(${_output_git_hash_} ${_git_hash_} PARENT_SCOPE) +endfunction() + + +# Take the version declared in the package, and increase the right number if we +# are actually installing a developement version from after the latest git tag +function(create_development_version _version_ _output_ _tag_prefix_) + git_version_info("${_tag_prefix_}" _n_commits_ _git_hash_) + + parse_version(${_version_} _major_ _minor_ _patch_ _rc_) + if(${_n_commits_} STREQUAL "0") + # we are building a release, leave the version number as-is + if("${_rc_}" STREQUAL "") + set(${_output_} "${_major_}.${_minor_}.${_patch_}" PARENT_SCOPE) + else() + set(${_output_} "${_major_}.${_minor_}.${_patch_}-rc${_rc_}" PARENT_SCOPE) + endif() + else() + # we are building a development version, increase the right part of the version + if("${_rc_}" STREQUAL "") + math(EXPR _minor_ "${_minor_} + 1") + set(${_output_} "${_major_}.${_minor_}.0-dev${_n_commits_}+${_git_hash_}" PARENT_SCOPE) + else() + math(EXPR _rc_ "${_rc_} + 1") + set(${_output_} "${_major_}.${_minor_}.${_patch_}-rc${_rc_}-dev${_n_commits_}+${_git_hash_}" PARENT_SCOPE) + endif() + endif() +endfunction() diff --git a/rascaline-c-api/cmake/rascaline-config-version.in.cmake b/featomic/cmake/featomic-config-version.in.cmake similarity index 100% rename from rascaline-c-api/cmake/rascaline-config-version.in.cmake rename to featomic/cmake/featomic-config-version.in.cmake diff --git a/featomic/cmake/featomic-config.in.cmake b/featomic/cmake/featomic-config.in.cmake new file mode 100644 index 000000000..cb0458c69 --- /dev/null +++ b/featomic/cmake/featomic-config.in.cmake @@ -0,0 +1,81 @@ +@PACKAGE_INIT@ + +cmake_minimum_required(VERSION 3.16) + +if(featomic_FOUND) + return() +endif() + +enable_language(CXX) + +get_filename_component(FEATOMIC_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/@PACKAGE_RELATIVE_PATH@" ABSOLUTE) + +if (WIN32) + set(FEATOMIC_SHARED_LOCATION ${FEATOMIC_PREFIX_DIR}/@BIN_INSTALL_DIR@/@FEATOMIC_SHARED_LIB_NAME@) + set(FEATOMIC_IMPLIB_LOCATION ${FEATOMIC_PREFIX_DIR}/@LIB_INSTALL_DIR@/@FEATOMIC_IMPLIB_NAME@) +else() + set(FEATOMIC_SHARED_LOCATION ${FEATOMIC_PREFIX_DIR}/@LIB_INSTALL_DIR@/@FEATOMIC_SHARED_LIB_NAME@) +endif() + +set(FEATOMIC_STATIC_LOCATION ${FEATOMIC_PREFIX_DIR}/@LIB_INSTALL_DIR@/@FEATOMIC_STATIC_LIB_NAME@) +set(FEATOMIC_INCLUDE ${FEATOMIC_PREFIX_DIR}/@INCLUDE_INSTALL_DIR@/) + +if (NOT EXISTS ${FEATOMIC_INCLUDE}/featomic.h OR NOT EXISTS ${FEATOMIC_INCLUDE}/featomic.hpp) + message(FATAL_ERROR "could not find featomic headers in '${FEATOMIC_INCLUDE}', please re-install featomic") +endif() + +find_package(metatensor @METATENSOR_REQUIRED_VERSION@ REQUIRED CONFIG) + +# Shared library target +if (@FEATOMIC_INSTALL_BOTH_STATIC_SHARED@ OR @BUILD_SHARED_LIBS@) + if (NOT EXISTS ${FEATOMIC_SHARED_LOCATION}) + message(FATAL_ERROR "could not find featomic library at '${FEATOMIC_SHARED_LOCATION}', please re-install featomic") + endif() + + add_library(featomic::shared SHARED IMPORTED GLOBAL) + set_target_properties(featomic::shared PROPERTIES + IMPORTED_LOCATION ${FEATOMIC_SHARED_LOCATION} + INTERFACE_INCLUDE_DIRECTORIES ${FEATOMIC_INCLUDE} + BUILD_VERSION "@FEATOMIC_FULL_VERSION@" + ) + target_link_libraries(featomic::shared INTERFACE metatensor::shared) + + target_compile_features(featomic::shared INTERFACE cxx_std_17) + + if (WIN32) + if (NOT EXISTS ${FEATOMIC_IMPLIB_LOCATION}) + message(FATAL_ERROR "could not find featomic library at '${FEATOMIC_IMPLIB_LOCATION}', please re-install featomic") + endif() + + set_target_properties(featomic::shared PROPERTIES + IMPORTED_IMPLIB ${FEATOMIC_IMPLIB_LOCATION} + ) + endif() +endif() + + +# Static library target +if (@FEATOMIC_INSTALL_BOTH_STATIC_SHARED@ OR NOT @BUILD_SHARED_LIBS@) + if (NOT EXISTS ${FEATOMIC_STATIC_LOCATION}) + message(FATAL_ERROR "could not find featomic library at '${FEATOMIC_STATIC_LOCATION}', please re-install featomic") + endif() + + add_library(featomic::static STATIC IMPORTED GLOBAL) + set_target_properties(featomic::static PROPERTIES + IMPORTED_LOCATION ${FEATOMIC_STATIC_LOCATION} + INTERFACE_INCLUDE_DIRECTORIES ${FEATOMIC_INCLUDE} + INTERFACE_LINK_LIBRARIES "@CARGO_DEFAULT_LIBRARIES@" + BUILD_VERSION "@FEATOMIC_FULL_VERSION@" + ) + target_link_libraries(featomic::static INTERFACE metatensor::shared) + + target_compile_features(featomic::static INTERFACE cxx_std_17) +endif() + + +# Export either the shared or static library as the featomic target +if (@BUILD_SHARED_LIBS@) + add_library(featomic ALIAS featomic::shared) +else() + add_library(featomic ALIAS featomic::static) +endif() diff --git a/rascaline-c-api/cmake/tempdir.cmake b/featomic/cmake/tempdir.cmake similarity index 100% rename from rascaline-c-api/cmake/tempdir.cmake rename to featomic/cmake/tempdir.cmake diff --git a/featomic/examples/common/systems.c b/featomic/examples/common/systems.c new file mode 100644 index 000000000..a5d7af91f --- /dev/null +++ b/featomic/examples/common/systems.c @@ -0,0 +1,186 @@ +#include + +#include + +#include "systems.h" + + +// This file shows an example on how to define a custom `featomic_system_t`, +// based on chemfiles (https://chemfiles.org). +// +// The core idea is to define a set of functions to communicate data (atomic +// types, positions, periodic cell, …) between your code and featomic. Each +// function takes a `void* user_data` parameter, which you can set to anything +// relevant. + +typedef struct { + uint64_t size; + CHFL_FRAME* frame; + int32_t* atom_types; +} chemfiles_system_t; + +static featomic_status_t chemfiles_system_size(const void* system, uintptr_t* size) { + *size = ((const chemfiles_system_t*)system)->size; + return FEATOMIC_SUCCESS; +} + +static featomic_status_t chemfiles_system_cell(const void* system, double cell[9]) { + CHFL_CELL* chemfiles_cell = chfl_cell_from_frame(((const chemfiles_system_t*)system)->frame); + if (chemfiles_cell == NULL) { + printf("Error: %s", chfl_last_error()); + return -1; + } + + chfl_status status = chfl_cell_matrix(chemfiles_cell, (chfl_vector3d*)cell); + chfl_free(chemfiles_cell); + + if (status == CHFL_SUCCESS) { + return FEATOMIC_SUCCESS; + } else { + printf("Error: %s", chfl_last_error()); + return -2; + } +} + +static featomic_status_t chemfiles_system_positions(const void* system, const double** positions) { + chfl_vector3d* chemfiles_positions = NULL; + uint64_t size = 0; + chfl_status status = chfl_frame_positions( + ((const chemfiles_system_t*)system)->frame, + &chemfiles_positions, + &size + ); + + if (status == CHFL_SUCCESS) { + *positions = (const double*)chemfiles_positions; + return FEATOMIC_SUCCESS; + } else { + return -1; + } +} + +static featomic_status_t chemfiles_system_types(const void* system, const int32_t** types) { + *types = ((const chemfiles_system_t*)system)->atom_types; + return FEATOMIC_SUCCESS; +} + +static featomic_status_t chemfiles_system_neighbors(void* system, double cutoff) { + // this system does not have a neighbor list, and needs to use the one + // inside featomic with `use_native_system=true` + return -1; +} + + +int read_systems_example(const char* path, featomic_system_t** systems, uintptr_t* n_systems) { + CHFL_TRAJECTORY* trajectory = NULL; + CHFL_ATOM* atom = NULL; + chemfiles_system_t* system = NULL; + chfl_status status; + uint64_t step = 0; + uint64_t n_steps = 0; + + trajectory = chfl_trajectory_open(path, 'r'); + if (trajectory == NULL) { + printf("Error: %s", chfl_last_error()); + goto error; + } + + status = chfl_trajectory_nsteps(trajectory, &n_steps); + if (status != CHFL_SUCCESS) { + printf("Error: %s", chfl_last_error()); + goto error; + } + + *systems = calloc(n_steps, sizeof(featomic_system_t)); + if (*systems == NULL) { + printf("Error: Failed to allocate systems"); + goto error; + } + *n_systems = (uintptr_t)n_steps; + + for (step=0; stepframe = chfl_frame(); + if (system->frame == NULL) { + printf("Error: %s", chfl_last_error()); + goto error; + } + + status = chfl_trajectory_read_step(trajectory, step, system->frame); + if (status != CHFL_SUCCESS) { + printf("Error: %s", chfl_last_error()); + goto error; + } + + // extract atomic types from the frame + chfl_frame_atoms_count(system->frame, &system->size); + if (status != CHFL_SUCCESS) { + printf("Error: %s", chfl_last_error()); + goto error; + } + + system->atom_types = calloc(system->size, sizeof(int32_t)); + if (system->atom_types == NULL) { + printf("Error: failed to allocate atom_types"); + goto error; + } + + for (uint64_t i=0; isize; i++) { + atom = chfl_atom_from_frame(system->frame, i); + if (atom == NULL) { + printf("Error: %s", chfl_last_error()); + goto error; + } + + uint64_t number = 0; + chfl_atom_atomic_number(atom, &number); + system->atom_types[i] = (int32_t)(number); + + chfl_free(atom); + } + + // setup all the data and functions for the system + (*systems)[step].user_data = system; + + (*systems)[step].size = chemfiles_system_size; + (*systems)[step].cell = chemfiles_system_cell; + (*systems)[step].positions = chemfiles_system_positions; + (*systems)[step].types = chemfiles_system_types; + (*systems)[step].compute_neighbors = chemfiles_system_neighbors; + (*systems)[step].pairs = NULL; + (*systems)[step].pairs_containing = NULL; + + system = NULL; + } + + chfl_free(trajectory); + return 0; + +error: + chfl_free(trajectory); + chfl_free(atom); + + // cleanup any sucesfully allocated frames + free_systems_example(*systems, step); + + *systems = NULL; + *n_systems = 0; + + return 1; +} + +void free_systems_example(featomic_system_t* systems, uintptr_t n_systems) { + for (size_t i=0; iframe); + free(system->atom_types); + free(system); + } + + free(systems); +} diff --git a/featomic/examples/common/systems.h b/featomic/examples/common/systems.h new file mode 100644 index 000000000..bc0002145 --- /dev/null +++ b/featomic/examples/common/systems.h @@ -0,0 +1,9 @@ +#ifndef FEATOMIC_EXAMPLE_SYSTEMS_H +#define FEATOMIC_EXAMPLE_SYSTEMS_H + +#include + +int read_systems_example(const char* path, featomic_system_t** systems, uintptr_t* n_systems); +void free_systems_example(featomic_system_t* systems, uintptr_t n_systems); + +#endif diff --git a/rascaline-c-api/examples/compute-soap.c b/featomic/examples/compute-soap.c similarity index 79% rename from rascaline-c-api/examples/compute-soap.c rename to featomic/examples/compute-soap.c index 32f01b736..2fc506b53 100644 --- a/rascaline-c-api/examples/compute-soap.c +++ b/featomic/examples/compute-soap.c @@ -2,16 +2,18 @@ #include #include -#include +#include #include +#include "common/systems.h" + static mts_tensormap_t* move_keys_to_samples(mts_tensormap_t* descriptor, const char* keys_to_move[], size_t keys_to_move_len); static mts_tensormap_t* move_keys_to_properties(mts_tensormap_t* descriptor, const char* keys_to_move[], size_t keys_to_move_len); int main(int argc, char* argv[]) { - int status = RASCAL_SUCCESS; - rascal_calculator_t* calculator = NULL; - rascal_system_t* systems = NULL; + int status = FEATOMIC_SUCCESS; + featomic_calculator_t* calculator = NULL; + featomic_system_t* systems = NULL; uintptr_t n_systems = 0; double* values = NULL; const uintptr_t* shape = NULL; @@ -21,10 +23,11 @@ int main(int argc, char* argv[]) { const char* keys_to_properties[] = {"neighbor_1_type", "neighbor_2_type"}; // use the default set of options, computing all samples and all features, // and including gradients with respect to positions - rascal_calculation_options_t options = {0}; + featomic_calculation_options_t options = {0}; const char* gradients_list[] = {"positions"}; options.gradients = gradients_list; options.gradients_count = 1; + options.use_native_system = true; mts_tensormap_t* descriptor = NULL; mts_block_t* block = NULL; @@ -32,17 +35,18 @@ int main(int argc, char* argv[]) { // hyper-parameters for the calculation as JSON const char* parameters = "{\n" - "\"cutoff\": 5.0,\n" - "\"max_radial\": 6,\n" - "\"max_angular\": 4,\n" - "\"atomic_gaussian_width\": 0.3,\n" - "\"center_atom_weight\": 1.0,\n" - "\"gradients\": false,\n" - "\"radial_basis\": {\n" - " \"Gto\": {}\n" + "\"cutoff\": {\n" + " \"radius\": 5.0,\n" + " \"smoothing\": {\"type\": \"ShiftedCosine\", \"width\": 0.5}\n" + "},\n" + "\"density\": {\n" + " \"type\": \"Gaussian\",\n" + " \"width\": 0.3\n" "},\n" - "\"cutoff_function\": {\n" - " \"ShiftedCosine\": {\"width\": 0.5}\n" + "\"basis\": {\n" + " \"type\": \"TensorProduct\",\n" + " \"max_angular\": 6,\n" + " \"radial\": {\"type\": \"Gto\", \"max_radial\": 6}\n" "}\n" "}"; @@ -51,25 +55,24 @@ int main(int argc, char* argv[]) { printf("error: expected a command line argument"); goto cleanup; } - status = rascal_basic_systems_read(argv[1], &systems, &n_systems); - if (status != RASCAL_SUCCESS) { - printf("Error: %s\n", rascal_last_error()); + status = read_systems_example(argv[1], &systems, &n_systems); + if (status != 0) { goto cleanup; } // create the calculator with its name and parameters - calculator = rascal_calculator("soap_power_spectrum", parameters); + calculator = featomic_calculator("soap_power_spectrum", parameters); if (calculator == NULL) { - printf("Error: %s\n", rascal_last_error()); + printf("Error: %s\n", featomic_last_error()); goto cleanup; } // run the calculation - status = rascal_calculator_compute( + status = featomic_calculator_compute( calculator, &descriptor, systems, n_systems, options ); - if (status != RASCAL_SUCCESS) { - printf("Error: %s\n", rascal_last_error()); + if (status != FEATOMIC_SUCCESS) { + printf("Error: %s\n", featomic_last_error()); goto cleanup; } @@ -126,8 +129,9 @@ int main(int argc, char* argv[]) { got_error = false; cleanup: mts_tensormap_free(descriptor); - rascal_calculator_free(calculator); - rascal_basic_systems_free(systems, n_systems); + featomic_calculator_free(calculator); + + free_systems_example(systems, n_systems); if (got_error) { return 1; @@ -167,3 +171,5 @@ mts_tensormap_t* move_keys_to_properties(mts_tensormap_t* descriptor, const char return moved_descriptor; } + +#include "common/systems.c" diff --git a/featomic/examples/compute-soap.cpp b/featomic/examples/compute-soap.cpp new file mode 100644 index 000000000..2d34c9ee1 --- /dev/null +++ b/featomic/examples/compute-soap.cpp @@ -0,0 +1,83 @@ +#include +#include +#include + +static std::vector read_systems(const std::string& path); + +int main(int argc, char* argv[]) { + if (argc < 2) { + std::cout << "error: expected a command line argument" << std::endl; + return 1; + } + auto systems = read_systems(argv[1]); + + // pass hyper-parameters as JSON + const char* parameters = R"({ + "cutoff": { + "radius": 5.0, + "smoothing": {"type": "ShiftedCosine", "width": 0.5} + }, + "density": { + "type": "Gaussian", + "width": 0.3 + }, + "basis": { + "type": "TensorProduct", + "max_angular": 6, + "radial": {"type": "Gto", "max_radial": 6} + } + })"; + + // create the calculator with its name and parameters + auto calculator = featomic::Calculator("soap_power_spectrum", parameters); + + // setup the calculation options + auto options = featomic::CalculationOptions(); + options.use_native_system = true; + options.gradients.push_back("positions"); + + // run the calculation + auto descriptor = calculator.compute(systems, options); + + // The descriptor is a metatensor `TensorMap`, containing multiple blocks. + // We can transform it to a single block containing a dense representation, + // with one sample for each atom-centered environment. + descriptor.keys_to_samples("center_type"); + descriptor.keys_to_properties(std::vector{"neighbor_1_type", "neighbor_2_type"}); + + // extract values from the descriptor in the only remaining block + auto block = descriptor.block_by_id(0); + auto values = block.values(); + + // you can now use values as the input of a machine learning algorithm + + return 0; +} + + +std::vector read_systems(const std::string& path) { + auto trajectory = chemfiles::Trajectory(path); + auto systems = std::vector(); + for (size_t step=0; step(frame[i].atomic_number().value_or(0)), + {positions[i][0], positions[i][1], positions[i][2]} + ); + } + + systems.push_back(system); + } + + return systems; +} diff --git a/rascaline/examples/compute-soap.rs b/featomic/examples/compute-soap.rs similarity index 62% rename from rascaline/examples/compute-soap.rs rename to featomic/examples/compute-soap.rs index 42b9b1221..4aaacf941 100644 --- a/rascaline/examples/compute-soap.rs +++ b/featomic/examples/compute-soap.rs @@ -1,27 +1,41 @@ use metatensor::Labels; -use rascaline::{Calculator, System, CalculationOptions}; +use featomic::{Calculator, System, CalculationOptions}; +use chemfiles::{Trajectory, Frame}; + +fn read_systems_from_file(path: &str) -> Vec { + let mut trajectory = Trajectory::open(path, 'r').expect("could not open the trajectory"); + let mut frame = Frame::new(); + let mut systems = Vec::new(); + for step in 0..trajectory.nsteps() { + trajectory.read_step(step, &mut frame).expect("failed to read single frame"); + systems.push((&frame).into()); + } + + systems +} fn main() -> Result<(), Box> { // load the systems from command line argument let path = std::env::args().nth(1).expect("expected a command line argument"); - let systems = rascaline::systems::read_from_file(path)?; - // transform systems into a vector of trait objects (`Vec>`) - let mut systems = systems.into_iter() - .map(|s| Box::new(s) as Box) - .collect::>(); + let mut systems = read_systems_from_file(&path); // pass hyper-parameters as JSON let parameters = r#"{ - "cutoff": 5.0, - "max_radial": 6, - "max_angular": 4, - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "radial_basis": { - "Gto": {} + "cutoff": { + "radius": 5.0, + "smoothing": { + "type": "ShiftedCosine", + "width": 0.5 + } + }, + "density": { + "type": "Gaussian", + "width": 0.3 }, - "cutoff_function": { - "ShiftedCosine": {"width": 0.5} + "basis": { + "type": "TensorProduct", + "max_angular": 4, + "radial": {"type": "Gto", "max_radial": 6} } }"#; // create the calculator with its name and parameters diff --git a/rascaline/examples/data/water.xyz b/featomic/examples/data/water.xyz similarity index 100% rename from rascaline/examples/data/water.xyz rename to featomic/examples/data/water.xyz diff --git a/rascaline-c-api/examples/profiling.c b/featomic/examples/profiling.c similarity index 67% rename from rascaline-c-api/examples/profiling.c rename to featomic/examples/profiling.c index 9f9c51a76..0b4c93be1 100644 --- a/rascaline-c-api/examples/profiling.c +++ b/featomic/examples/profiling.c @@ -2,14 +2,16 @@ #include #include -#include +#include + +#include "common/systems.h" /// Compute SOAP power spectrum, this is the same code as the 'compute-soap' /// example static mts_tensormap_t* compute_soap(const char* path); int main(int argc, char* argv[]) { - rascal_status_t status = RASCAL_SUCCESS; + featomic_status_t status = FEATOMIC_SUCCESS; char* buffer = NULL; size_t buffer_size = 8192; bool got_error = true; @@ -20,16 +22,16 @@ int main(int argc, char* argv[]) { } // enable collection of profiling data - status = rascal_profiling_enable(true); - if (status != RASCAL_SUCCESS) { - printf("Error: %s\n", rascal_last_error()); + status = featomic_profiling_enable(true); + if (status != FEATOMIC_SUCCESS) { + printf("Error: %s\n", featomic_last_error()); goto cleanup; } // clear any existing collected data - status = rascal_profiling_clear(); - if (status != RASCAL_SUCCESS) { - printf("Error: %s\n", rascal_last_error()); + status = featomic_profiling_clear(); + if (status != FEATOMIC_SUCCESS) { + printf("Error: %s\n", featomic_last_error()); goto cleanup; } @@ -45,17 +47,17 @@ int main(int argc, char* argv[]) { } // Get the profiling data as a table to display it directly - status = rascal_profiling_get("short_table", buffer, buffer_size); - if (status != RASCAL_SUCCESS) { - printf("Error: %s\n", rascal_last_error()); + status = featomic_profiling_get("short_table", buffer, buffer_size); + if (status != FEATOMIC_SUCCESS) { + printf("Error: %s\n", featomic_last_error()); goto cleanup; } printf("%s\n", buffer); // Or save this data as json for future usage - status = rascal_profiling_get("json", buffer, buffer_size); - if (status != RASCAL_SUCCESS) { - printf("Error: %s\n", rascal_last_error()); + status = featomic_profiling_get("json", buffer, buffer_size); + if (status != FEATOMIC_SUCCESS) { + printf("Error: %s\n", featomic_last_error()); goto cleanup; } printf("%s\n", buffer); @@ -77,9 +79,9 @@ static mts_tensormap_t* move_keys_to_properties(mts_tensormap_t* descriptor, con // this is the same function as in the compute-soap.c example mts_tensormap_t* compute_soap(const char* path) { - int status = RASCAL_SUCCESS; - rascal_calculator_t* calculator = NULL; - rascal_system_t* systems = NULL; + int status = FEATOMIC_SUCCESS; + featomic_calculator_t* calculator = NULL; + featomic_system_t* systems = NULL; uintptr_t n_systems = 0; const double* values = NULL; const uintptr_t* shape = NULL; @@ -89,10 +91,11 @@ mts_tensormap_t* compute_soap(const char* path) { const char* keys_to_properties[] = {"neighbor_1_type", "neighbor_2_type"}; // use the default set of options, computing all samples and all features - rascal_calculation_options_t options = {0}; + featomic_calculation_options_t options = {0}; const char* gradients_list[] = {"positions"}; options.gradients = gradients_list; options.gradients_count = 1; + options.use_native_system = true; mts_tensormap_t* descriptor = NULL; const mts_block_t* block = NULL; @@ -100,38 +103,39 @@ mts_tensormap_t* compute_soap(const char* path) { mts_labels_t keys_to_move = {0}; const char* parameters = "{\n" - "\"cutoff\": 5.0,\n" - "\"max_radial\": 6,\n" - "\"max_angular\": 4,\n" - "\"atomic_gaussian_width\": 0.3,\n" - "\"center_atom_weight\": 1.0,\n" - "\"gradients\": false,\n" - "\"radial_basis\": {\n" - " \"Gto\": {}\n" + "\"cutoff\": {\n" + " \"radius\": 5.0,\n" + " \"smoothing\": {\"type\": \"ShiftedCosine\", \"width\": 0.5}\n" + "},\n" + "\"density\": {\n" + " \"type\": \"Gaussian\",\n" + " \"width\": 0.3\n" "},\n" - "\"cutoff_function\": {\n" - " \"ShiftedCosine\": {\"width\": 0.5}\n" + "\"basis\": {\n" + " \"type\": \"TensorProduct\",\n" + " \"max_angular\": 6,\n" + " \"radial\": {\"type\": \"Gto\", \"max_radial\": 6}\n" "}\n" "}"; - status = rascal_basic_systems_read(path, &systems, &n_systems); - if (status != RASCAL_SUCCESS) { - printf("Error: %s\n", rascal_last_error()); + status = read_systems_example(path, &systems, &n_systems); + if (status != FEATOMIC_SUCCESS) { + printf("Error: %s\n", featomic_last_error()); goto cleanup; } - calculator = rascal_calculator("soap_power_spectrum", parameters); + calculator = featomic_calculator("soap_power_spectrum", parameters); if (calculator == NULL) { - printf("Error: %s\n", rascal_last_error()); + printf("Error: %s\n", featomic_last_error()); goto cleanup; } - status = rascal_calculator_compute( + status = featomic_calculator_compute( calculator, &descriptor, systems, n_systems, options ); - if (status != RASCAL_SUCCESS) { - printf("Error: %s\n", rascal_last_error()); + if (status != FEATOMIC_SUCCESS) { + printf("Error: %s\n", featomic_last_error()); goto cleanup; } @@ -148,8 +152,8 @@ mts_tensormap_t* compute_soap(const char* path) { } cleanup: - rascal_calculator_free(calculator); - rascal_basic_systems_free(systems, n_systems); + featomic_calculator_free(calculator); + free_systems_example(systems, n_systems); return descriptor; } @@ -185,3 +189,6 @@ mts_tensormap_t* move_keys_to_properties(mts_tensormap_t* descriptor, const char return moved_descriptor; } + + +#include "common/systems.c" diff --git a/featomic/examples/profiling.cpp b/featomic/examples/profiling.cpp new file mode 100644 index 000000000..f51235d85 --- /dev/null +++ b/featomic/examples/profiling.cpp @@ -0,0 +1,88 @@ +#include +#include +#include + +/// Compute SOAP power spectrum, this is the same code as the 'compute-soap' +/// example +static metatensor::TensorMap compute_soap(const std::string& path); + +int main(int argc, char* argv[]) { + if (argc < 2) { + std::cout << "error: expected a command line argument" << std::endl; + return 1; + } + + // enable collection of profiling data + featomic::Profiler::enable(true); + // clear any existing collected data + featomic::Profiler::clear(); + + auto descriptor = compute_soap(argv[1]); + + // Get the profiling data as a table to display it directly + std::cout << featomic::Profiler::get("short_table") << std::endl; + // Or save this data as json for future usage + std::cout << featomic::Profiler::get("json") << std::endl; + + return 0; +} + +std::vector read_systems(const std::string& path) { + auto trajectory = chemfiles::Trajectory(path); + auto systems = std::vector(); + for (size_t step=0; step(frame[i].atomic_number().value_or(0)), + {positions[i][0], positions[i][1], positions[i][2]} + ); + } + + systems.push_back(system); + } + + return systems; +} + +metatensor::TensorMap compute_soap(const std::string& path) { + auto systems = read_systems(path); + + const char* parameters = R"({ + "cutoff": { + "radius": 5.0, + "smoothing": {"type": "ShiftedCosine", "width": 0.5} + }, + "density": { + "type": "Gaussian", + "width": 0.3 + }, + "basis": { + "type": "TensorProduct", + "max_angular": 6, + "radial": {"type": "Gto", "max_radial": 6} + } + })"; + + auto calculator = featomic::Calculator("soap_power_spectrum", parameters); + + auto options = featomic::CalculationOptions(); + options.use_native_system = true; + options.gradients.push_back("positions"); + + auto descriptor = calculator.compute(systems, options); + + descriptor.keys_to_samples("center_type"); + descriptor.keys_to_properties(std::vector{"neighbor_1_type", "neighbor_2_type"}); + + return descriptor; +} diff --git a/rascaline/examples/profiling.rs b/featomic/examples/profiling.rs similarity index 65% rename from rascaline/examples/profiling.rs rename to featomic/examples/profiling.rs index c172e610c..119cf5434 100644 --- a/rascaline/examples/profiling.rs +++ b/featomic/examples/profiling.rs @@ -1,5 +1,6 @@ use metatensor::{TensorMap, Labels}; -use rascaline::{Calculator, System, CalculationOptions}; +use featomic::{Calculator, System, CalculationOptions}; +use chemfiles::{Trajectory, Frame}; fn main() -> Result<(), Box> { let path = std::env::args().nth(1).expect("expected a command line argument"); @@ -24,26 +25,40 @@ fn main() -> Result<(), Box> { Ok(()) } +fn read_systems_from_file(path: &str) -> Vec { + let mut trajectory = Trajectory::open(path, 'r').expect("could not open the trajectory"); + let mut frame = Frame::new(); + let mut systems = Vec::new(); + for step in 0..trajectory.nsteps() { + trajectory.read_step(step, &mut frame).expect("failed to read single frame"); + systems.push((&frame).into()); + } + + systems +} + /// Compute SOAP power spectrum, this is the same code as the 'compute-soap' /// example fn compute_soap(path: &str) -> Result> { - let systems = rascaline::systems::read_from_file(path)?; - let mut systems = systems.into_iter() - .map(|s| Box::new(s) as Box) - .collect::>(); + let mut systems = read_systems_from_file(path); let parameters = r#"{ - "cutoff": 5.0, - "max_radial": 6, - "max_angular": 4, - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "radial_basis": { - "Gto": {} + "cutoff": { + "radius": 5.0, + "smoothing": { + "type": "ShiftedCosine", + "width": 0.5 + } + }, + "density": { + "type": "Gaussian", + "width": 0.3 }, - "cutoff_function": { - "ShiftedCosine": {"width": 0.5} + "basis": { + "type": "TensorProduct", + "max_angular": 4, + "radial": {"type": "Gto", "max_radial": 6} } }"#; diff --git a/rascaline-c-api/include/rascaline.h b/featomic/include/featomic.h similarity index 65% rename from rascaline-c-api/include/rascaline.h rename to featomic/include/featomic.h index 1f4addb26..00f625861 100644 --- a/rascaline-c-api/include/rascaline.h +++ b/featomic/include/featomic.h @@ -1,75 +1,69 @@ /* ============ Automatically generated file, DOT NOT EDIT. ============ * * * - * This file is automatically generated from the rascaline-c-api sources, * + * This file is automatically generated from the featomic sources, * * using cbindgen. If you want to make change to this file (including * - * documentation), make the corresponding changes in the rust sources. * + * documentation), make the corresponding changes in the rust sources * + * in `featomic/src/c_api/` * * =========================================================================== */ -#ifndef RASCALINE_H -#define RASCALINE_H +#ifndef FEATOMIC_H +#define FEATOMIC_H -#include #include #include -#include -#include "metatensor.h" +#include /** * Status code used when a function succeeded */ -#define RASCAL_SUCCESS 0 +#define FEATOMIC_SUCCESS 0 /** * Status code used when a function got an invalid parameter */ -#define RASCAL_INVALID_PARAMETER_ERROR 1 +#define FEATOMIC_INVALID_PARAMETER_ERROR 1 /** * Status code used when there was an error reading or writing JSON */ -#define RASCAL_JSON_ERROR 2 +#define FEATOMIC_JSON_ERROR 2 /** * Status code used when a string contains non-utf8 data */ -#define RASCAL_UTF8_ERROR 3 - -/** - * Status code used for error related to reading files with chemfiles - */ -#define RASCAL_CHEMFILES_ERROR 4 +#define FEATOMIC_UTF8_ERROR 3 /** * Status code used for errors coming from the system implementation if we * don't have a more specific status */ -#define RASCAL_SYSTEM_ERROR 128 +#define FEATOMIC_SYSTEM_ERROR 128 /** * Status code used when a memory buffer is too small to fit the requested data */ -#define RASCAL_BUFFER_SIZE_ERROR 254 +#define FEATOMIC_BUFFER_SIZE_ERROR 254 /** * Status code used when there was an internal error, i.e. there is a bug - * inside rascaline + * inside featomic */ -#define RASCAL_INTERNAL_ERROR 255 +#define FEATOMIC_INTERNAL_ERROR 255 /** * The "error" level designates very serious errors */ -#define RASCAL_LOG_LEVEL_ERROR 1 +#define FEATOMIC_LOG_LEVEL_ERROR 1 /** * The "warn" level designates hazardous situations */ -#define RASCAL_LOG_LEVEL_WARN 2 +#define FEATOMIC_LOG_LEVEL_WARN 2 /** * The "info" level designates useful information */ -#define RASCAL_LOG_LEVEL_INFO 3 +#define FEATOMIC_LOG_LEVEL_INFO 3 /** * The "debug" level designates lower priority information @@ -77,47 +71,47 @@ * By default, log messages at this level are disabled in release mode, and * enabled in debug mode. */ -#define RASCAL_LOG_LEVEL_DEBUG 4 +#define FEATOMIC_LOG_LEVEL_DEBUG 4 /** * The "trace" level designates very low priority, often extremely verbose, * information. * - * By default, rascaline disable this level, you can enable it by editing the + * By default, featomic disable this level, you can enable it by editing the * code. */ -#define RASCAL_LOG_LEVEL_TRACE 5 +#define FEATOMIC_LOG_LEVEL_TRACE 5 /** * Opaque type representing a `Calculator` */ -typedef struct rascal_calculator_t rascal_calculator_t; +typedef struct featomic_calculator_t featomic_calculator_t; /** * Status type returned by all functions in the C API. * - * The value 0 (`RASCAL_SUCCESS`) is used to indicate successful operations. - * Positive non-zero values are reserved for internal use in rascaline. + * The value 0 (`FEATOMIC_SUCCESS`) is used to indicate successful operations. + * Positive non-zero values are reserved for internal use in featomic. * Negative values are reserved for use in user code, in particular to indicate * error coming from callbacks. */ -typedef int32_t rascal_status_t; +typedef int32_t featomic_status_t; /** - * Callback function type for rascaline logging system. Such functions are + * Callback function type for featomic logging system. Such functions are * called when a log event is emitted in the code. * - * The first argument is the log level, one of `RASCAL_LOG_LEVEL_ERROR`, - * `RASCAL_LOG_LEVEL_WARN` `RASCAL_LOG_LEVEL_INFO`, `RASCAL_LOG_LEVEL_DEBUG`, - * or `RASCAL_LOG_LEVEL_TRACE`. The second argument is a NULL-terminated string + * The first argument is the log level, one of `FEATOMIC_LOG_LEVEL_ERROR`, + * `FEATOMIC_LOG_LEVEL_WARN` `FEATOMIC_LOG_LEVEL_INFO`, `FEATOMIC_LOG_LEVEL_DEBUG`, + * or `FEATOMIC_LOG_LEVEL_TRACE`. The second argument is a NULL-terminated string * containing the message associated with the log event. */ -typedef void (*rascal_logging_callback_t)(int32_t level, const char *message); +typedef void (*featomic_logging_callback_t)(int32_t level, const char *message); /** * Pair of atoms coming from a neighbor list */ -typedef struct rascal_pair_t { +typedef struct featomic_pair_t { /** * index of the first atom in the pair */ @@ -142,10 +136,10 @@ typedef struct rascal_pair_t { * pair. */ int32_t cell_shift_indices[3]; -} rascal_pair_t; +} featomic_pair_t; /** - * A `rascal_system_t` deals with the storage of atoms and related information, + * A `featomic_system_t` deals with the storage of atoms and related information, * as well as the computation of neighbor lists. * * This struct contains a manual implementation of a virtual table, allowing to @@ -154,23 +148,23 @@ typedef struct rascal_pair_t { * implementing the `System` trait; and then there is one function pointers * (`Option`) for each function in the `System` trait. * - * The `rascal_status_t` return value for the function is used to communicate - * error messages. It should be 0/`RASCAL_SUCCESS` in case of success, any + * The `featomic_status_t` return value for the function is used to communicate + * error messages. It should be 0/`FEATOMIC_SUCCESS` in case of success, any * non-zero value in case of error. The error will be propagated to the - * top-level caller as a `RASCAL_SYSTEM_ERROR` + * top-level caller as a `FEATOMIC_SYSTEM_ERROR` * * A new implementation of the System trait can then be created in any language * supporting a C API (meaning any language for our purposes); by correctly * setting `user_data` to the actual data storage, and setting all function * pointers to the correct functions. For an example of code doing this, see - * the `SystemBase` class in the Python interface to rascaline. + * the `SystemBase` class in the Python interface to featomic. * * **WARNING**: all function implementations **MUST** be thread-safe, function * taking `const` pointer parameters can be called from multiple threads at the - * same time. The `rascal_system_t` itself might be moved from one thread to + * same time. The `featomic_system_t` itself might be moved from one thread to * another. */ -typedef struct rascal_system_t { +typedef struct featomic_system_t { /** * User-provided data should be stored here, it will be passed as the * first parameter to all function pointers below. @@ -179,33 +173,33 @@ typedef struct rascal_system_t { /** * This function should set `*size` to the number of atoms in this system */ - rascal_status_t (*size)(const void *user_data, uintptr_t *size); + featomic_status_t (*size)(const void *user_data, uintptr_t *size); /** * This function should set `*types` to a pointer to the first element of * a contiguous array containing the atomic types of each atom in the * system. Different atomic types should be identified with a different * value. These values are usually the atomic number, but don't have to be. - * The array should contain `rascal_system_t::size()` elements. + * The array should contain `featomic_system_t::size()` elements. */ - rascal_status_t (*types)(const void *user_data, const int32_t **types); + featomic_status_t (*types)(const void *user_data, const int32_t **types); /** * This function should set `*positions` to a pointer to the first element * of a contiguous array containing the atomic cartesian coordinates. * `positions[0], positions[1], positions[2]` must contain the x, y, z * cartesian coordinates of the first atom, and so on. */ - rascal_status_t (*positions)(const void *user_data, const double **positions); + featomic_status_t (*positions)(const void *user_data, const double **positions); /** * This function should write the unit cell matrix in `cell`, which have * space for 9 values. The cell should be written in row major order, i.e. * `ax ay az bx by bz cx cy cz`, where a/b/c are the unit cell vectors. */ - rascal_status_t (*cell)(const void *user_data, double *cell); + featomic_status_t (*cell)(const void *user_data, double *cell); /** * This function should compute the neighbor list with the given cutoff, * and store it for later access using `pairs` or `pairs_containing`. */ - rascal_status_t (*compute_neighbors)(void *user_data, double cutoff); + featomic_status_t (*compute_neighbors)(void *user_data, double cutoff); /** * This function should set `*pairs` to a pointer to the first element of a * contiguous array containing all pairs in this system; and `*count` to @@ -217,25 +211,25 @@ typedef struct rascal_system_t { * cutoff passed in the last call to `compute_neighbors`. This function is * only valid to call after a call to `compute_neighbors`. */ - rascal_status_t (*pairs)(const void *user_data, - const struct rascal_pair_t **pairs, - uintptr_t *count); + featomic_status_t (*pairs)(const void *user_data, + const struct featomic_pair_t **pairs, + uintptr_t *count); /** * This function should set `*pairs` to a pointer to the first element of a * contiguous array containing all pairs in this system containing the atom * with index `atom`; and `*count` to the size of the array/the number of * pairs. * - * The same restrictions on the list of pairs as `rascal_system_t::pairs` + * The same restrictions on the list of pairs as `featomic_system_t::pairs` * applies, with the additional condition that the pair `i-j` should be * included both in the return of `pairs_containing(i)` and * `pairs_containing(j)`. */ - rascal_status_t (*pairs_containing)(const void *user_data, - uintptr_t atom, - const struct rascal_pair_t **pairs, - uintptr_t *count); -} rascal_system_t; + featomic_status_t (*pairs_containing)(const void *user_data, + uintptr_t atom, + const struct featomic_pair_t **pairs, + uintptr_t *count); +} featomic_system_t; /** * Rules to select labels (either samples or properties) on which the user @@ -244,7 +238,7 @@ typedef struct rascal_system_t { * To run the calculation for all possible labels, users should set both fields * to NULL. */ -typedef struct rascal_labels_selection_t { +typedef struct featomic_labels_selection_t { /** * Select a subset of labels, using the same selection criterion for all * keys in the final `mts_tensormap_t`. @@ -268,12 +262,12 @@ typedef struct rascal_labels_selection_t { * full calculation. */ const mts_tensormap_t *predefined; -} rascal_labels_selection_t; +} featomic_labels_selection_t; /** * Options that can be set to change how a calculator operates. */ -typedef struct rascal_calculation_options_t { +typedef struct featomic_calculation_options_t { /** * @verbatim embed:rst:leading-asterisk * Array of NULL-terminated strings containing the gradients to compute. @@ -318,7 +312,7 @@ typedef struct rascal_calculation_options_t { * - ``"cell"``, for gradients of the representation with respect to the * system's cell parameters. These gradients are computed at fixed * positions, and often not what you want when computing gradients - * explicitly (they are mainly used in ``rascaline.torch`` to integrate + * explicitly (they are mainly used in ``featomic.torch`` to integrate * with backward propagation). If you are trying to compute the virial * or the stress, you should use ``"strain"`` gradients instead. * @@ -341,11 +335,11 @@ typedef struct rascal_calculation_options_t { /** * Selection of samples on which to run the computation */ - struct rascal_labels_selection_t selected_samples; + struct featomic_labels_selection_t selected_samples; /** * Selection of properties to compute for the samples */ - struct rascal_labels_selection_t selected_properties; + struct featomic_labels_selection_t selected_properties; /** * Selection for the keys to include in the output. Set this parameter to * `NULL` to use the default set of keys, as determined by the calculator. @@ -353,7 +347,7 @@ typedef struct rascal_calculation_options_t { * running the calculation on. */ const mts_labels_t *selected_keys; -} rascal_calculation_options_t; +} featomic_calculation_options_t; #ifdef __cplusplus extern "C" { @@ -364,58 +358,14 @@ extern "C" { * * @returns the last error message, as a NULL-terminated string */ -const char *rascal_last_error(void); +const char *featomic_last_error(void); /** * Set the given ``callback`` function as the global logging callback. This * function will be called on all log events. If a logging callback was already * set, it is replaced by the new one. */ -rascal_status_t rascal_set_logging_callback(rascal_logging_callback_t callback); - -/** - * Read all structures in the file at the given `path` using - * [chemfiles](https://chemfiles.org/), and convert them to an array of - * `rascal_system_t`. - * - * This function can read all [formats supported by - * chemfiles](https://chemfiles.org/chemfiles/latest/formats.html). - * - * This function allocates memory, which must be released using - * `rascal_basic_systems_free`. - * - * If you need more control over the system behavior, consider writing your own - * instance of `rascal_system_t`. - * - * @param path path of the file to read from in the local filesystem - * @param systems `*systems` will be set to a pointer to the first element of - * the array of `rascal_system_t` - * @param count `*count` will be set to the number of systems read from the file - * - * @returns The status code of this operation. If the status is not - * `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the full - * error message. - */ -rascal_status_t rascal_basic_systems_read(const char *path, - struct rascal_system_t **systems, - uintptr_t *count); - -/** - * Release memory allocated by `rascal_basic_systems_read`. - * - * This function is only valid to call with a pointer to systems obtained from - * `rascal_basic_systems_read`, and the corresponding `count`. Any other use - * will probably result in segmentation faults or double free. If `systems` is - * NULL, this function does nothing. - * - * @param systems pointer to the first element of the array of - * `rascal_system_t` @param count number of systems in the array - * - * @returns The status code of this operation. If the status is not - * `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the full - * error message. - */ -rascal_status_t rascal_basic_systems_free(struct rascal_system_t *systems, uintptr_t count); +featomic_status_t featomic_set_logging_callback(featomic_logging_callback_t callback); /** * Create a new calculator with the given `name` and `parameters`. @@ -429,31 +379,31 @@ rascal_status_t rascal_basic_systems_free(struct rascal_system_t *systems, uintp * @endverbatim * * All memory allocated by this function can be released using - * `rascal_calculator_free`. + * `featomic_calculator_free`. * * @param name name of the calculator as a NULL-terminated string * @param parameters hyper-parameters of the calculator, JSON-formatted in a * NULL-terminated string * * @returns A pointer to the newly allocated calculator, or a `NULL` pointer in - * case of error. In case of error, you can use `rascal_last_error()` + * case of error. In case of error, you can use `featomic_last_error()` * to get the error message. */ -struct rascal_calculator_t *rascal_calculator(const char *name, const char *parameters); +struct featomic_calculator_t *featomic_calculator(const char *name, const char *parameters); /** * Free the memory associated with a `calculator` previously created with - * `rascal_calculator`. + * `featomic_calculator`. * * If `calculator` is `NULL`, this function does nothing. * * @param calculator pointer to an existing calculator, or `NULL` * * @returns The status code of this operation. If the status is not - * `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the + * `FEATOMIC_SUCCESS`, you can use `featomic_last_error()` to get the * full error message. */ -rascal_status_t rascal_calculator_free(struct rascal_calculator_t *calculator); +featomic_status_t featomic_calculator_free(struct featomic_calculator_t *calculator); /** * Get a copy of the name of this calculator in the `name` buffer of size @@ -461,19 +411,19 @@ rascal_status_t rascal_calculator_free(struct rascal_calculator_t *calculator); * * `name` will be NULL-terminated by this function. If the buffer is too small * to fit the whole name, this function will return - * `RASCAL_BUFFER_SIZE_ERROR` + * `FEATOMIC_BUFFER_SIZE_ERROR` * * @param calculator pointer to an existing calculator * @param name string buffer to fill with the calculator name * @param bufflen number of characters available in the buffer * * @returns The status code of this operation. If the status is not - * `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the full + * `FEATOMIC_SUCCESS`, you can use `featomic_last_error()` to get the full * error message. */ -rascal_status_t rascal_calculator_name(const struct rascal_calculator_t *calculator, - char *name, - uintptr_t bufflen); +featomic_status_t featomic_calculator_name(const struct featomic_calculator_t *calculator, + char *name, + uintptr_t bufflen); /** * Get a copy of the parameters used to create this calculator in the @@ -481,7 +431,7 @@ rascal_status_t rascal_calculator_name(const struct rascal_calculator_t *calcula * * `parameters` will be NULL-terminated by this function. If the buffer is too * small to fit the whole name, this function will return - * `RASCAL_BUFFER_SIZE_ERROR`. + * `FEATOMIC_BUFFER_SIZE_ERROR`. * * @param calculator pointer to an existing calculator * @param parameters string buffer to fill with the parameters used to create @@ -489,12 +439,12 @@ rascal_status_t rascal_calculator_name(const struct rascal_calculator_t *calcula * @param bufflen number of characters available in the buffer * * @returns The status code of this operation. If the status is not - * `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the full + * `FEATOMIC_SUCCESS`, you can use `featomic_last_error()` to get the full * error message. */ -rascal_status_t rascal_calculator_parameters(const struct rascal_calculator_t *calculator, - char *parameters, - uintptr_t bufflen); +featomic_status_t featomic_calculator_parameters(const struct featomic_calculator_t *calculator, + char *parameters, + uintptr_t bufflen); /** * Get all radial cutoffs used by this `calculator`'s neighbors lists (which @@ -509,12 +459,12 @@ rascal_status_t rascal_calculator_parameters(const struct rascal_calculator_t *c * @param cutoffs_count pointer to be filled with the number of elements in the * `cutoffs` array * @returns The status code of this operation. If the status is not - * `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the full + * `FEATOMIC_SUCCESS`, you can use `featomic_last_error()` to get the full * error message. */ -rascal_status_t rascal_calculator_cutoffs(const struct rascal_calculator_t *calculator, - const double **cutoffs, - uintptr_t *cutoffs_count); +featomic_status_t featomic_calculator_cutoffs(const struct featomic_calculator_t *calculator, + const double **cutoffs, + uintptr_t *cutoffs_count); /** * Compute the representation of the given list of `systems` with a @@ -531,66 +481,66 @@ rascal_status_t rascal_calculator_cutoffs(const struct rascal_calculator_t *calc * @param options options for this calculation * * @returns The status code of this operation. If the status is not - * `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the full + * `FEATOMIC_SUCCESS`, you can use `featomic_last_error()` to get the full * error message. */ -rascal_status_t rascal_calculator_compute(struct rascal_calculator_t *calculator, - mts_tensormap_t **descriptor, - struct rascal_system_t *systems, - uintptr_t systems_count, - struct rascal_calculation_options_t options); +featomic_status_t featomic_calculator_compute(struct featomic_calculator_t *calculator, + mts_tensormap_t **descriptor, + struct featomic_system_t *systems, + uintptr_t systems_count, + struct featomic_calculation_options_t options); /** * Clear all collected profiling data * - * See also `rascal_profiling_enable` and `rascal_profiling_get`. + * See also `featomic_profiling_enable` and `featomic_profiling_get`. * * @returns The status code of this operation. If the status is not - * `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the full + * `FEATOMIC_SUCCESS`, you can use `featomic_last_error()` to get the full * error message. */ -rascal_status_t rascal_profiling_clear(void); +featomic_status_t featomic_profiling_clear(void); /** * Enable or disable profiling data collection. By default, data collection * is disabled. * - * Rascaline uses the [`time_graph`](https://docs.rs/time-graph/) to collect + * Featomic uses the [`time_graph`](https://docs.rs/time-graph/) to collect * timing information on the calculations. This profiling code collects the * total time spent inside the most important functions, as well as the * function call graph (which function called which other function). * - * You can use `rascal_profiling_clear` to reset profiling data to an empty - * state, and `rascal_profiling_get` to extract the profiling data. + * You can use `featomic_profiling_clear` to reset profiling data to an empty + * state, and `featomic_profiling_get` to extract the profiling data. * * @param enabled whether data collection should be enabled or not * * @returns The status code of this operation. If the status is not - * `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the full + * `FEATOMIC_SUCCESS`, you can use `featomic_last_error()` to get the full * error message. */ -rascal_status_t rascal_profiling_enable(bool enabled); +featomic_status_t featomic_profiling_enable(bool enabled); /** * Extract the current set of data collected for profiling. * - * See also `rascal_profiling_enable` and `rascal_profiling_clear`. + * See also `featomic_profiling_enable` and `featomic_profiling_clear`. * * @param format in which format should the data be provided. `"table"`, * `"short_table"` and `"json"` are currently supported * @param buffer pre-allocated buffer in which profiling data will be copied. * If the buffer is too small, this function will return - * `RASCAL_BUFFER_SIZE_ERROR` + * `FEATOMIC_BUFFER_SIZE_ERROR` * @param bufflen size of the `buffer` * * @returns The status code of this operation. If the status is not - * `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the full + * `FEATOMIC_SUCCESS`, you can use `featomic_last_error()` to get the full * error message. */ -rascal_status_t rascal_profiling_get(const char *format, char *buffer, uintptr_t bufflen); +featomic_status_t featomic_profiling_get(const char *format, char *buffer, uintptr_t bufflen); #ifdef __cplusplus } // extern "C" #endif // __cplusplus -#endif /* RASCALINE_H */ +#endif /* FEATOMIC_H */ diff --git a/rascaline-c-api/include/rascaline.hpp b/featomic/include/featomic.hpp similarity index 75% rename from rascaline-c-api/include/rascaline.hpp rename to featomic/include/featomic.hpp index 83b0a6c90..902f722d7 100644 --- a/rascaline-c-api/include/rascaline.hpp +++ b/featomic/include/featomic.hpp @@ -1,5 +1,5 @@ -#ifndef RASCALINE_HPP -#define RASCALINE_HPP +#ifndef FEATOMIC_HPP +#define FEATOMIC_HPP #include #include @@ -16,30 +16,30 @@ #include "metatensor.h" #include "metatensor.hpp" -#include "rascaline.h" +#include "featomic.h" -/// This file contains the C++ API to rascaline, manually built on top of the C -/// API defined in `rascaline.h`. This API uses the standard C++ library where +/// This file contains the C++ API to featomic, manually built on top of the C +/// API defined in `featomic.h`. This API uses the standard C++ library where /// convenient, but also allow to drop back to the C API if required, by -/// providing functions to extract the C API handles (named `as_rascal_XXX`). +/// providing functions to extract the C API handles (named `as_featomic_XXX`). -namespace rascaline { +namespace featomic { -/// Exception class for all error thrown by rascaline -class RascalineError : public std::runtime_error { +/// Exception class for all error thrown by featomic +class FeatomicError : public std::runtime_error { public: /// Create a new error with the given message - RascalineError(const std::string& message): std::runtime_error(message) {} - ~RascalineError() override = default; - - /// RascalineError is copy-constructible - RascalineError(const RascalineError&) = default; - /// RascalineError is move-constructible - RascalineError(RascalineError&&) = default; - /// RascalineError can be copy-assigned - RascalineError& operator=(const RascalineError&) = default; - /// RascalineError can be move-assigned - RascalineError& operator=(RascalineError&&) = default; + FeatomicError(const std::string& message): std::runtime_error(message) {} + ~FeatomicError() override = default; + + /// FeatomicError is copy-constructible + FeatomicError(const FeatomicError&) = default; + /// FeatomicError is move-constructible + FeatomicError(FeatomicError&&) = default; + /// FeatomicError can be copy-assigned + FeatomicError& operator=(const FeatomicError&) = default; + /// FeatomicError can be move-assigned + FeatomicError& operator=(FeatomicError&&) = default; }; namespace details { @@ -60,7 +60,7 @@ namespace details { // this should not underflow, but better safe than sorry if (next_id_ == INT32_MIN) { - throw RascalineError("too many exceptions, what are you doing???"); + throw FeatomicError("too many exceptions, what are you doing???"); } next_id_ -= 1; @@ -74,7 +74,7 @@ namespace details { std::exception_ptr extract_exception(int32_t exception_id) { auto iterator = map_.find(exception_id); if (iterator == map_.end()) { - throw RascalineError("internal error: tried to access a non-existing exception"); + throw FeatomicError("internal error: tried to access a non-existing exception"); } auto exception = iterator->second; @@ -123,12 +123,12 @@ namespace details { } }; - /// Check the status returned by a rascal function, throwing an exception - /// with the latest error message if the status is not `RASCAL_SUCCESS`. - inline void check_status(rascal_status_t status) { - if (status > RASCAL_SUCCESS) { - throw RascalineError(rascal_last_error()); - } else if (status < RASCAL_SUCCESS) { + /// Check the status returned by a featomic function, throwing an exception + /// with the latest error message if the status is not `FEATOMIC_SUCCESS`. + inline void check_status(featomic_status_t status) { + if (status > FEATOMIC_SUCCESS) { + throw FeatomicError(featomic_last_error()); + } else if (status < FEATOMIC_SUCCESS) { // this error comes from C++, let's restore it and pass it up auto exception = GlobalExceptionsStore::extract_exception(status); std::rethrow_exception(exception); @@ -136,11 +136,11 @@ namespace details { } } -#define RASCAL_SYSTEM_CATCH_EXCEPTIONS(__code__) \ +#define FEATOMIC_SYSTEM_CATCH_EXCEPTIONS(__code__) \ do { \ try { \ __code__ \ - return RASCAL_SUCCESS; \ + return FEATOMIC_SUCCESS; \ } catch (...) { \ auto exception = std::current_exception(); \ return details::GlobalExceptionsStore::save_exception( \ @@ -206,7 +206,7 @@ class System { /// cutoff passed in the last call to `System::compute_neighbors`. This /// function is only valid to call after a call to /// `System::compute_neighbors`. - virtual const std::vector& pairs() const = 0; + virtual const std::vector& pairs() const = 0; /// Get the list of pairs in this system containing the `atom` at the given /// index. @@ -215,59 +215,59 @@ class System { /// with the additional condition that the pair `i-j` should be included /// both in the return of `System::pairs_containing(i)` and /// `System::pairs_containing(j)`. - virtual const std::vector& pairs_containing(uintptr_t atom) const = 0; + virtual const std::vector& pairs_containing(uintptr_t atom) const = 0; - /// Convert a child instance of the `System` class to a `rascal_system_t` to - /// be passed to the rascaline functions. + /// Convert a child instance of the `System` class to a `featomic_system_t` to + /// be passed to the featomic functions. /// /// This is an advanced function that most users don't need to call /// directly. - rascal_system_t as_rascal_system_t() { - return rascal_system_t { + featomic_system_t as_featomic_system_t() { + return featomic_system_t { // user_data static_cast(this), // size [](const void* self, uintptr_t* size) { - RASCAL_SYSTEM_CATCH_EXCEPTIONS( + FEATOMIC_SYSTEM_CATCH_EXCEPTIONS( *size = static_cast(self)->size(); ); }, // types [](const void* self, const int32_t** types) { - RASCAL_SYSTEM_CATCH_EXCEPTIONS( + FEATOMIC_SYSTEM_CATCH_EXCEPTIONS( *types = static_cast(self)->types(); ); }, // positions [](const void* self, const double** positions) { - RASCAL_SYSTEM_CATCH_EXCEPTIONS( + FEATOMIC_SYSTEM_CATCH_EXCEPTIONS( *positions = (reinterpret_cast(self))->positions(); ); }, // cell [](const void* self, double* cell) { - RASCAL_SYSTEM_CATCH_EXCEPTIONS( + FEATOMIC_SYSTEM_CATCH_EXCEPTIONS( auto cpp_cell = reinterpret_cast(self)->cell(); std::memcpy(cell, cpp_cell.data(), 9 * sizeof(double)); ); }, // compute_neighbors [](void* self, double cutoff) { - RASCAL_SYSTEM_CATCH_EXCEPTIONS( + FEATOMIC_SYSTEM_CATCH_EXCEPTIONS( reinterpret_cast(self)->compute_neighbors(cutoff); ); }, // pairs - [](const void* self, const rascal_pair_t** pairs, uintptr_t* size) { - RASCAL_SYSTEM_CATCH_EXCEPTIONS( + [](const void* self, const featomic_pair_t** pairs, uintptr_t* size) { + FEATOMIC_SYSTEM_CATCH_EXCEPTIONS( const auto& cpp_pairs = reinterpret_cast(self)->pairs(); *pairs = cpp_pairs.data(); *size = cpp_pairs.size(); ); }, // pairs_containing - [](const void* self, uintptr_t atom, const rascal_pair_t** pairs, uintptr_t* size) { - RASCAL_SYSTEM_CATCH_EXCEPTIONS( + [](const void* self, uintptr_t atom, const featomic_pair_t** pairs, uintptr_t* size) { + FEATOMIC_SYSTEM_CATCH_EXCEPTIONS( const auto& cpp_pairs = reinterpret_cast(self)->pairs_containing(atom); *pairs = cpp_pairs.data(); *size = cpp_pairs.size(); @@ -277,67 +277,57 @@ class System { } }; -#undef RASCAL_SYSTEM_CATCH_EXCEPTIONS +#undef FEATOMIC_SYSTEM_CATCH_EXCEPTIONS -/// A collection of systems read from a file. This is a convenience class -/// enabling the common use case of reading systems from a file and runnning a -/// calculation on the systems. If you need more control or access to advanced -/// functionalities, you should consider writing a new class extending `System`. -class BasicSystems { +/// A very minimal implementation of the System interface, only providing data +/// and no neighbor list calculation. This class must thus be used with +/// `use_native_system=true` in the calculation options. +class SimpleSystem final: public System { public: - /// Read all structures in the file at the given `path` using - /// [chemfiles](https://chemfiles.org/). - /// - /// This function can read all [formats supported by - /// chemfiles](https://chemfiles.org/chemfiles/latest/formats.html). - /// - /// @throws RascalineError if chemfiles can not read the file - BasicSystems(const std::string& path) { - details::check_status(rascal_basic_systems_read(path.c_str(), &systems_, &count_)); - } + /// Create a new `SimpleSystem`, with the atoms contained in the give cell + SimpleSystem(CellMatrix cell = {{0}}): cell_(cell) {} - ~BasicSystems() noexcept { - rascal_basic_systems_free(systems_, count_); + /// Add an atom with the given type and position to this system + void add_atom(int32_t type, std::array position) { + this->atomic_types_.push_back(type); + this->positions_.push_back(position[0]); + this->positions_.push_back(position[1]); + this->positions_.push_back(position[2]); } - /// BasicSystems is **NOT** copy-constructible - BasicSystems(const BasicSystems&) = delete; - /// BasicSystems can **NOT** be copy-assigned - BasicSystems& operator=(const BasicSystems&) = delete; + uintptr_t size() const override { + return this->atomic_types_.size(); + } - /// BasicSystems is move-constructible - BasicSystems(BasicSystems&& other) noexcept { - *this = std::move(other); + const int32_t* types() const override { + return this->atomic_types_.data(); } - /// BasicSystems can be move-assigned - BasicSystems& operator=(BasicSystems&& other) noexcept { - this->~BasicSystems(); - this->systems_ = nullptr; - this->count_ = 0; + const double* positions() const override { + return this->positions_.data(); + } - std::swap(this->systems_, other.systems_); - std::swap(this->count_, other.count_); + CellMatrix cell() const override { + return cell_; + } - return *this; + void compute_neighbors(double /*cutoff*/) override { + throw std::runtime_error("SimpleSystem can only be used with `use_native_system=true`"); } - /// Get a pointer to the first element of the underlying array of systems - /// - /// This function is intended for internal use only. - rascal_system_t* systems() { - return systems_; + const std::vector& pairs() const override { + throw std::runtime_error("SimpleSystem can only be used with `use_native_system=true`"); } - /// Get the number of systems managed by this `BasicSystems` - uintptr_t count() const { - return count_; + const std::vector& pairs_containing(uintptr_t /*atom*/) const override { + throw std::runtime_error("SimpleSystem can only be used with `use_native_system=true`"); } private: - rascal_system_t* systems_ = nullptr; - uintptr_t count_ = 0; + CellMatrix cell_; + std::vector positions_; + std::vector atomic_types_; }; /// Rules to select labels (either samples or properties) on which the user @@ -417,10 +407,10 @@ class LabelsSelection { return *this; } - /// Get the `rascal_labels_selection_t` corresponding to this LabelsSelection - rascal_labels_selection_t as_rascal_labels_selection_t() const { - auto selection = rascal_labels_selection_t{}; - std::memset(&selection, 0, sizeof(rascal_labels_selection_t)); + /// Get the `featomic_labels_selection_t` corresponding to this LabelsSelection + featomic_labels_selection_t as_featomic_labels_selection_t() const { + auto selection = featomic_labels_selection_t{}; + std::memset(&selection, 0, sizeof(featomic_labels_selection_t)); if (subset_) { selection.subset = &raw_subset_; @@ -510,7 +500,7 @@ class CalculationOptions { /// - ``"cell"``, for gradients of the representation with respect to the /// system's cell parameters. These gradients are computed at fixed /// positions, and often not what you want when computing gradients - /// explicitly (they are mainly used in ``rascaline.torch`` to integrate + /// explicitly (they are mainly used in ``featomic.torch`` to integrate /// with backward propagation). If you are trying to compute the virial /// or the stress, you should use ``"strain"`` gradients instead. /// @@ -522,21 +512,21 @@ class CalculationOptions { std::vector gradients; /// Convert this instance of `CalculationOptions` to a - /// `rascal_calculation_options_t`. + /// `featomic_calculation_options_t`. /// /// This is an advanced function that most users don't need to call /// directly. - rascal_calculation_options_t as_rascal_calculation_options_t() { - auto options = rascal_calculation_options_t{}; - std::memset(&options, 0, sizeof(rascal_calculation_options_t)); + featomic_calculation_options_t as_featomic_calculation_options_t() { + auto options = featomic_calculation_options_t{}; + std::memset(&options, 0, sizeof(featomic_calculation_options_t)); options.use_native_system = this->use_native_system; options.gradients = this->gradients.data(); options.gradients_count = this->gradients.size(); - options.selected_samples = this->selected_samples.as_rascal_labels_selection_t(); - options.selected_properties = this->selected_properties.as_rascal_labels_selection_t(); + options.selected_samples = this->selected_samples.as_featomic_labels_selection_t(); + options.selected_properties = this->selected_properties.as_featomic_labels_selection_t(); if (this->selected_keys) { // store the raw mts_labels_t in a class variable to make sure @@ -560,7 +550,7 @@ class Calculator { public: /// Create a new calculator with the given `name` and `parameters`. /// - /// @throws RascalineError if `name` is not associated with a known calculator, + /// @throws FeatomicError if `name` is not associated with a known calculator, /// if `parameters` is not valid JSON, or if `parameters` do not /// contains the expected values for the requested calculator. /// @@ -571,15 +561,15 @@ class Calculator { /// schema. /// @endverbatim Calculator(std::string name, std::string parameters): - calculator_(rascal_calculator(name.data(), parameters.data())) + calculator_(featomic_calculator(name.data(), parameters.data())) { if (this->calculator_ == nullptr) { - throw RascalineError(rascal_last_error()); + throw FeatomicError(featomic_last_error()); } } ~Calculator() { - rascal_calculator_free(this->calculator_); + featomic_calculator_free(this->calculator_); } /// Calculator is **NOT** copy-constructible @@ -606,11 +596,11 @@ class Calculator { std::string name() const { auto buffer = std::vector(32, '\0'); while (true) { - auto status = rascal_calculator_name( + auto status = featomic_calculator_name( calculator_, buffer.data(), buffer.size() ); - if (status != RASCAL_BUFFER_SIZE_ERROR) { + if (status != FEATOMIC_BUFFER_SIZE_ERROR) { details::check_status(status); return std::string(buffer.data()); } @@ -624,11 +614,11 @@ class Calculator { std::string parameters() const { auto buffer = std::vector(256, '\0'); while (true) { - auto status = rascal_calculator_parameters( + auto status = featomic_calculator_parameters( calculator_, buffer.data(), buffer.size() ); - if (status != RASCAL_BUFFER_SIZE_ERROR) { + if (status != FEATOMIC_BUFFER_SIZE_ERROR) { details::check_status(status); return std::string(buffer.data()); } @@ -642,7 +632,7 @@ class Calculator { std::vector cutoffs() const { const double* data = nullptr; uintptr_t length = 0; - details::check_status(rascal_calculator_cutoffs( + details::check_status(featomic_calculator_cutoffs( calculator_, &data, &length @@ -652,17 +642,17 @@ class Calculator { /// Runs a calculation with this calculator on the given ``systems`` metatensor::TensorMap compute( - std::vector& systems, + std::vector& systems, CalculationOptions options = CalculationOptions() ) const { mts_tensormap_t* descriptor = nullptr; - details::check_status(rascal_calculator_compute( + details::check_status(featomic_calculator_compute( calculator_, &descriptor, systems.data(), systems.size(), - options.as_rascal_calculation_options_t() + options.as_featomic_calculation_options_t() )); return metatensor::TensorMap(descriptor); @@ -674,12 +664,12 @@ class Calculator { std::vector& systems, CalculationOptions options = CalculationOptions() ) const { - auto rascal_systems = std::vector(); + auto featomic_systems = std::vector(); for (auto& system: systems) { - rascal_systems.push_back(system.as_rascal_system_t()); + featomic_systems.push_back(system.as_featomic_system_t()); } - return this->compute(rascal_systems, std::move(options)); + return this->compute(featomic_systems, std::move(options)); } /// Runs a calculation for a single `system` @@ -690,59 +680,40 @@ class Calculator { ) const { mts_tensormap_t* descriptor = nullptr; - auto rascal_system = system.as_rascal_system_t(); - details::check_status(rascal_calculator_compute( + auto featomic_system = system.as_featomic_system_t(); + details::check_status(featomic_calculator_compute( calculator_, &descriptor, - &rascal_system, + &featomic_system, 1, - options.as_rascal_calculation_options_t() - )); - - return metatensor::TensorMap(descriptor); - } - - /// Runs a calculation for all the `systems` that where read from a file - /// using the `BasicSystems(std::string path)` constructor - metatensor::TensorMap compute( - BasicSystems& systems, - CalculationOptions options = CalculationOptions() - ) const { - mts_tensormap_t* descriptor = nullptr; - - details::check_status(rascal_calculator_compute( - calculator_, - &descriptor, - systems.systems(), - systems.count(), - options.as_rascal_calculation_options_t() + options.as_featomic_calculation_options_t() )); return metatensor::TensorMap(descriptor); } - /// Get the underlying pointer to a `rascal_calculator_t`. + /// Get the underlying pointer to a `featomic_calculator_t`. /// /// This is an advanced function that most users don't need to call /// directly. - rascal_calculator_t* as_rascal_calculator_t() { + featomic_calculator_t* as_featomic_calculator_t() { return calculator_; } - /// Get the underlying const pointer to a `rascal_calculator_t`. + /// Get the underlying const pointer to a `featomic_calculator_t`. /// /// This is an advanced function that most users don't need to call /// directly. - const rascal_calculator_t* as_rascal_calculator_t() const { + const featomic_calculator_t* as_featomic_calculator_t() const { return calculator_; } private: - rascal_calculator_t* calculator_ = nullptr; + featomic_calculator_t* calculator_ = nullptr; }; -/// Rascaline uses the [`time_graph`](https://docs.rs/time-graph/) to collect +/// Featomic uses the [`time_graph`](https://docs.rs/time-graph/) to collect /// timing information on the calculations. The `Profiler` static class provides /// access to this functionality. /// @@ -761,14 +732,14 @@ class Profiler { /// /// @param enabled whether data collection should be enabled or not static void enable(bool enabled) { - details::check_status(rascal_profiling_enable(enabled)); + details::check_status(featomic_profiling_enable(enabled)); } /// Clear all collected profiling data /// /// See also `Profiler::enable` and `Profiler::get`. static void clear() { - details::check_status(rascal_profiling_clear()); + details::check_status(featomic_profiling_clear()); } /// Extract the current set of data collected for profiling. @@ -781,11 +752,11 @@ class Profiler { static std::string get(const std::string& format) { auto buffer = std::vector(1024, '\0'); while (true) { - auto status = rascal_profiling_get( + auto status = featomic_profiling_get( format.c_str(), buffer.data(), buffer.size() ); - if (status != RASCAL_BUFFER_SIZE_ERROR) { + if (status != FEATOMIC_BUFFER_SIZE_ERROR) { details::check_status(status); return std::string(buffer.data()); } diff --git a/rascaline-c-api/src/calculator.rs b/featomic/src/c_api/calculator.rs similarity index 82% rename from rascaline-c-api/src/calculator.rs rename to featomic/src/c_api/calculator.rs index 7bd3bfad4..26c9c362f 100644 --- a/rascaline-c-api/src/calculator.rs +++ b/featomic/src/c_api/calculator.rs @@ -4,25 +4,26 @@ use std::ops::{Deref, DerefMut}; use metatensor::{Labels, TensorMap}; use metatensor::c_api::{mts_tensormap_t, mts_labels_t}; -use rascaline::{Calculator, System, CalculationOptions, LabelsSelection}; + +use crate::{CalculationOptions, Calculator, Error, LabelsSelection, System}; use super::utils::copy_str_to_c; -use super::{catch_unwind, rascal_status_t}; +use super::{catch_unwind, featomic_status_t}; -use super::system::rascal_system_t; +use super::system::featomic_system_t; /// Opaque type representing a `Calculator` #[allow(non_camel_case_types)] -pub struct rascal_calculator_t(Calculator); +pub struct featomic_calculator_t(Calculator); -impl Deref for rascal_calculator_t { +impl Deref for featomic_calculator_t { type Target = Calculator; fn deref(&self) -> &Self::Target { &self.0 } } -impl DerefMut for rascal_calculator_t { +impl DerefMut for featomic_calculator_t { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } @@ -39,18 +40,18 @@ impl DerefMut for rascal_calculator_t { /// @endverbatim /// /// All memory allocated by this function can be released using -/// `rascal_calculator_free`. +/// `featomic_calculator_free`. /// /// @param name name of the calculator as a NULL-terminated string /// @param parameters hyper-parameters of the calculator, JSON-formatted in a /// NULL-terminated string /// /// @returns A pointer to the newly allocated calculator, or a `NULL` pointer in -/// case of error. In case of error, you can use `rascal_last_error()` +/// case of error. In case of error, you can use `featomic_last_error()` /// to get the error message. #[no_mangle] #[allow(clippy::module_name_repetitions)] -pub unsafe extern fn rascal_calculator(name: *const c_char, parameters: *const c_char) -> *mut rascal_calculator_t { +pub unsafe extern fn featomic_calculator(name: *const c_char, parameters: *const c_char) -> *mut featomic_calculator_t { let mut raw = std::ptr::null_mut(); let unwind_wrapper = std::panic::AssertUnwindSafe(&mut raw); let status = catch_unwind(move || { @@ -58,7 +59,7 @@ pub unsafe extern fn rascal_calculator(name: *const c_char, parameters: *const c let name = CStr::from_ptr(name).to_str()?; let parameters = CStr::from_ptr(parameters).to_str()?; let calculator = Calculator::new(name, parameters.to_owned())?; - let boxed = Box::new(rascal_calculator_t(calculator)); + let boxed = Box::new(featomic_calculator_t(calculator)); let _ = &unwind_wrapper; *unwind_wrapper.0 = Box::into_raw(boxed); @@ -73,17 +74,17 @@ pub unsafe extern fn rascal_calculator(name: *const c_char, parameters: *const c } /// Free the memory associated with a `calculator` previously created with -/// `rascal_calculator`. +/// `featomic_calculator`. /// /// If `calculator` is `NULL`, this function does nothing. /// /// @param calculator pointer to an existing calculator, or `NULL` /// /// @returns The status code of this operation. If the status is not -/// `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the +/// `FEATOMIC_SUCCESS`, you can use `featomic_last_error()` to get the /// full error message. #[no_mangle] -pub unsafe extern fn rascal_calculator_free(calculator: *mut rascal_calculator_t) -> rascal_status_t { +pub unsafe extern fn featomic_calculator_free(calculator: *mut featomic_calculator_t) -> featomic_status_t { catch_unwind(|| { if !calculator.is_null() { let boxed = Box::from_raw(calculator); @@ -99,21 +100,21 @@ pub unsafe extern fn rascal_calculator_free(calculator: *mut rascal_calculator_t /// /// `name` will be NULL-terminated by this function. If the buffer is too small /// to fit the whole name, this function will return -/// `RASCAL_BUFFER_SIZE_ERROR` +/// `FEATOMIC_BUFFER_SIZE_ERROR` /// /// @param calculator pointer to an existing calculator /// @param name string buffer to fill with the calculator name /// @param bufflen number of characters available in the buffer /// /// @returns The status code of this operation. If the status is not -/// `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the full +/// `FEATOMIC_SUCCESS`, you can use `featomic_last_error()` to get the full /// error message. #[no_mangle] -pub unsafe extern fn rascal_calculator_name( - calculator: *const rascal_calculator_t, +pub unsafe extern fn featomic_calculator_name( + calculator: *const featomic_calculator_t, name: *mut c_char, bufflen: usize -) -> rascal_status_t { +) -> featomic_status_t { catch_unwind(|| { check_pointers!(calculator, name); copy_str_to_c(&(*calculator).name(), name, bufflen)?; @@ -126,7 +127,7 @@ pub unsafe extern fn rascal_calculator_name( /// /// `parameters` will be NULL-terminated by this function. If the buffer is too /// small to fit the whole name, this function will return -/// `RASCAL_BUFFER_SIZE_ERROR`. +/// `FEATOMIC_BUFFER_SIZE_ERROR`. /// /// @param calculator pointer to an existing calculator /// @param parameters string buffer to fill with the parameters used to create @@ -134,14 +135,14 @@ pub unsafe extern fn rascal_calculator_name( /// @param bufflen number of characters available in the buffer /// /// @returns The status code of this operation. If the status is not -/// `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the full +/// `FEATOMIC_SUCCESS`, you can use `featomic_last_error()` to get the full /// error message. #[no_mangle] -pub unsafe extern fn rascal_calculator_parameters( - calculator: *const rascal_calculator_t, +pub unsafe extern fn featomic_calculator_parameters( + calculator: *const featomic_calculator_t, parameters: *mut c_char, bufflen: usize -) -> rascal_status_t { +) -> featomic_status_t { catch_unwind(|| { check_pointers!(calculator, parameters); copy_str_to_c((*calculator).parameters(), parameters, bufflen)?; @@ -163,14 +164,14 @@ pub unsafe extern fn rascal_calculator_parameters( /// @param cutoffs_count pointer to be filled with the number of elements in the /// `cutoffs` array /// @returns The status code of this operation. If the status is not -/// `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the full +/// `FEATOMIC_SUCCESS`, you can use `featomic_last_error()` to get the full /// error message. #[no_mangle] -pub unsafe extern fn rascal_calculator_cutoffs( - calculator: *const rascal_calculator_t, +pub unsafe extern fn featomic_calculator_cutoffs( + calculator: *const featomic_calculator_t, cutoffs: *mut *const f64, cutoffs_count: *mut usize -) -> rascal_status_t { +) -> featomic_status_t { catch_unwind(|| { check_pointers!(calculator, cutoffs, cutoffs_count); @@ -190,7 +191,7 @@ pub unsafe extern fn rascal_calculator_cutoffs( #[repr(C)] #[derive(Debug)] #[allow(non_camel_case_types)] -pub struct rascal_labels_selection_t { +pub struct featomic_labels_selection_t { /// Select a subset of labels, using the same selection criterion for all /// keys in the final `mts_tensormap_t`. /// @@ -212,7 +213,7 @@ pub struct rascal_labels_selection_t { predefined: *const mts_tensormap_t, } -fn c_labels_to_rust(mut labels: mts_labels_t) -> Result { +fn c_labels_to_rust(mut labels: mts_labels_t) -> Result { if labels.internal_ptr_.is_null() { // create new metatensor-core labels unsafe { @@ -222,29 +223,29 @@ fn c_labels_to_rust(mut labels: mts_labels_t) -> Result( - selection: &'a rascal_labels_selection_t, + selection: &'a featomic_labels_selection_t, labels: &'a mut Option, predefined: &'a mut Option, -) -> Result, rascaline::Error> { +) -> Result, Error> { match (selection.subset.is_null(), selection.predefined.is_null()) { (true, true) => Ok(LabelsSelection::All), (false, true) => { @@ -269,21 +270,21 @@ fn convert_labels_selection<'a>( Err(e) => { // same as above let _ = TensorMap::into_raw(tensor); - return Err(rascaline::Error::from(e)); + return Err(Error::from(e)); } } Ok(LabelsSelection::Predefined(predefined.as_ref().expect("just created it"))) } (false, false) => { - Err(rascaline::Error::InvalidParameter( - "can not have both global and predefined non-NULL in rascal_labels_selection_t".into() + Err(Error::InvalidParameter( + "can not have both global and predefined non-NULL in featomic_labels_selection_t".into() )) } } } -fn key_selection(value: *const mts_labels_t, labels: &'_ mut Option) -> Result, rascaline::Error> { +fn key_selection(value: *const mts_labels_t, labels: &'_ mut Option) -> Result, Error> { if value.is_null() { return Ok(None); } @@ -300,7 +301,7 @@ fn key_selection(value: *const mts_labels_t, labels: &'_ mut Option) -> #[repr(C)] #[derive(Debug)] #[allow(non_camel_case_types)] -pub struct rascal_calculation_options_t { +pub struct featomic_calculation_options_t { /// @verbatim embed:rst:leading-asterisk /// Array of NULL-terminated strings containing the gradients to compute. /// If this field is `NULL` and `gradients_count` is 0, no gradients are @@ -344,7 +345,7 @@ pub struct rascal_calculation_options_t { /// - ``"cell"``, for gradients of the representation with respect to the /// system's cell parameters. These gradients are computed at fixed /// positions, and often not what you want when computing gradients - /// explicitly (they are mainly used in ``rascaline.torch`` to integrate + /// explicitly (they are mainly used in ``featomic.torch`` to integrate /// with backward propagation). If you are trying to compute the virial /// or the stress, you should use ``"strain"`` gradients instead. /// @@ -360,9 +361,9 @@ pub struct rascal_calculation_options_t { /// faster than having to cross the FFI boundary too often. use_native_system: bool, /// Selection of samples on which to run the computation - selected_samples: rascal_labels_selection_t, + selected_samples: featomic_labels_selection_t, /// Selection of properties to compute for the samples - selected_properties: rascal_labels_selection_t, + selected_properties: featomic_labels_selection_t, /// Selection for the keys to include in the output. Set this parameter to /// `NULL` to use the default set of keys, as determined by the calculator. /// Note that this default set of keys can depend on which systems we are @@ -385,24 +386,24 @@ pub struct rascal_calculation_options_t { /// @param options options for this calculation /// /// @returns The status code of this operation. If the status is not -/// `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the full +/// `FEATOMIC_SUCCESS`, you can use `featomic_last_error()` to get the full /// error message. #[no_mangle] -pub unsafe extern fn rascal_calculator_compute( - calculator: *mut rascal_calculator_t, +pub unsafe extern fn featomic_calculator_compute( + calculator: *mut featomic_calculator_t, descriptor: *mut *mut mts_tensormap_t, - systems: *mut rascal_system_t, + systems: *mut featomic_system_t, systems_count: usize, - options: rascal_calculation_options_t, -) -> rascal_status_t { + options: featomic_calculation_options_t, +) -> featomic_status_t { catch_unwind(move || { if systems_count == 0 { - log::warn!("0 systems given to rascal_calculator_compute, nothing to do"); + log::warn!("0 systems given to featomic_calculator_compute, nothing to do"); return Ok(()); } check_pointers!(calculator, descriptor, systems); - // Create a Vec> from the passed systems + // Create a Vec from the passed systems let c_systems = if systems_count == 0 { &mut [] } else { @@ -411,7 +412,7 @@ pub unsafe extern fn rascal_calculator_compute( }; let mut systems = Vec::with_capacity(c_systems.len()); for system in c_systems { - systems.push(Box::new(system) as Box); + systems.push(System::new(system)); } let c_gradients = if options.gradients_count == 0 { diff --git a/rascaline-c-api/src/logging.rs b/featomic/src/c_api/logging.rs similarity index 55% rename from rascaline-c-api/src/logging.rs rename to featomic/src/c_api/logging.rs index f3866f168..f2d0aac23 100644 --- a/rascaline-c-api/src/logging.rs +++ b/featomic/src/c_api/logging.rs @@ -4,55 +4,55 @@ use std::sync::Mutex; use log::{Record, Metadata}; use once_cell::sync::Lazy; -use crate::status::{rascal_status_t, catch_unwind}; +use super::status::{featomic_status_t, catch_unwind}; /// The "error" level designates very serious errors -pub const RASCAL_LOG_LEVEL_ERROR: i32 = 1; +pub const FEATOMIC_LOG_LEVEL_ERROR: i32 = 1; /// The "warn" level designates hazardous situations -pub const RASCAL_LOG_LEVEL_WARN: i32 = 2; +pub const FEATOMIC_LOG_LEVEL_WARN: i32 = 2; /// The "info" level designates useful information -pub const RASCAL_LOG_LEVEL_INFO: i32 = 3; +pub const FEATOMIC_LOG_LEVEL_INFO: i32 = 3; /// The "debug" level designates lower priority information /// /// By default, log messages at this level are disabled in release mode, and /// enabled in debug mode. -pub const RASCAL_LOG_LEVEL_DEBUG: i32 = 4; +pub const FEATOMIC_LOG_LEVEL_DEBUG: i32 = 4; /// The "trace" level designates very low priority, often extremely verbose, /// information. /// -/// By default, rascaline disable this level, you can enable it by editing the +/// By default, featomic disable this level, you can enable it by editing the /// code. -pub const RASCAL_LOG_LEVEL_TRACE: i32 = 5; +pub const FEATOMIC_LOG_LEVEL_TRACE: i32 = 5; -/// Callback function type for rascaline logging system. Such functions are +/// Callback function type for featomic logging system. Such functions are /// called when a log event is emitted in the code. /// -/// The first argument is the log level, one of `RASCAL_LOG_LEVEL_ERROR`, -/// `RASCAL_LOG_LEVEL_WARN` `RASCAL_LOG_LEVEL_INFO`, `RASCAL_LOG_LEVEL_DEBUG`, -/// or `RASCAL_LOG_LEVEL_TRACE`. The second argument is a NULL-terminated string +/// The first argument is the log level, one of `FEATOMIC_LOG_LEVEL_ERROR`, +/// `FEATOMIC_LOG_LEVEL_WARN` `FEATOMIC_LOG_LEVEL_INFO`, `FEATOMIC_LOG_LEVEL_DEBUG`, +/// or `FEATOMIC_LOG_LEVEL_TRACE`. The second argument is a NULL-terminated string /// containing the message associated with the log event. #[allow(non_camel_case_types)] -pub type rascal_logging_callback_t = Option; +pub type featomic_logging_callback_t = Option; -static GLOBAL_CALLBACK: Lazy> = Lazy::new(|| Mutex::new(None)); +static GLOBAL_CALLBACK: Lazy> = Lazy::new(|| Mutex::new(None)); /// Implementation of `log::Log` that forward all log messages to the global -/// `rascal_logging_callback_t`. -struct RascalLogger; +/// `featomic_logging_callback_t`. +struct FeatomicLogger; /// Set the given ``callback`` function as the global logging callback. This /// function will be called on all log events. If a logging callback was already /// set, it is replaced by the new one. #[no_mangle] -pub unsafe extern fn rascal_set_logging_callback(callback: rascal_logging_callback_t) -> rascal_status_t { +pub unsafe extern fn featomic_set_logging_callback(callback: featomic_logging_callback_t) -> featomic_status_t { catch_unwind(|| { *GLOBAL_CALLBACK.lock().expect("mutex was poisoned") = callback; // we allow multiple sets of logger, therefore the result will be ignored - let _ = log::set_boxed_logger(Box::new(RascalLogger)); + let _ = log::set_boxed_logger(Box::new(FeatomicLogger)); if cfg!(debug_assertions) { log::set_max_level(log::LevelFilter::Debug); @@ -65,7 +65,7 @@ pub unsafe extern fn rascal_set_logging_callback(callback: rascal_logging_callba } -impl log::Log for RascalLogger { +impl log::Log for FeatomicLogger { fn enabled(&self, _: &Metadata) -> bool { return true; } @@ -77,7 +77,7 @@ impl log::Log for RascalLogger { unsafe { match *(GLOBAL_CALLBACK.lock().expect("mutex was poisoned")) { Some(callback) => callback(record.level() as i32, message_cstr.as_ptr()), - None => unreachable!("missing callback but RascalLogger is set as the global logger"), + None => unreachable!("missing callback but FeatomicLogger is set as the global logger"), } } } @@ -92,10 +92,10 @@ mod tests { #[test] fn log_levels() { - assert_eq!(RASCAL_LOG_LEVEL_ERROR, log::Level::Error as i32); - assert_eq!(RASCAL_LOG_LEVEL_WARN, log::Level::Warn as i32); - assert_eq!(RASCAL_LOG_LEVEL_INFO, log::Level::Info as i32); - assert_eq!(RASCAL_LOG_LEVEL_DEBUG, log::Level::Debug as i32); - assert_eq!(RASCAL_LOG_LEVEL_TRACE, log::Level::Trace as i32); + assert_eq!(FEATOMIC_LOG_LEVEL_ERROR, log::Level::Error as i32); + assert_eq!(FEATOMIC_LOG_LEVEL_WARN, log::Level::Warn as i32); + assert_eq!(FEATOMIC_LOG_LEVEL_INFO, log::Level::Info as i32); + assert_eq!(FEATOMIC_LOG_LEVEL_DEBUG, log::Level::Debug as i32); + assert_eq!(FEATOMIC_LOG_LEVEL_TRACE, log::Level::Trace as i32); } } diff --git a/featomic/src/c_api/mod.rs b/featomic/src/c_api/mod.rs new file mode 100644 index 000000000..cf74e480e --- /dev/null +++ b/featomic/src/c_api/mod.rs @@ -0,0 +1,19 @@ +#![allow(clippy::missing_safety_doc, clippy::doc_markdown)] + +mod utils; +#[macro_use] +mod status; +pub use self::status::{catch_unwind, featomic_status_t}; +pub use self::status::{FEATOMIC_SUCCESS, FEATOMIC_INVALID_PARAMETER_ERROR, FEATOMIC_JSON_ERROR}; +pub use self::status::{FEATOMIC_UTF8_ERROR, FEATOMIC_SYSTEM_ERROR}; +pub use self::status::{FEATOMIC_BUFFER_SIZE_ERROR, FEATOMIC_INTERNAL_ERROR}; + +mod logging; +pub use self::logging::{FEATOMIC_LOG_LEVEL_ERROR, FEATOMIC_LOG_LEVEL_WARN, FEATOMIC_LOG_LEVEL_INFO}; +pub use self::logging::{FEATOMIC_LOG_LEVEL_DEBUG, FEATOMIC_LOG_LEVEL_TRACE}; +pub use self::logging::{featomic_logging_callback_t, featomic_set_logging_callback}; + +pub mod system; +pub mod calculator; + +pub mod profiling; diff --git a/rascaline-c-api/src/profiling.rs b/featomic/src/c_api/profiling.rs similarity index 65% rename from rascaline-c-api/src/profiling.rs rename to featomic/src/c_api/profiling.rs index aac56ce45..4bd9b3c7a 100644 --- a/rascaline-c-api/src/profiling.rs +++ b/featomic/src/c_api/profiling.rs @@ -1,20 +1,20 @@ use std::os::raw::c_char; use std::ffi::CStr; -use rascaline::Error; +use crate::Error; -use crate::{catch_unwind, rascal_status_t}; -use crate::utils::copy_str_to_c; +use super::{catch_unwind, featomic_status_t}; +use super::utils::copy_str_to_c; /// Clear all collected profiling data /// -/// See also `rascal_profiling_enable` and `rascal_profiling_get`. +/// See also `featomic_profiling_enable` and `featomic_profiling_get`. /// /// @returns The status code of this operation. If the status is not -/// `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the full +/// `FEATOMIC_SUCCESS`, you can use `featomic_last_error()` to get the full /// error message. #[no_mangle] -pub unsafe extern fn rascal_profiling_clear() -> rascal_status_t { +pub extern fn featomic_profiling_clear() -> featomic_status_t { catch_unwind(|| { time_graph::clear_collected_data(); Ok(()) @@ -25,21 +25,21 @@ pub unsafe extern fn rascal_profiling_clear() -> rascal_status_t { /// Enable or disable profiling data collection. By default, data collection /// is disabled. /// -/// Rascaline uses the [`time_graph`](https://docs.rs/time-graph/) to collect +/// Featomic uses the [`time_graph`](https://docs.rs/time-graph/) to collect /// timing information on the calculations. This profiling code collects the /// total time spent inside the most important functions, as well as the /// function call graph (which function called which other function). /// -/// You can use `rascal_profiling_clear` to reset profiling data to an empty -/// state, and `rascal_profiling_get` to extract the profiling data. +/// You can use `featomic_profiling_clear` to reset profiling data to an empty +/// state, and `featomic_profiling_get` to extract the profiling data. /// /// @param enabled whether data collection should be enabled or not /// /// @returns The status code of this operation. If the status is not -/// `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the full +/// `FEATOMIC_SUCCESS`, you can use `featomic_last_error()` to get the full /// error message. #[no_mangle] -pub unsafe extern fn rascal_profiling_enable(enabled: bool) -> rascal_status_t { +pub extern fn featomic_profiling_enable(enabled: bool) -> featomic_status_t { catch_unwind(|| { time_graph::enable_data_collection(enabled); Ok(()) @@ -48,24 +48,24 @@ pub unsafe extern fn rascal_profiling_enable(enabled: bool) -> rascal_status_t { /// Extract the current set of data collected for profiling. /// -/// See also `rascal_profiling_enable` and `rascal_profiling_clear`. +/// See also `featomic_profiling_enable` and `featomic_profiling_clear`. /// /// @param format in which format should the data be provided. `"table"`, /// `"short_table"` and `"json"` are currently supported /// @param buffer pre-allocated buffer in which profiling data will be copied. /// If the buffer is too small, this function will return -/// `RASCAL_BUFFER_SIZE_ERROR` +/// `FEATOMIC_BUFFER_SIZE_ERROR` /// @param bufflen size of the `buffer` /// /// @returns The status code of this operation. If the status is not -/// `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the full +/// `FEATOMIC_SUCCESS`, you can use `featomic_last_error()` to get the full /// error message. #[no_mangle] -pub unsafe extern fn rascal_profiling_get( +pub unsafe extern fn featomic_profiling_get( format: *const c_char, buffer: *mut c_char, bufflen: usize, -) -> rascal_status_t { +) -> featomic_status_t { catch_unwind(|| { check_pointers!(format); @@ -80,7 +80,7 @@ pub unsafe extern fn rascal_profiling_get( time_graph::get_full_graph().as_json() }, format => return Err(Error::InvalidParameter(format!( - "invalid data format in rascal_profiling_get: {}, expected 'table', 'short_table' or 'json'", + "invalid data format in featomic_profiling_get: {}, expected 'table', 'short_table' or 'json'", format ))) }; diff --git a/rascaline-c-api/src/status.rs b/featomic/src/c_api/status.rs similarity index 63% rename from rascaline-c-api/src/status.rs rename to featomic/src/c_api/status.rs index 2dd255a67..7a1c318f0 100644 --- a/rascaline-c-api/src/status.rs +++ b/featomic/src/c_api/status.rs @@ -3,7 +3,7 @@ use std::cell::RefCell; use std::os::raw::c_char; use std::ffi::CString; -use rascaline::Error; +use crate::Error; // Save the last error message in thread local storage. // @@ -15,19 +15,19 @@ thread_local! { /// Status type returned by all functions in the C API. /// -/// The value 0 (`RASCAL_SUCCESS`) is used to indicate successful operations. -/// Positive non-zero values are reserved for internal use in rascaline. +/// The value 0 (`FEATOMIC_SUCCESS`) is used to indicate successful operations. +/// Positive non-zero values are reserved for internal use in featomic. /// Negative values are reserved for use in user code, in particular to indicate /// error coming from callbacks. #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[allow(non_camel_case_types)] #[must_use] -pub struct rascal_status_t(i32); +pub struct featomic_status_t(i32); -impl rascal_status_t { +impl featomic_status_t { pub fn is_success(self) -> bool { - self.0 == RASCAL_SUCCESS + self.0 == FEATOMIC_SUCCESS } pub fn as_i32(self) -> i32 { @@ -36,55 +36,52 @@ impl rascal_status_t { } /// Status code used when a function succeeded -pub const RASCAL_SUCCESS: i32 = 0; +pub const FEATOMIC_SUCCESS: i32 = 0; /// Status code used when a function got an invalid parameter -pub const RASCAL_INVALID_PARAMETER_ERROR: i32 = 1; +pub const FEATOMIC_INVALID_PARAMETER_ERROR: i32 = 1; /// Status code used when there was an error reading or writing JSON -pub const RASCAL_JSON_ERROR: i32 = 2; +pub const FEATOMIC_JSON_ERROR: i32 = 2; /// Status code used when a string contains non-utf8 data -pub const RASCAL_UTF8_ERROR: i32 = 3; -/// Status code used for error related to reading files with chemfiles -pub const RASCAL_CHEMFILES_ERROR: i32 = 4; +pub const FEATOMIC_UTF8_ERROR: i32 = 3; /// Status code used for errors coming from the system implementation if we /// don't have a more specific status -pub const RASCAL_SYSTEM_ERROR: i32 = 128; +pub const FEATOMIC_SYSTEM_ERROR: i32 = 128; /// Status code used when a memory buffer is too small to fit the requested data -pub const RASCAL_BUFFER_SIZE_ERROR: i32 = 254; +pub const FEATOMIC_BUFFER_SIZE_ERROR: i32 = 254; /// Status code used when there was an internal error, i.e. there is a bug -/// inside rascaline -pub const RASCAL_INTERNAL_ERROR: i32 = 255; +/// inside featomic +pub const FEATOMIC_INTERNAL_ERROR: i32 = 255; -impl From for rascal_status_t { +impl From for featomic_status_t { #[allow(clippy::match_same_arms)] - fn from(error: Error) -> rascal_status_t { + fn from(error: Error) -> featomic_status_t { LAST_ERROR_MESSAGE.with(|message| { *message.borrow_mut() = CString::new(format!("{}", error)).expect("error message contains a null byte"); }); match error { - Error::InvalidParameter(_) => rascal_status_t(RASCAL_INVALID_PARAMETER_ERROR), - Error::Json(_) => rascal_status_t(RASCAL_JSON_ERROR), - Error::Utf8(_) => rascal_status_t(RASCAL_UTF8_ERROR), - Error::Chemfiles(_) => rascal_status_t(RASCAL_CHEMFILES_ERROR), - Error::BufferSize(_) => rascal_status_t(RASCAL_BUFFER_SIZE_ERROR), + Error::InvalidParameter(_) => featomic_status_t(FEATOMIC_INVALID_PARAMETER_ERROR), + Error::Json(_) => featomic_status_t(FEATOMIC_JSON_ERROR), + Error::Utf8(_) => featomic_status_t(FEATOMIC_UTF8_ERROR), + Error::BufferSize(_) => featomic_status_t(FEATOMIC_BUFFER_SIZE_ERROR), Error::External{status, ..} => { if status < 0 { - rascal_status_t(status) + featomic_status_t(status) } else { - rascal_status_t(RASCAL_SYSTEM_ERROR) + featomic_status_t(FEATOMIC_SYSTEM_ERROR) } }, - Error::Internal(_) => rascal_status_t(RASCAL_INTERNAL_ERROR), - _ => rascal_status_t(RASCAL_INTERNAL_ERROR), + Error::Internal(_) => featomic_status_t(FEATOMIC_INTERNAL_ERROR), + _ => featomic_status_t(FEATOMIC_INTERNAL_ERROR), } } } /// An alternative to `std::panic::catch_unwind` that automatically transform -/// the error into `rascal_status_t`. -pub fn catch_unwind(function: F) -> rascal_status_t where F: FnOnce() -> Result<(), Error> + UnwindSafe { +/// the error into `featomic_status_t`. +pub fn catch_unwind(function: F) -> featomic_status_t where F: FnOnce() -> Result<(), Error> + UnwindSafe { match std::panic::catch_unwind(function) { - Ok(Ok(())) => rascal_status_t(RASCAL_SUCCESS), + Ok(Ok(())) => featomic_status_t(FEATOMIC_SUCCESS), Ok(Err(error)) => error.into(), Err(error) => Error::from(error).into() } @@ -95,7 +92,7 @@ pub fn catch_unwind(function: F) -> rascal_status_t where F: FnOnce() -> Resu macro_rules! check_pointers { ($pointer: ident) => { if $pointer.is_null() { - return Err(rascaline::Error::InvalidParameter( + return Err($crate::Error::InvalidParameter( format!( "got invalid NULL pointer for {} at {}:{}", stringify!($pointer), file!(), line!() @@ -112,7 +109,7 @@ macro_rules! check_pointers { /// /// @returns the last error message, as a NULL-terminated string #[no_mangle] -pub unsafe extern fn rascal_last_error() -> *const c_char { +pub unsafe extern fn featomic_last_error() -> *const c_char { let mut result = std::ptr::null(); let unwind_wrapper = std::panic::AssertUnwindSafe(&mut result); let status = catch_unwind(move || { @@ -125,7 +122,7 @@ pub unsafe extern fn rascal_last_error() -> *const c_char { Ok(()) }); - if status.0 != RASCAL_SUCCESS { + if status.0 != FEATOMIC_SUCCESS { eprintln!("ERROR: unable to get last error message!"); return std::ptr::null(); } diff --git a/rascaline-c-api/src/system.rs b/featomic/src/c_api/system.rs similarity index 60% rename from rascaline-c-api/src/system.rs rename to featomic/src/c_api/system.rs index 61cbb8a35..74c503eca 100644 --- a/rascaline-c-api/src/system.rs +++ b/featomic/src/c_api/system.rs @@ -1,18 +1,17 @@ -use std::os::raw::{c_char, c_void}; -use std::ffi::CStr; +use std::os::raw::c_void; -use rascaline::types::{Vector3D, Matrix3}; -use rascaline::systems::{SimpleSystem, Pair, UnitCell}; -use rascaline::{Error, System}; +use crate::types::{Vector3D, Matrix3}; +use crate::systems::{SimpleSystem, Pair, UnitCell}; +use crate::{Error, SystemBase}; -use crate::RASCAL_SYSTEM_ERROR; +use super::FEATOMIC_SYSTEM_ERROR; -use super::{catch_unwind, rascal_status_t}; +use super::{catch_unwind, featomic_status_t}; /// Pair of atoms coming from a neighbor list #[repr(C)] #[allow(non_camel_case_types)] -pub struct rascal_pair_t { +pub struct featomic_pair_t { /// index of the first atom in the pair pub first: usize, /// index of the second atom in the pair @@ -29,7 +28,7 @@ pub struct rascal_pair_t { pub cell_shift_indices: [i32; 3], } -/// A `rascal_system_t` deals with the storage of atoms and related information, +/// A `featomic_system_t` deals with the storage of atoms and related information, /// as well as the computation of neighbor lists. /// /// This struct contains a manual implementation of a virtual table, allowing to @@ -38,54 +37,54 @@ pub struct rascal_pair_t { /// implementing the `System` trait; and then there is one function pointers /// (`Option`) for each function in the `System` trait. /// -/// The `rascal_status_t` return value for the function is used to communicate -/// error messages. It should be 0/`RASCAL_SUCCESS` in case of success, any +/// The `featomic_status_t` return value for the function is used to communicate +/// error messages. It should be 0/`FEATOMIC_SUCCESS` in case of success, any /// non-zero value in case of error. The error will be propagated to the -/// top-level caller as a `RASCAL_SYSTEM_ERROR` +/// top-level caller as a `FEATOMIC_SYSTEM_ERROR` /// /// A new implementation of the System trait can then be created in any language /// supporting a C API (meaning any language for our purposes); by correctly /// setting `user_data` to the actual data storage, and setting all function /// pointers to the correct functions. For an example of code doing this, see -/// the `SystemBase` class in the Python interface to rascaline. +/// the `SystemBase` class in the Python interface to featomic. /// /// **WARNING**: all function implementations **MUST** be thread-safe, function /// taking `const` pointer parameters can be called from multiple threads at the -/// same time. The `rascal_system_t` itself might be moved from one thread to +/// same time. The `featomic_system_t` itself might be moved from one thread to /// another. // Function pointers have type `Option`, where `Option` -// ensure that the `impl System for rascal_system_t` is forced to deal with the +// ensure that the `impl System for featomic_system_t` is forced to deal with the // function pointer potentially being NULL. `unsafe` is required since these // function come from another language and are not checked by the Rust compiler. // Finally `extern` defaults to `extern "C"`, setting the ABI of the function to // the default C ABI on the current system. #[repr(C)] #[allow(non_camel_case_types)] -pub struct rascal_system_t { +pub struct featomic_system_t { /// User-provided data should be stored here, it will be passed as the /// first parameter to all function pointers below. user_data: *mut c_void, /// This function should set `*size` to the number of atoms in this system - size: Option rascal_status_t>, + size: Option featomic_status_t>, /// This function should set `*types` to a pointer to the first element of /// a contiguous array containing the atomic types of each atom in the /// system. Different atomic types should be identified with a different /// value. These values are usually the atomic number, but don't have to be. - /// The array should contain `rascal_system_t::size()` elements. - types: Option rascal_status_t>, + /// The array should contain `featomic_system_t::size()` elements. + types: Option featomic_status_t>, /// This function should set `*positions` to a pointer to the first element /// of a contiguous array containing the atomic cartesian coordinates. /// `positions[0], positions[1], positions[2]` must contain the x, y, z /// cartesian coordinates of the first atom, and so on. - positions: Option rascal_status_t>, + positions: Option featomic_status_t>, /// This function should write the unit cell matrix in `cell`, which have /// space for 9 values. The cell should be written in row major order, i.e. /// `ax ay az bx by bz cx cy cz`, where a/b/c are the unit cell vectors. - cell: Option rascal_status_t>, + cell: Option featomic_status_t>, /// This function should compute the neighbor list with the given cutoff, /// and store it for later access using `pairs` or `pairs_containing`. - compute_neighbors: Option rascal_status_t>, + compute_neighbors: Option featomic_status_t>, /// This function should set `*pairs` to a pointer to the first element of a /// contiguous array containing all pairs in this system; and `*count` to /// the size of the array/the number of pairs. @@ -95,27 +94,27 @@ pub struct rascal_system_t { /// contains pairs where the distance between atoms is actually bellow the /// cutoff passed in the last call to `compute_neighbors`. This function is /// only valid to call after a call to `compute_neighbors`. - pairs: Option rascal_status_t>, + pairs: Option featomic_status_t>, /// This function should set `*pairs` to a pointer to the first element of a /// contiguous array containing all pairs in this system containing the atom /// with index `atom`; and `*count` to the size of the array/the number of /// pairs. /// - /// The same restrictions on the list of pairs as `rascal_system_t::pairs` + /// The same restrictions on the list of pairs as `featomic_system_t::pairs` /// applies, with the additional condition that the pair `i-j` should be /// included both in the return of `pairs_containing(i)` and /// `pairs_containing(j)`. - pairs_containing: Option rascal_status_t>, + pairs_containing: Option featomic_status_t>, } -unsafe impl Send for rascal_system_t {} -unsafe impl Sync for rascal_system_t {} +unsafe impl Send for featomic_system_t {} +unsafe impl Sync for featomic_system_t {} -impl<'a> System for &'a mut rascal_system_t { +impl<'a> SystemBase for &'a mut featomic_system_t { fn size(&self) -> Result { let function = self.size.ok_or_else(|| Error::External { - status: RASCAL_SYSTEM_ERROR, - message: "rascal_system_t.size function is NULL".into(), + status: FEATOMIC_SYSTEM_ERROR, + message: "featomic_system_t.size function is NULL".into(), })?; let mut value = 0; @@ -126,7 +125,7 @@ impl<'a> System for &'a mut rascal_system_t { if !status.is_success() { return Err(Error::External { status: status.as_i32(), - message: "call to rascal_system_t.size failed".into(), + message: "call to featomic_system_t.size failed".into(), }); } @@ -135,8 +134,8 @@ impl<'a> System for &'a mut rascal_system_t { fn types(&self) -> Result<&[i32], Error> { let function = self.types.ok_or_else(|| Error::External { - status: RASCAL_SYSTEM_ERROR, - message: "rascal_system_t.types function is NULL".into(), + status: FEATOMIC_SYSTEM_ERROR, + message: "featomic_system_t.types function is NULL".into(), })?; let mut ptr = std::ptr::null(); @@ -147,31 +146,31 @@ impl<'a> System for &'a mut rascal_system_t { if !status.is_success() { return Err(Error::External { status: status.as_i32(), - message: "call to rascal_system_t.types failed".into(), + message: "call to featomic_system_t.types failed".into(), }); } let size = self.size()?; if ptr.is_null() && size != 0 { return Err(Error::External { - status: RASCAL_SYSTEM_ERROR, - message: "rascal_system_t.types returned a NULL pointer with non zero size".into(), + status: FEATOMIC_SYSTEM_ERROR, + message: "featomic_system_t.types returned a NULL pointer with non zero size".into(), }); } if size == 0 { return Ok(&[]) - } else { - unsafe { - return Ok(std::slice::from_raw_parts(ptr, self.size()?)); - } + } + + unsafe { + return Ok(std::slice::from_raw_parts(ptr, self.size()?)); } } fn positions(&self) -> Result<&[Vector3D], Error> { let function = self.positions.ok_or_else(|| Error::External { - status: RASCAL_SYSTEM_ERROR, - message: "rascal_system_t.positions function is NULL".into(), + status: FEATOMIC_SYSTEM_ERROR, + message: "featomic_system_t.positions function is NULL".into(), })?; let mut ptr = std::ptr::null(); @@ -181,31 +180,31 @@ impl<'a> System for &'a mut rascal_system_t { if !status.is_success() { return Err(Error::External { status: status.as_i32(), - message: "call to rascal_system_t.positions failed".into(), + message: "call to featomic_system_t.positions failed".into(), }); } let size = self.size()?; if ptr.is_null() && size != 0 { return Err(Error::External { - status: RASCAL_SYSTEM_ERROR, - message: "rascal_system_t.positions returned a NULL pointer with non zero size".into(), + status: FEATOMIC_SYSTEM_ERROR, + message: "featomic_system_t.positions returned a NULL pointer with non zero size".into(), }); } if size == 0 { return Ok(&[]) - } else { - unsafe { - return Ok(std::slice::from_raw_parts(ptr.cast(), self.size()?)); - } + } + + unsafe { + return Ok(std::slice::from_raw_parts(ptr.cast(), self.size()?)); } } fn cell(&self) -> Result { let function = self.cell.ok_or_else(|| Error::External { - status: RASCAL_SYSTEM_ERROR, - message: "rascal_system_t.cell function is NULL".into(), + status: FEATOMIC_SYSTEM_ERROR, + message: "featomic_system_t.cell function is NULL".into(), })?; let mut value = [[0.0; 3]; 3]; @@ -216,7 +215,7 @@ impl<'a> System for &'a mut rascal_system_t { if !status.is_success() { return Err(Error::External { status: status.as_i32(), - message: "call to rascal_system_t.cell failed".into(), + message: "call to featomic_system_t.cell failed".into(), }); } @@ -230,8 +229,8 @@ impl<'a> System for &'a mut rascal_system_t { fn compute_neighbors(&mut self, cutoff: f64) -> Result<(), Error> { let function = self.compute_neighbors.ok_or_else(|| Error::External { - status: RASCAL_SYSTEM_ERROR, - message: "rascal_system_t.compute_neighbors function is NULL".into(), + status: FEATOMIC_SYSTEM_ERROR, + message: "featomic_system_t.compute_neighbors function is NULL".into(), })?; let status = unsafe { @@ -240,7 +239,7 @@ impl<'a> System for &'a mut rascal_system_t { if !status.is_success() { return Err(Error::External { status: status.as_i32(), - message: "call to rascal_system_t.compute_neighbors failed".into(), + message: "call to featomic_system_t.compute_neighbors failed".into(), }); } Ok(()) @@ -248,8 +247,8 @@ impl<'a> System for &'a mut rascal_system_t { fn pairs(&self) -> Result<&[Pair], Error> { let function = self.pairs.ok_or_else(|| Error::External { - status: RASCAL_SYSTEM_ERROR, - message: "rascal_system_t.pairs function is NULL".into(), + status: FEATOMIC_SYSTEM_ERROR, + message: "featomic_system_t.pairs function is NULL".into(), })?; let mut ptr = std::ptr::null(); @@ -260,31 +259,31 @@ impl<'a> System for &'a mut rascal_system_t { if !status.is_success() { return Err(Error::External { status: status.as_i32(), - message: "call to rascal_system_t.pairs failed".into(), + message: "call to featomic_system_t.pairs failed".into(), }); } if ptr.is_null() && count != 0 { return Err(Error::External { - status: RASCAL_SYSTEM_ERROR, - message: "rascal_system_t.pairs returned a NULL pointer with non zero size".into(), + status: FEATOMIC_SYSTEM_ERROR, + message: "featomic_system_t.pairs returned a NULL pointer with non zero size".into(), }); } if count == 0 { return Ok(&[]) - } else { - unsafe { - // SAFETY: ptr is non null, and Pair / rascal_pair_t have the same layout - return Ok(std::slice::from_raw_parts(ptr.cast(), count)); - } + } + + unsafe { + // SAFETY: ptr is non null, and Pair / featomic_pair_t have the same layout + return Ok(std::slice::from_raw_parts(ptr.cast(), count)); } } fn pairs_containing(&self, atom: usize) -> Result<&[Pair], Error> { let function = self.pairs_containing.ok_or_else(|| Error::External { - status: RASCAL_SYSTEM_ERROR, - message: "rascal_system_t.pairs_containing function is NULL".into(), + status: FEATOMIC_SYSTEM_ERROR, + message: "featomic_system_t.pairs_containing function is NULL".into(), })?; let mut ptr = std::ptr::null(); @@ -296,53 +295,53 @@ impl<'a> System for &'a mut rascal_system_t { if !status.is_success() { return Err(Error::External { status: status.as_i32(), - message: "call to rascal_system_t.pairs_containing failed".into(), + message: "call to featomic_system_t.pairs_containing failed".into(), }); } if ptr.is_null() && count != 0 { return Err(Error::External { - status: RASCAL_SYSTEM_ERROR, - message: "rascal_system_t.pairs_containing returned a NULL pointer with non zero size".into(), + status: FEATOMIC_SYSTEM_ERROR, + message: "featomic_system_t.pairs_containing returned a NULL pointer with non zero size".into(), }); } if count == 0 { return Ok(&[]) - } else { - unsafe { - // SAFETY: ptr is non null, and Pair / rascal_pair_t have the same layout - return Ok(std::slice::from_raw_parts(ptr.cast(), count)); - } + } + + unsafe { + // SAFETY: ptr is non null, and Pair / featomic_pair_t have the same layout + return Ok(std::slice::from_raw_parts(ptr.cast(), count)); } } } -/// Convert a Simple System to a `rascal_system_t` -impl From for rascal_system_t { - fn from(system: SimpleSystem) -> rascal_system_t { - unsafe extern fn size(this: *const c_void, size: *mut usize) -> rascal_status_t { +/// Convert a Simple System to a `featomic_system_t` +impl From for featomic_system_t { + fn from(system: SimpleSystem) -> featomic_system_t { + unsafe extern fn size(this: *const c_void, size: *mut usize) -> featomic_status_t { catch_unwind(|| { *size = (*this.cast::()).size()?; Ok(()) }) } - unsafe extern fn types(this: *const c_void, types: *mut *const i32) -> rascal_status_t { + unsafe extern fn types(this: *const c_void, types: *mut *const i32) -> featomic_status_t { catch_unwind(|| { *types = (*this.cast::()).types()?.as_ptr(); Ok(()) }) } - unsafe extern fn positions(this: *const c_void, positions: *mut *const f64) -> rascal_status_t { + unsafe extern fn positions(this: *const c_void, positions: *mut *const f64) -> featomic_status_t { catch_unwind(|| { *positions = (*this.cast::()).positions()?.as_ptr().cast(); Ok(()) }) } - unsafe extern fn cell(this: *const c_void, cell: *mut f64) -> rascal_status_t { + unsafe extern fn cell(this: *const c_void, cell: *mut f64) -> featomic_status_t { catch_unwind(|| { let matrix = (*this.cast::()).cell()?.matrix(); cell.add(0).write(matrix[0][0]); @@ -361,7 +360,7 @@ impl From for rascal_system_t { }) } - unsafe extern fn compute_neighbors(this: *mut c_void, cutoff: f64) -> rascal_status_t { + unsafe extern fn compute_neighbors(this: *mut c_void, cutoff: f64) -> featomic_status_t { catch_unwind(|| { (*this.cast::()).compute_neighbors(cutoff)?; @@ -371,9 +370,9 @@ impl From for rascal_system_t { unsafe extern fn pairs( this: *const c_void, - pairs: *mut *const rascal_pair_t, + pairs: *mut *const featomic_pair_t, count: *mut usize, - ) -> rascal_status_t { + ) -> featomic_status_t { catch_unwind(|| { let all_pairs = (*this.cast::()).pairs()?; *pairs = all_pairs.as_ptr().cast(); @@ -386,9 +385,9 @@ impl From for rascal_system_t { unsafe extern fn pairs_containing( this: *const c_void, atom: usize, - pairs: *mut *const rascal_pair_t, + pairs: *mut *const featomic_pair_t, count: *mut usize, - ) -> rascal_status_t { + ) -> featomic_status_t { catch_unwind(|| { let all_pairs = (*this.cast::()).pairs_containing(atom)?; *pairs = all_pairs.as_ptr().cast(); @@ -398,7 +397,7 @@ impl From for rascal_system_t { }) } - rascal_system_t { + featomic_system_t { user_data: Box::into_raw(Box::new(system)).cast(), size: Some(size), types: Some(types), @@ -410,80 +409,3 @@ impl From for rascal_system_t { } } } - -/// Read all structures in the file at the given `path` using -/// [chemfiles](https://chemfiles.org/), and convert them to an array of -/// `rascal_system_t`. -/// -/// This function can read all [formats supported by -/// chemfiles](https://chemfiles.org/chemfiles/latest/formats.html). -/// -/// This function allocates memory, which must be released using -/// `rascal_basic_systems_free`. -/// -/// If you need more control over the system behavior, consider writing your own -/// instance of `rascal_system_t`. -/// -/// @param path path of the file to read from in the local filesystem -/// @param systems `*systems` will be set to a pointer to the first element of -/// the array of `rascal_system_t` -/// @param count `*count` will be set to the number of systems read from the file -/// -/// @returns The status code of this operation. If the status is not -/// `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the full -/// error message. -#[no_mangle] -#[allow(clippy::missing_panics_doc)] -pub unsafe extern fn rascal_basic_systems_read( - path: *const c_char, - systems: *mut *mut rascal_system_t, - count: *mut usize, -) -> rascal_status_t { - catch_unwind(move || { - check_pointers!(path, systems, count); - let path = CStr::from_ptr(path).to_str()?; - let simple_systems = rascaline::systems::read_from_file(path)?; - - let mut c_systems = Vec::with_capacity(simple_systems.len()); - for system in simple_systems { - c_systems.push(system.into()); - } - - // we rely on this below to drop the vector - assert!(c_systems.capacity() == c_systems.len()); - - *systems = c_systems.as_mut_ptr(); - *count = c_systems.len(); - std::mem::forget(c_systems); - - Ok(()) - }) -} - -/// Release memory allocated by `rascal_basic_systems_read`. -/// -/// This function is only valid to call with a pointer to systems obtained from -/// `rascal_basic_systems_read`, and the corresponding `count`. Any other use -/// will probably result in segmentation faults or double free. If `systems` is -/// NULL, this function does nothing. -/// -/// @param systems pointer to the first element of the array of -/// `rascal_system_t` @param count number of systems in the array -/// -/// @returns The status code of this operation. If the status is not -/// `RASCAL_SUCCESS`, you can use `rascal_last_error()` to get the full -/// error message. -#[no_mangle] -pub unsafe extern fn rascal_basic_systems_free(systems: *mut rascal_system_t, count: usize) -> rascal_status_t { - catch_unwind(|| { - if !systems.is_null() { - let vec = Vec::from_raw_parts(systems, count, count); - for element in vec { - let boxed = Box::from_raw(element.user_data.cast::()); - std::mem::drop(boxed); - } - } - - Ok(()) - }) -} diff --git a/rascaline-c-api/src/utils.rs b/featomic/src/c_api/utils.rs similarity index 95% rename from rascaline-c-api/src/utils.rs rename to featomic/src/c_api/utils.rs index 358d3e0dd..2987e7ef4 100644 --- a/rascaline-c-api/src/utils.rs +++ b/featomic/src/c_api/utils.rs @@ -1,6 +1,6 @@ use std::os::raw::c_char; -use rascaline::Error; +use crate::Error; pub unsafe fn copy_str_to_c(string: &str, buffer: *mut c_char, buflen: usize) -> Result<(), Error> { let size = std::cmp::min(string.len(), buflen - 1); diff --git a/rascaline/src/calculator.rs b/featomic/src/calculator.rs similarity index 97% rename from rascaline/src/calculator.rs rename to featomic/src/calculator.rs index 71e1d8fd6..5c512b9b9 100644 --- a/rascaline/src/calculator.rs +++ b/featomic/src/calculator.rs @@ -1,5 +1,4 @@ use std::collections::BTreeMap; -use std::convert::TryFrom; use log::warn; use metatensor::c_api::MTS_INVALID_PARAMETER_ERROR; @@ -9,8 +8,8 @@ use metatensor::{Labels, LabelsBuilder}; use metatensor::{TensorBlockRef, TensorBlock, TensorMap}; use ndarray::ArrayD; -use crate::{SimpleSystem, System, Error}; - +use crate::{System, Error}; +use crate::systems::SimpleSystem; use crate::calculators::CalculatorBase; pub struct Calculator { @@ -200,7 +199,7 @@ pub struct CalculationOptions<'a> { /// - ``"cell"``, for gradients of the representation with respect to the /// system's cell parameters. These gradients are computed at fixed positions, /// and often not what you want when computing gradients explicitly (they are - /// mainly used in ``rascaline.torch`` to integrate with backward + /// mainly used in ``featomic.torch`` to integrate with backward /// propagation). /// /// $$ \left. \frac{\partial \langle q \vert A_i \rangle} {\partial \mathbf{H}} \right |_\mathbf{r} $$ @@ -285,7 +284,7 @@ impl Calculator { } #[time_graph::instrument(name="Calculator::prepare")] - fn prepare(&mut self, systems: &mut [Box], options: CalculationOptions) -> Result { + fn prepare(&mut self, systems: &mut [System], options: CalculationOptions) -> Result { let default_keys = self.implementation.keys(systems)?; let keys = match options.selected_keys { @@ -348,7 +347,7 @@ impl Calculator { ))); } - if std::env::var("RASCALINE_NO_WARN_CELL_GRADIENTS").is_err() { + if std::env::var("FEATOMIC_NO_WARN_CELL_GRADIENTS").is_err() { // TODO: remove this warning around November 2024 (~6 months // after this change) warn!( @@ -503,14 +502,14 @@ impl Calculator { /// features. pub fn compute( &mut self, - systems: &mut [Box], + systems: &mut [System], options: CalculationOptions, ) -> Result { let mut native_systems; let systems = if options.use_native_system { native_systems = Vec::with_capacity(systems.len()); for system in systems { - native_systems.push(Box::new(SimpleSystem::try_from(&**system)?) as Box); + native_systems.push(System::new(SimpleSystem::try_from(&**system)?) as System); } &mut native_systems } else { @@ -547,9 +546,12 @@ use crate::calculators::SortedDistances; use crate::calculators::NeighborList; use crate::calculators::{SphericalExpansionByPair, SphericalExpansionParameters}; use crate::calculators::SphericalExpansion; -use crate::calculators::{SoapPowerSpectrum, PowerSpectrumParameters}; use crate::calculators::{SoapRadialSpectrum, RadialSpectrumParameters}; +use crate::calculators::{SoapPowerSpectrum, PowerSpectrumParameters}; +use crate::calculators::{SphericalExpansionForBonds, SphericalExpansionForBondsParameters}; use crate::calculators::{LodeSphericalExpansion, LodeSphericalExpansionParameters}; + + type CalculatorCreator = fn(&str) -> Result, Error>; macro_rules! add_calculator { @@ -581,6 +583,7 @@ static REGISTERED_CALCULATORS: Lazy> = add_calculator!(map, "spherical_expansion", SphericalExpansion, SphericalExpansionParameters); add_calculator!(map, "soap_radial_spectrum", SoapRadialSpectrum, RadialSpectrumParameters); add_calculator!(map, "soap_power_spectrum", SoapPowerSpectrum, PowerSpectrumParameters); + add_calculator!(map, "spherical_expansion_for_bonds", SphericalExpansionForBonds, SphericalExpansionForBondsParameters); add_calculator!(map, "lode_spherical_expansion", LodeSphericalExpansion, LodeSphericalExpansionParameters); return map; diff --git a/rascaline/src/calculators/atomic_composition.rs b/featomic/src/calculators/atomic_composition.rs similarity index 96% rename from rascaline/src/calculators/atomic_composition.rs rename to featomic/src/calculators/atomic_composition.rs index 41be866dd..530db5ba2 100644 --- a/rascaline/src/calculators/atomic_composition.rs +++ b/featomic/src/calculators/atomic_composition.rs @@ -36,7 +36,7 @@ impl CalculatorBase for AtomicComposition { &[] } - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { return CenterTypesKeys.keys(systems); } @@ -48,7 +48,7 @@ impl CalculatorBase for AtomicComposition { return vec!["system", "atom"]; } - fn samples(&self, keys: &Labels, systems: &mut [Box]) -> Result, Error> { + fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error> { assert_eq!(keys.names(), ["center_type"]); let mut samples = Vec::new(); for [center_type_key] in keys.iter_fixed_size() { @@ -84,7 +84,7 @@ impl CalculatorBase for AtomicComposition { &self, keys: &Labels, _samples: &[Labels], - _systems: &mut [Box], + _systems: &mut [System], ) -> Result, Error> { // Positions/cell gradients of the composition are zero everywhere. // Therefore, we only return a vector of empty labels (one for each key). @@ -110,7 +110,7 @@ impl CalculatorBase for AtomicComposition { fn compute( &mut self, - systems: &mut [Box], + systems: &mut [System], descriptor: &mut TensorMap, ) -> Result<(), Error> { assert_eq!(descriptor.keys().names(), ["center_type"]); diff --git a/featomic/src/calculators/bondatom/bond_atom_math.rs b/featomic/src/calculators/bondatom/bond_atom_math.rs new file mode 100644 index 000000000..bb4a72de7 --- /dev/null +++ b/featomic/src/calculators/bondatom/bond_atom_math.rs @@ -0,0 +1,803 @@ +use std::collections::BTreeMap; +use std::collections::btree_map::Entry; +use std::cell::RefCell; +use thread_local::ThreadLocal; +use log::warn; + +use crate::Error; +use crate::types::{Vector3D,Matrix3}; +use crate::systems::BATripletInfo; + +use crate::calculators::soap::Cutoff; +use crate::calculators::shared::{Density, SoapRadialBasis, SphericalExpansionBasis}; + +use crate::calculators::soap::SoapRadialIntegralCacheByAngular; +use crate::math::SphericalHarmonicsCache; + + +/// for a given vector (`vec`), compute a rotation matrix (`M`) so that `M×vec` +/// is expressed as `(0,0,+z)` +/// currently, this matrix corresponds to a rotatoin expressed as `-z;+y;+z` in euler angles, +/// or as `(x,y,0),theta` in axis-angle representation. +fn rotate_vector_to_z(vec: Vector3D) -> Matrix3 { + // re-orientation is done through a rotation matrix, computed through the axis-angle and quaternion representations of the rotation + // axis/angle representation of the rotation: axis is norm(-y,x,0), angle is arctan2( sqrt(x**2+y**2), z) + // meaning sin(angle) = sqrt((x**2+y**2) /r2); cos(angle) = z/sqrt(r2) + + let (xylen,len) = { + let xyl = vec[0]*vec[0] + vec[1]*vec[1]; + (xyl.sqrt(), (xyl+vec[2]*vec[2]).sqrt()) + }; + + if xylen.abs()<1E-7 { + if vec[2] < 0. { + return Matrix3::new([[-1.,0.,0.], [0.,1.,0.], [0.,0.,-1.]]) + } + else { + return Matrix3::new([[1.,0.,0.], [0.,1.,0.], [0.,0.,1.]]) + } + } + + let c = vec[2]/len; + let s = xylen/len; + let t = 1. - c; + + let x2 = -vec[1]/xylen; + let y2 = vec[0]/xylen; + + let tx = t*x2; + let sx = s*x2; + let sy = s*y2; + + return Matrix3::new([ + [tx*x2 +c, tx*y2, -sy], + [tx*y2, t*y2*y2 + c, sx], + [sy, -sx, c], + ]); +} + + +/// returns the derivatives of the reoriention matrix with the three components of the vector to reorient +fn rotate_vector_to_z_derivatives(vec: Vector3D) -> (Matrix3,Matrix3,Matrix3) { + + let (xylen,len) = { + let xyl = vec[0]*vec[0] + vec[1]*vec[1]; + (xyl.sqrt(), (xyl+vec[2]*vec[2]).sqrt()) + }; + + if xylen.abs()<1E-7 { + let co = 1./len; + if vec[2] < 0. { + warn!("trying to get the derivative of a rotation near a breaking point: expect pure jank"); + return ( + //Matrix3::new([[-1.,0.,0.], [0.,1.,0.], [0.,0.,-1.]]) <- the value to derive off of: a +y rotation + Matrix3::new([[0.,0.,-co], [0.,0.,0.], [co,0.,0.]]), // +x change -> +y rotation + Matrix3::new([[0.,0.,0.], [0.,0.,-co], [0.,-co,0.]]), // +y change -> -x rotation + Matrix3::new([[0.,0.,0.], [0.,0.,0.], [0.,0.,0.]]), // +z change -> nuthin + ) + } + else { + return ( + //Matrix3::new([[1.,0.,0.], [0.,1.,0.], [0.,0.,1.]]) <- the value to derive off of + Matrix3::new([[0.,0.,-co], [0.,0.,0.], [co,0.,0.]]), // +x change -> -y rotation + Matrix3::new([[0.,0.,0.], [0.,0.,-co], [0.,co,0.]]), // +y change -> +x rotation + Matrix3::new([[0.,0.,0.], [0.,0.,0.], [0.,0.,0.]]), // +z change -> nuthin + ) + } + } + + let inv_len = 1./len; + let inv_len2 = inv_len*inv_len; + let inv_len3 = inv_len2*inv_len; + let inv_xy = 1./xylen; + let inv_xy2 = inv_xy*inv_xy; + let inv_xy3 = inv_xy2*inv_xy; + + let c = vec[2]/len; // needed + let dcdz = 1./len - vec[2]*vec[2]*inv_len3; + let dcdx = -vec[2]*vec[0]*inv_len3; + let dcdy = -vec[2]*vec[1]*inv_len3; + let s = xylen/len; + let dsdx = vec[0]*inv_len*(inv_xy - xylen*inv_len2); + let dsdy = vec[1]*inv_len*(inv_xy - xylen*inv_len2); + let dsdz = -xylen*vec[2]*inv_len3; + + let t = 1. - c; + + let x2 = -vec[1]*inv_xy; + let dx2dx = vec[1]*vec[0]*inv_xy3; + let dx2dy = inv_xy * (-1. + vec[1]*vec[1]*inv_xy2); + + let y2 = vec[0]/xylen; + let dy2dy = -vec[1]*vec[0]*inv_xy3; + let dy2dx = inv_xy * (1. - vec[0]*vec[0]*inv_xy2); + + let tx = t*x2; + let dtxdx = -dcdx*x2 + t*dx2dx; + let dtxdy = -dcdy*x2 + t*dx2dy; + let dtxdz = -dcdz*x2; + + //let sx = s*x2; // needed + let dsxdx = dsdx*x2 + s*dx2dx; + let dsxdy = dsdy*x2 + s*dx2dy; + let dsxdz = dsdz*x2; + + //let sy = s*y2; //needed + let dsydx = dsdx*y2 + s*dy2dx; + let dsydy = dsdy*y2 + s*dy2dy; + let dsydz = dsdz*y2; + + //let t1 = tx*x2 +c; // needed + let dt1dx = dcdx + dtxdx*x2 + tx*dx2dx; + let dt1dy = dcdy + dtxdy*x2 + tx*dx2dy; + let dt1dz = dcdz + dtxdz*x2; + + //let t2 = tx*y2; // needed + let dt2dx = dtxdx*y2 + tx*dy2dx; + let dt2dy = dtxdy*y2 + tx*dy2dy; + let dt2dz = dtxdz*y2; + + //let t3 = t*y2*y2 +c; // needed + let dt3dx = -dcdx*y2*y2 + 2.*t*y2*dy2dx +dcdx; + let dt3dy = -dcdy*y2*y2 + 2.*t*y2*dy2dy +dcdy; + let dt3dz = -dcdz*y2*y2 +dcdz; + + return ( + // Matrix3::new([ + // [tx*x2 +c, tx*y2, -sy], + // [tx*y2, t*y2*y2 + c, sx], + // [sy, -sx, c], + // ]), + Matrix3::new([ + [dt1dx, dt2dx, -dsydx], + [dt2dx, dt3dx, dsxdx], + [dsydx, -dsxdx, dcdx], + ]), + Matrix3::new([ + [dt1dy, dt2dy, -dsydy], + [dt2dy, dt3dy, dsxdy], + [dsydy, -dsxdy, dcdy], + ]), + Matrix3::new([ + [dt1dz, dt2dz, -dsydz], + [dt2dz, dt3dz, dsxdz], + [dsydz, -dsxdz, dcdz], + ]), + ); +} + +/// result structure for canonical_vector_for_single_triplet +#[derive(Default,Debug)] +pub(crate) struct VectorResult{ + /// the canonical vector itelf + pub vect: Vector3D, + /// gradients of the canonical vector, as an array of three matrices + /// matrix is [quantity_component,gradient_component] + /// each matrix corresponds to a different atom to gradiate upon + pub grads: [Option<(usize,Matrix3)>;3], +} + +/// From a list of bond/atom triplets, compute the 'canonical third vector'. +/// Each triplet is composed of two 'neighbors': one is the center of a pair of atoms +/// (first and second atom), and the other is a simple atom (third atom). +/// The third vector of such a triplet is the vector from the center of the atom pair and to the third atom. +/// this third vector becomes canonical when the frame of reference is rotated to express +/// the triplet's bond vector (vector from the first and to the second atom) as (0,0,+z). +/// +/// Users can request either a "full" neighbor list (including an entry for both +/// `i-j +k` triplets and `j-i +k` triplets) or save memory/computational by only +/// working with "half" neighbor list (only including one entry for each `i-j +k` +/// bond) +/// When using a half neighbor list, i and j are ordered so the atom with the smallest species comes first. +/// +/// The two first atoms must not be the same atom, but the third atom may be one of them, +/// if the `bond_conbtribution` option is active +/// (When periodic boundaries arise, atom which must not be the same may be images of each other.) +/// +/// This sample produces a single property (`"distance"`) with three components +/// (`"vector_direction"`) containing the x, y, and z component of the vector from +/// the center of the triplet's 'bond' to the triplet's 'third atom', in the bond's canonical orientation. +/// +/// In addition to the atom indexes, the samples also contain a pair and triplet index, +/// to be able to distinguish between multiple triplets involving the same atoms +/// (which can occur in periodic boundary conditions when the cutoffs are larger than the unit cell). +pub(crate) fn canonical_vector_for_single_triplet( + triplet: &BATripletInfo, + invert: bool, + compute_grad: bool, + mtx_cache: &mut BTreeMap<(usize,usize,[i32;3],bool),Matrix3>, + dmtx_cache: &mut BTreeMap<(usize,usize,[i32;3],bool),(Matrix3,Matrix3,Matrix3)>, +) -> Result { + + let bond_vector = triplet.bond_vector; + let third_vector = triplet.third_vector; + let (atom_i,atom_j,bond_vector) = if invert { + (triplet.atom_j, triplet.atom_i, -bond_vector) + } else { + (triplet.atom_i, triplet.atom_j, bond_vector) + }; + + let mut res = VectorResult::default(); + + if triplet.is_self_contrib { + let vec_len = third_vector.norm(); + let vec_len = if third_vector * bond_vector > 0. { + // third atom on second atom + vec_len + } else { + // third atom on first atom + -vec_len + }; + res.vect[2] = vec_len; + + if compute_grad { + let inv_len = 1./vec_len; + + res.grads[0] = Some((atom_i,Matrix3::new([ + [ -0.25* inv_len * third_vector[0], 0., 0.], + [ 0., -0.25* inv_len * third_vector[0], 0.], + [ 0., 0., -0.25* inv_len * third_vector[0]], + ]))); + res.grads[1] = Some((atom_j,Matrix3::new([ + [ 0.25* inv_len * third_vector[0], 0., 0.], + [ 0., 0.25* inv_len * third_vector[0], 0.], + [ 0., 0., 0.25* inv_len * third_vector[0]], + ]))); + + } + } else { + + let tf_mtx = match mtx_cache.entry((triplet.atom_i,triplet.atom_j,triplet.bond_cell_shift,invert)) { + Entry::Occupied(entry) => entry.get().clone(), + Entry::Vacant(entry) => { + entry.insert(rotate_vector_to_z(bond_vector)).clone() + }, + }; + res.vect = tf_mtx * third_vector; + + if compute_grad { + + // for a transformed vector v from an untransformed vector u, + // dv = TF*du + dTF*u + // also: the indexing of the gradient array is: i_gradsample, derivation_component, value_component, i_property + + let du_term = -0.5* tf_mtx; + let (tf_mtx_dx, tf_mtx_dy, tf_mtx_dz) = match dmtx_cache.entry((triplet.atom_i,triplet.atom_j,triplet.bond_cell_shift,invert)) { + Entry::Occupied(entry) => entry.get().clone(), + Entry::Vacant(entry) => { + entry.insert(rotate_vector_to_z_derivatives(bond_vector)).clone() + }, + }; + + let dmat_term_dx = tf_mtx_dx * third_vector; + let dmat_term_dy = tf_mtx_dy * third_vector; + let dmat_term_dz = tf_mtx_dz * third_vector; + + res.grads[0] = Some((atom_i,Matrix3::new([ + [-dmat_term_dx[0] + du_term[0][0], -dmat_term_dy[0] + du_term[0][1], -dmat_term_dz[0] + du_term[0][2]], + [-dmat_term_dx[1] + du_term[1][0], -dmat_term_dy[1] + du_term[1][1], -dmat_term_dz[1] + du_term[1][2]], + [-dmat_term_dx[2] + du_term[2][0], -dmat_term_dy[2] + du_term[2][1], -dmat_term_dz[2] + du_term[2][2]], + ]))); + res.grads[1] = Some((atom_j,Matrix3::new([ + [dmat_term_dx[0] + du_term[0][0], dmat_term_dy[0] + du_term[0][1], dmat_term_dz[0] + du_term[0][2]], + [dmat_term_dx[1] + du_term[1][0], dmat_term_dy[1] + du_term[1][1], dmat_term_dz[1] + du_term[1][2]], + [dmat_term_dx[2] + du_term[2][0], dmat_term_dy[2] + du_term[2][1], dmat_term_dz[2] + du_term[2][2]], + ]))); + res.grads[2] = Some((triplet.atom_k,tf_mtx)); + } + } + return Ok(res); +} + +// /// get the result of canonical_vector_for_single_triplet +// /// and store it in a TensorBlock +// pub(crate) fn canonical_vector_for_single_triplet_inplace( +// triplet: &BATripletInfo, +// out_block: &mut TensorBlockRefMut, +// sample_i: usize, +// system_i: usize, +// invert: bool, +// mtx_cache: &mut BTreeMap<(usize,usize,[i32;3],bool),Matrix3>, +// dmtx_cache: &mut BTreeMap<(usize,usize,[i32;3],bool),(Matrix3,Matrix3,Matrix3)>, +// ) -> Result<(),Error> { +// let compute_grad = out_block.gradient_mut("positions").is_some(); +// let block_data = out_block.data_mut(); +// let array = block_data.values.to_array_mut(); + +// let res = canonical_vector_for_single_triplet( +// triplet, +// invert, +// compute_grad, +// mtx_cache, +// dmtx_cache +// )?; + +// array[[sample_i, 0, 0]] = res.vect[0]; +// array[[sample_i, 1, 0]] = res.vect[1]; +// array[[sample_i, 2, 0]] = res.vect[2]; + +// if let Some(mut gradient) = out_block.gradient_mut("positions") { +// let gradient = gradient.data_mut(); +// let array = gradient.values.to_array_mut(); + +// for grad in res.grads { +// if let Some((atom_i, grad_mtx)) = grad { +// let grad_sample_i = gradient.samples.position(&[ +// sample_i.into(), system_i.into(), atom_i.into() +// ]).expect("missing gradient sample"); + +// array[[grad_sample_i, 0, 0, 0]] = grad_mtx[0][0]; +// array[[grad_sample_i, 1, 0, 0]] = grad_mtx[0][1]; +// array[[grad_sample_i, 2, 0, 0]] = grad_mtx[0][2]; +// array[[grad_sample_i, 0, 1, 0]] = grad_mtx[1][0]; +// array[[grad_sample_i, 1, 1, 0]] = grad_mtx[1][1]; +// array[[grad_sample_i, 2, 1, 0]] = grad_mtx[1][2]; +// array[[grad_sample_i, 0, 2, 0]] = grad_mtx[2][0]; +// array[[grad_sample_i, 1, 2, 0]] = grad_mtx[2][1]; +// array[[grad_sample_i, 2, 2, 0]] = grad_mtx[2][2]; +// } +// } +// } +// Ok(()) +// } + + +/// Contribution of a single triplet to the spherical expansion +pub(super) struct ExpansionContribution { + /// Values of the contribution. The `BTreeMap` contains one array for each + /// angular channel, and the shape of the arrays is (2 * L + 1, N) + pub values: BTreeMap>, + /// Gradients of the contribution w.r.t. the distance between the atoms in + /// the pair. shape of the arrays is (3, 2 * L + 1, N) + pub gradients: Option>>, +} + +impl ExpansionContribution { + pub fn new(angular_channels: &[usize], radial_sizes: &[usize], do_gradients: bool) -> Self { + let values = angular_channels.iter().zip(radial_sizes).map(|(&o3_lambda, &radial_size)| { + let array = ndarray::Array2::from_elem((2 * o3_lambda + 1, radial_size), 0.0); + (o3_lambda, array) + }).collect(); + + let gradients = if do_gradients { + Some(angular_channels.iter().zip(radial_sizes).map(|(&o3_lambda, &radial_size)| { + let array = ndarray::Array3::from_elem((3, 2 * o3_lambda + 1, radial_size), 0.0); + (o3_lambda, array) + }).collect()) + } else { + None + }; + + Self { values, gradients } + } +} + +/// Parameters for spherical expansion calculator. +/// +/// The spherical expansion is at the core of representations in the SOAP +/// (Smooth Overlap of Atomic Positions) family. The core idea is to define +/// atom-centered environments using a spherical cutoff; create an atomic +/// density according to all neighbors in a given environment; and finally +/// expand this density on a given set of basis functions. The parameters for +/// each of these steps can be defined separately below. +/// +/// See [this review article](https://doi.org/10.1063/1.5090481) for more +/// information on the SOAP representation, and [this +/// paper](https://doi.org/10.1063/5.0044689) for information on how it is +/// implemented in featomic. +#[derive(Debug, Clone)] +#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct RawSphericalExpansionParameters { + /// Definition of the atomic environment within a ("third") cutoff, + /// and how neighboring atoms enter and leave the environment. + pub cutoff: Cutoff, + /// Definition of the density arising from atoms in the local environment. + pub density: Density, + /// Definition of the basis functions used to expand the atomic density + pub basis: SphericalExpansionBasis, +} + +impl RawSphericalExpansionParameters { + /// Validate all the parameters + pub fn validate(&mut self) -> Result<(), Error> { + self.cutoff.validate()?; + if let Some(scaling) = self.density.scaling { + scaling.validate()?; + } + + // try constructing a radial integral cache to catch any errors early + SoapRadialIntegralCacheByAngular::new(self.cutoff.radius, self.density.kind, &self.basis)?; + + return Ok(()); + } +} + +pub(super) struct RawSphericalExpansion{ + parameters: RawSphericalExpansionParameters, + /// implementation + cached allocation to compute the radial integral for a + /// single pair + /// implementation + cached allocation to compute the radial integral for a + /// single pair + radial_integral: ThreadLocal>, + /// implementation + cached allocation to compute the spherical harmonics + /// for a single pair + spherical_harmonics: ThreadLocal>, +} + +impl RawSphericalExpansion { + pub(super) fn new(parameters: RawSphericalExpansionParameters) -> Self { + Self{ + parameters, + radial_integral: ThreadLocal::new(), + spherical_harmonics: ThreadLocal::new(), + } + } + + pub(super) fn parameters(&self) -> &RawSphericalExpansionParameters { + &self.parameters + } + + pub(super) fn make_contribution_buffer(&self, do_gradients: bool) -> ExpansionContribution { + let angular = self.parameters.basis.angular_channels(); + let radial = angular.iter().map(|&o3_lambda| match self.parameters.basis{ + SphericalExpansionBasis::TensorProduct(ref basis) => basis.radial.size(), + SphericalExpansionBasis::Explicit(ref basis) => { + basis.by_angular.get(&o3_lambda).expect("missing o3_lambda").size() + }, + }).collect::>(); + ExpansionContribution::new(&angular, &radial, do_gradients) + } + + /// Compute the product of radial scaling & cutoff smoothing functions + pub(crate) fn scaling_functions(&self, r: f64) -> f64 { + let mut scaling = 1.0; + if let Some(scaler) = self.parameters.density.scaling { + scaling = scaler.compute(r); + } + return scaling * self.parameters.cutoff.smoothing(r); + } + + // /// Compute the gradient of the product of radial scaling & cutoff smoothing functions + // pub(crate) fn scaling_functions_gradient(&self, r: f64) -> f64 { + // let mut scaling = 1.0; + // let mut scaling_grad = 0.0; + // if let Some(scaler) = self.parameters.density.scaling { + // scaling = scaler.compute(r); + // scaling_grad = scaler.gradient(r); + // } + + // let cutoff = self.parameters.cutoff.smoothing(r); + // let cutoff_grad = self.parameters.cutoff.smoothing_gradient(r); + + // return cutoff_grad * scaling + cutoff * scaling_grad; + // } + + /// compute the spherical expansion coefficients associated with + /// a single center->neighbor vector (`vector`), + /// and store it in a ExpansionContribution buffer (`contribution`). + /// `gradient_orientation` serves two purposes: + /// it tells whether or not the gradient should be computed, + /// and it deals with the case where the vector (and the spherical expansion) take place + /// in a rotated/scaled/sheared frame of reference: its three vectors contain + /// the changes of the vector (in the rotated frame of reference) when adding the + /// +x, +y, and +z vectors from the 'real' frame of reference. + /// `extra_scaling` simply applies a scaling factor to all coefficients + pub(super) fn compute_coefficients(&self, contribution: &mut ExpansionContribution, vector: Vector3D, extra_scaling: f64, gradient_orientation: Option<(Vector3D,Vector3D,Vector3D)>){ + let mut radial_integral = self.radial_integral.get_or(|| { + RefCell::new(SoapRadialIntegralCacheByAngular::new( + self.parameters.cutoff.radius, + self.parameters.density.kind, + &self.parameters.basis + ).expect("invalid radial integral parameters") + ) + }).borrow_mut(); + + let mut spherical_harmonics = self.spherical_harmonics.get_or(|| { + let max_angular = self.parameters.basis.angular_channels().into_iter().max().unwrap_or(0); + RefCell::new(SphericalHarmonicsCache::new(max_angular)) + }).borrow_mut(); + + let distance = vector.norm(); + let direction = if distance < 1e-6 { + Vector3D::new(0.0, 0.0, 1.0) + } else { + vector/distance + }; + + // Compute the three factors that appear in the center contribution. + // Note that this is simply the pair contribution for the special + // case where the pair distance is zero. + radial_integral.compute(distance, gradient_orientation.is_some()); + spherical_harmonics.compute(direction, gradient_orientation.is_some()); + + let f_scaling = self.scaling_functions(distance) * extra_scaling; + //let f_scaling_grad = self.scaling_functions_gradient(distance); + + for o3_lambda in self.parameters.basis.angular_channels() { + let radial_basis_size = match self.parameters.basis { + SphericalExpansionBasis::TensorProduct(ref basis) => basis.radial.size(), + SphericalExpansionBasis::Explicit(ref basis) => { + basis.by_angular.get(&o3_lambda).expect("missing o3_lambda").size() + }, + }; + + // let spherical_harmonics_grad = [ + // spherical_harmonics.gradients[0].angular_slice(o3_lambda), + // spherical_harmonics.gradients[1].angular_slice(o3_lambda), + // spherical_harmonics.gradients[2].angular_slice(o3_lambda), + // ]; + let spherical_harmonics = spherical_harmonics.values.angular_slice(o3_lambda); + + // let radial_integral_grad = &radial_integral.get(o3_lambda).expect("missing o3_lambda").gradients; + let radial_integral = &radial_integral.get(o3_lambda).expect("missing o3_lambda").values; + + let values = contribution.values.get_mut(&o3_lambda).expect("missing o3_lambda"); + debug_assert_eq!( + values.shape(), + [2*o3_lambda+1, radial_basis_size] + ); + + // compute the full spherical expansion coefficients & gradients + for m in 0..(2 * o3_lambda + 1) { + let sph_value = spherical_harmonics[m]; + for (n, ri_value) in radial_integral.iter().enumerate() { + values[[m, n]] = f_scaling * sph_value * ri_value; + } + } + + if let Some(ref mut _gradients) = contribution.gradients { + unimplemented!("ööps, gradient not ready yet"); + + // let ilen = 1./distance; + // let dlen_dv = direction; + // let (dv_dx,dv_dy,dv_dz) = gradient_orientation.expect("`gradient_orientation` needed when gradient appears"); + // let dlen_dx = dlen_dv*dv_dx; + // let dlen_dy = dlen_dv*dv_dy; + // let dlen_dz = dlen_dv*dv_dz; + // let ddir_dx = dv_dx*ilen - vector*dlen_dx*ilen*ilen; + // let ddir_dy = dv_dy*ilen - vector*dlen_dy*ilen*ilen; + // let ddir_dz = dv_dy*ilen - vector*dlen_dz*ilen*ilen; + + // let gradients = gradients.get_mut(&o3_lambda).expect("missing o3_lambda"); + + // // let single_grad = |i:usize,ri_value:f64, sph_value:f64, ri_grad:f64, sph_grad:f64| { + // // f_scaling_grad * dlen_dv[i] * ri_value * sph_value + // // + f_scaling * ri_grad * dlen_dv[i] * sph_value + // // + f_scaling * ri_value * sph_grad / distance + // // }; + // let single_grad_v2 = |ri_val: f64, sph_val: f64, ri_grad: f64, sph_grad: Vector3D, dlen_da: f64, ddir_da: Vector3D| { + // (dlen_da * f_scaling_grad) * ri_val * sph_val + // + f_scaling * (dlen_da * ri_grad) * sph_val + // + f_scaling * ri_val * (ddir_da * sph_grad) + // }; + + // for m in 0..(2 * o3_lambda + 1) { + // let sph_value = spherical_harmonics[m]; + // let sph_grad_x = spherical_harmonics_grad[0][m]; + // let sph_grad_y = spherical_harmonics_grad[1][m]; + // let sph_grad_z = spherical_harmonics_grad[2][m]; + // let sph_grad = Vector3D::new(sph_grad_x, sph_grad_y, sph_grad_z); + + // for n in 0..radial_basis_size { + // let ri_value = radial_integral[n]; + // let ri_grad = radial_integral_grad[n]; + + // // gradients[[0, m, n]] = single_grad(0, ri_value, sph_value, ri_grad, sph_grad_x); + // // gradients[[1, m, n]] = single_grad(1, ri_value, sph_value, ri_grad, sph_grad_y); + // // gradients[[2, m, n]] = single_grad(2, ri_value, sph_value, ri_grad, sph_grad_z); + // gradients[[0, m, n]] = single_grad_v2(ri_value, sph_value, ri_grad, sph_grad, dlen_dx, ddir_dx); + // gradients[[1, m, n]] = single_grad_v2(ri_value, sph_value, ri_grad, sph_grad, dlen_dy, ddir_dy); + // gradients[[2, m, n]] = single_grad_v2(ri_value, sph_value, ri_grad, sph_grad, dlen_dz, ddir_dz); + // } + // } + } + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + use super::Vector3D; + + use approx::assert_relative_eq; + + use crate::systems::BATripletNeighborList; + use crate::systems::test_utils::test_systems; + use super::{RawSphericalExpansion,RawSphericalExpansionParameters}; + use super::canonical_vector_for_single_triplet; + //use super::super::CalculatorBase; + + #[test] + fn half_neighbor_list() { + let pre_calculator = BATripletNeighborList{ + cutoffs: [2.0,2.0], + }; + + let mut systems = test_systems(&["water"]); + let expected = &[[ + [0.0, 0.0, -0.478948537162397], // SC 0 1 0 + [0.0, 0.0, 0.478948537162397], // SC 0 1 1 + [0.0, 0.9289563, -0.7126298], // 0 1 2 + [0.0, 0.0, -0.478948537162397], // SC 1 0 1 + [0.0, 0.0, 0.478948537162397], // SC 1 0 0 + [0.0, -0.9289563, -0.7126298], // 1 0 2 + [0.0, 0.0, -0.75545], // SC 1 2 1 + [0.0, 0.0, 0.75545], // SC 1 2 2 + [0.0, 0.58895, 0.0], // 1 2 0 + ]]; + for (system,expected) in systems.iter_mut().zip(expected) { + let mut mtx_cache = BTreeMap::new(); + let mut dmtx_cache = BTreeMap::new(); + pre_calculator.ensure_computed_for_system(system).unwrap(); + let triplets = pre_calculator.get_for_system(system).unwrap() + .into_iter().filter(|v| v.bond_cell_shift == [0,0,0] && v.third_cell_shift == [0,0,0]); + for (expected,triplet) in expected.iter().zip(triplets) { + let res = canonical_vector_for_single_triplet(&triplet, false, false, &mut mtx_cache, &mut dmtx_cache).unwrap(); + assert_relative_eq!(res.vect, Vector3D::new(expected[0],expected[1],expected[2]), max_relative=1e-6); + } + } + } + + #[test] + fn full_neighbor_list() { + let pre_calculator = BATripletNeighborList{ + cutoffs: [2.0,2.0], + }; + + let mut systems = test_systems(&["water"]); + let expected = &[[ + [0.0, 0.0, 0.478948537162397], // SC 0 1 0 + [0.0, 0.0, -0.478948537162397], // SC 0 1 1 + [0.0, -0.9289563, 0.7126298], // 0 1 2 + [0.0, 0.0, 0.478948537162397], // SC 1 0 1 + [0.0, 0.0, -0.478948537162397], // SC 1 0 0 + [0.0, 0.9289563, 0.7126298], // 1 0 2 + [0.0, 0.0, 0.75545], // SC 1 2 1 + [0.0, 0.0, -0.75545], // SC 1 2 2 + [0.0, -0.58895, 0.0], // 1 2 0 + ]]; + for (system,expected) in systems.iter_mut().zip(expected) { + let mut mtx_cache = BTreeMap::new(); + let mut dmtx_cache = BTreeMap::new(); + pre_calculator.ensure_computed_for_system(system).unwrap(); + let triplets = pre_calculator.get_for_system(system).unwrap() + .into_iter().filter(|v| v.bond_cell_shift == [0,0,0] && v.third_cell_shift == [0,0,0]); + for (expected,triplet) in expected.iter().zip(triplets) { + let res = canonical_vector_for_single_triplet(&triplet, true, false, &mut mtx_cache, &mut dmtx_cache).unwrap(); + assert_relative_eq!(res.vect, Vector3D::new(expected[0],expected[1],expected[2]), max_relative=1e-6); + } + } + } + + // note: the following test does pass, but gradients are disabled because we discovered that + // the values of this calculator ARE NOT CONTINUOUS around the values of bond_vector == (0,0,-z) + // //// + // #[test] + // fn finite_differences_positions() { + // // half neighbor list + // let calculator = Calculator::from(Box::new(BANeighborList::Half(HalfBANeighborList{ + // cutoffs: [2.0,3.0], + // bond_contribution: false, + // })) as Box); + + // let system = test_system("water"); + // let options = crate::calculators::tests_utils::FinalDifferenceOptions { + // displacement: 1e-6, + // max_relative: 1e-9, + // epsilon: 1e-16, + // }; + // crate::calculators::tests_utils::finite_differences_positions(calculator, &system, options); + + // // full neighbor list + // let calculator = Calculator::from(Box::new(BANeighborList::Full(FullBANeighborList{ + // cutoffs: [2.0,3.0], + // bond_contribution: false, + // })) as Box); + // crate::calculators::tests_utils::finite_differences_positions(calculator, &system, options); + // } + + use crate::calculators::soap::{Cutoff, Smoothing}; + use crate::calculators::shared::{Density, DensityKind, DensityScaling}; + use crate::calculators::shared::{SoapRadialBasis, SphericalExpansionBasis, TensorProductBasis}; + + #[test] + fn spherical_expansion() { + + let expected = [ + BTreeMap::from([ + (0, &[[0.16902879658926248, 0.028869505770363096, -0.012939303519269344]] as &[[f64;3]]), + (1, &[[0.0, 0.0, -0.0], + [0.26212372007374773, 0.04923860892292029, -0.02052369607421798], + [0.0, 0.0, -0.0]] as &[[f64;3]]), + (2, &[[0.0, 0.0, -0.0], + [0.0, 0.0, -0.0], + [0.2734914300150501, 0.05977378771423668, -0.022198889165475678], + [0.0, 0.0, -0.0], + [0.0, 0.0, -0.0]] as &[[f64;3]]), + ]), + BTreeMap::from([ + (0, &[[0.16902879658926248, 0.028869505770363096, -0.012939303519269344]] as &[[f64;3]]), + (1, &[[0.0, 0.0, -0.0], + [0.0, 0.0, -0.0], + [0.26212372007374773, 0.04923860892292029, -0.02052369607421798]] as &[[f64;3]]), + (2, &[[0.0, 0.0, -0.0], + [0.0, 0.0, -0.0], + [-0.13674571500752503, -0.029886893857118332, 0.011099444582737835], + [0.0, 0.0, -0.0], + [0.23685052611036728, 0.051765618640947135, -0.019224801953097073]] as &[[f64;3]]), + ]), + BTreeMap::from([ + (0, &[[0.055690489760816295, 0.06300370381466462, -0.002920081734154629]] as &[[f64;3]]), + (1, &[[0.0, 0.0, -0.0], + [0.06460583443346805, 0.07393339000508466, -0.003326135960304247], + [0.06460583443346805, 0.07393339000508466, -0.003326135960304247]] as &[[f64;3]]), + (2, &[[0.0, 0.0, -0.0], + [0.0, 0.0, -0.0], + [0.02647774981023968, 0.030983159900568692, -0.0013111835538098728], + [0.09172161588286466, 0.10732881425363133, -0.004542073066494842], + [0.04586080794143233, 0.053664407126815666, -0.002271036533247421]] as &[[f64;3]]), + ]), + BTreeMap::from([ + (0, &[[0.11139019922564429, 0.05256952008314618, -0.00976648821140304]] as &[[f64;3]]), + (1, &[[-0.09132548515017183, -0.044385580585388364, 0.008057500982983558], + [0.15220914191695306, 0.07397596764231394, -0.013429168304972592], + [0.015220914191695308, 0.007397596764231396, -0.00134291683049726]] as &[[f64;3]]), + (2, &[[-0.014919461593876988, -0.007651634517671902, 0.001329600535188854], + [-0.1491946159387699, -0.07651634517671901, 0.013296005351888539], + [0.11700350769036943, 0.06000672829237635, -0.010427180998805892], + [0.024865769323128322, 0.012752724196119839, -0.0022160008919814237], + [-0.04351509631547453, -0.022317267343209705, 0.0038780015609674885]] as &[[f64;3]]), + ]), + ]; + let expected = expected.into_iter().map(|btm| BTreeMap::from_iter( + btm.into_iter() + .map(|(k,v)|(k, ndarray::arr2(v))) + )); + + let vectors = [ + (0., 0., 1.), + (1., 0., 0.), + (1., 0., 1.), + (0.1,-0.6,1.), + ].into_iter() + .map(|(x,y,z)|Vector3D::new(x,y,z)) + .collect::>(); + + let parameters = RawSphericalExpansionParameters { + cutoff: Cutoff { + radius: 3.5, + smoothing: Smoothing::ShiftedCosine { width: 0.5 } + }, + density: Density { + kind: DensityKind::Gaussian { width: 0.3 }, + scaling: Some(DensityScaling::Willatt2018 { + scale: 1.5, + rate: 0.8, + exponent: 2.0 + }), + center_atom_weight: 1.0, + }, + basis: SphericalExpansionBasis::TensorProduct(TensorProductBasis { + max_angular: 2, + radial: SoapRadialBasis::Gto { max_radial: 2, radius: None }, + spline_accuracy: Some(1e-8), + }), + }; + + let expander = RawSphericalExpansion::new(parameters.clone()); + let mut contrib = expander.make_contribution_buffer(false); + + for (vector,expected) in vectors.into_iter().zip(expected) { + expander.compute_coefficients(&mut contrib, vector, 1., None); + for o3_lambda in parameters.basis.angular_channels() { + assert_relative_eq!( + contrib.values.get(&o3_lambda).unwrap(), + expected.get(&o3_lambda).unwrap(), + max_relative=1E-6 + ); + } + } + } +} diff --git a/featomic/src/calculators/bondatom/mod.rs b/featomic/src/calculators/bondatom/mod.rs new file mode 100644 index 000000000..d2dd12817 --- /dev/null +++ b/featomic/src/calculators/bondatom/mod.rs @@ -0,0 +1,41 @@ +pub mod spherical_expansion_bondcentered; + +mod bond_atom_math; +pub(crate) use bond_atom_math::canonical_vector_for_single_triplet; +use bond_atom_math::{RawSphericalExpansion,RawSphericalExpansionParameters,ExpansionContribution}; + +//pub use bondatom_neighbor_list::BANeighborList; +pub use spherical_expansion_bondcentered::{ + SphericalExpansionForBonds, + SphericalExpansionForBondsParameters, +}; + + + +const FEATURE_GATE: &'static str = "RASCALINE_EXPERIMENTAL_BOND_ATOM_SPX"; +fn get_feature_gate() -> bool { + use std::env; + if let Ok(var) = env::var(FEATURE_GATE) { + if var.len() == 0 { + false + } else { + let var = var.to_lowercase(); + !(&var=="0" || var == "false" || var == "no" || var == "off") + } + } else { + false + } +} +fn assert_feature_gate() { + if !get_feature_gate() { + if !get_feature_gate() { + unimplemented!("Bond-Atom spherical expansion requires UNSTABLE feature gate: {}", FEATURE_GATE); + } + } +} + +#[cfg(test)] +fn set_feature_gate() { + use std::env; + env::set_var(FEATURE_GATE, "true"); +} diff --git a/featomic/src/calculators/bondatom/spherical_expansion_bondcentered.rs b/featomic/src/calculators/bondatom/spherical_expansion_bondcentered.rs new file mode 100644 index 000000000..32c5f9e49 --- /dev/null +++ b/featomic/src/calculators/bondatom/spherical_expansion_bondcentered.rs @@ -0,0 +1,730 @@ +use std::collections::BTreeMap; +use std::collections::btree_map::Entry; +use std::cell::RefCell; + +use ndarray::s; +use rayon::prelude::*; + +use metatensor::{LabelsBuilder, Labels, LabelValue}; +use metatensor::TensorMap; + +use crate::{Error, System}; + +use crate::labels::{SamplesBuilder, AtomicTypeFilter, BondCenteredSamples}; +use crate::labels::{KeysBuilder, TwoCentersSingleNeighborsTypesKeys}; + +use crate::calculators::{CalculatorBase,GradientsOptions}; + +use crate::calculators::shared::SphericalExpansionBasis; + +use super::super::shared::descriptors_by_systems::{array_mut_for_system, split_tensor_map_by_system}; + + +use crate::systems::BATripletNeighborList; +use super::{canonical_vector_for_single_triplet,ExpansionContribution,RawSphericalExpansion,RawSphericalExpansionParameters}; + +use super::assert_feature_gate; + +/// Parameters for spherical expansion calculator for bond-centered neighbor densities. +/// +/// (The spherical expansion is at the core of representations in the SOAP +/// (Smooth Overlap of Atomic Positions) family. See [this review +/// article](https://doi.org/10.1063/1.5090481) for more information on the SOAP +/// representation, and [this paper](https://doi.org/10.1063/5.0044689) for +/// information on how it is implemented in rascaline.) +/// +/// This calculator is only needed to characterize local environments that are centered +/// on a pair of atoms rather than a single one. +#[derive(Debug, Clone)] +#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +pub struct SphericalExpansionForBondsParameters { + pub(crate) center_atoms_weight: f64, + /// Spherical cutoffs to use for atomic environments + pub(super) bond_cutoff_radius: f64, + pub raw_spherical_expansion: RawSphericalExpansionParameters, +} + +impl SphericalExpansionForBondsParameters { + /// Validate all the parameters + pub fn validate(&mut self) -> Result<(), Error> { + assert_feature_gate(); + self.raw_spherical_expansion.validate()?; + let third_cutoff = self.raw_spherical_expansion.cutoff.radius; + if self.bond_cutoff_radius < 0. { + return Err(Error::InvalidParameter("negative bond cutoff".into())); + } else if third_cutoff < 0.5*self.bond_cutoff_radius{ + return Err(Error::InvalidParameter( + "neighbor cutoff too small, some atom pairs would have incorrectly-described environments".into() + )); + } + return Ok(()); + } + + pub fn third_cutoff(&self) -> f64 { + self.raw_spherical_expansion.cutoff.radius + } + + // fn decompose(self) -> (RawSphericalExpansionParameters,f64){ + // let (bond_cutoff,center_atoms_weight) = (self.bond_cutoff,self.center_atoms_weight); + // ( + // RawSphericalExpansionParameters{ + // cutoff: self.third_cutoff(), + // max_radial: self.max_radial, + // max_angular: self.max_angular, + // atomic_gaussian_width: self.atomic_gaussian_width, + // radial_basis: self.radial_basis, + // cutoff_function: self.cutoff_function, + // radial_scaling: self.radial_scaling, + // }, + // bond_cutoff, + // center_atoms_weight, + // )} + // fn recompose(expansion_params: RawSphericalExpansionParameters, bond_cutoff: f64, center_atoms_weight: f64) -> Self { + // Self{ + // cutoffs: [bond_cutoff, expansion_params.cutoff], + // max_radial: expansion_params.max_radial, + // max_angular: expansion_params.max_angular, + // atomic_gaussian_width: expansion_params.atomic_gaussian_width, + // radial_basis: expansion_params.radial_basis, + // cutoff_function: expansion_params.cutoff_function, + // radial_scaling: expansion_params.radial_scaling, + // center_atoms_weight, + // } + // } + +} + + +/// The actual calculator used to compute SOAP-like spherical expansion coefficients for bond-centered environments +/// In other words, the spherical expansion of the neighbor density function centered on the center of a bond, +/// 'after' rotating the system so that the bond is aligned with the z axis. +/// +/// This radial+angular decomposition yields coefficients with labels `n` (radial), and `l` and `m` (angular) +/// as a Calculator, it yields tonsorblocks of with individual values of `l` +/// and individual atomic types for center_1, center_2, and neighbor. +/// Each block has components for each possible value of `m`, and properties for each value of `n`. +/// a given sample corresponds to a single center bond (a pair of center atoms) within a given structure. +pub struct SphericalExpansionForBonds { + /// The object in charge of computing the vectors and distances + /// between the bond and the lone atom of the BA triplet (after rotating the system to put the bond in it canonical orientation) + distance_calculator: BATripletNeighborList, + /// actual spherical expansion object + raw_expansion: RawSphericalExpansion, + /// a weight multiplier for expansion coefficients from self-contributions + center_atoms_weight: f64, +} + +impl SphericalExpansionForBonds { + /// Create a new `SphericalExpansion` calculator with the given parameters + pub fn new(mut parameters: SphericalExpansionForBondsParameters) -> Result { + parameters.validate()?; + let cutoffs = [parameters.bond_cutoff_radius, parameters.third_cutoff()]; + let exp_params = parameters.raw_spherical_expansion; + //let (exp_params, _bond_cut, center_weight) = parameters.decompose(); + + return Ok(Self { + center_atoms_weight: parameters.center_atoms_weight, + raw_expansion: RawSphericalExpansion::new(exp_params), + distance_calculator: BATripletNeighborList{ + cutoffs, + }, + }); + } + + + /// a smart-ish way to obtain the coefficients of all bond expansions: + /// this function's API is designed to be resource-efficient for both SphericalExpansionForBondType and + /// SphericalExpansionForBonds, while being computationally efficient for the underlying BANeighborList calculator. + pub(super) fn get_coefficients_for<'a>( + &'a self, system: &'a System, + s1: i32, s2: i32, s3_list: &'a Vec, + do_gradients: GradientsOptions, + ) -> Result>)> + 'a, Error> { + + let types = system.types().unwrap(); + + + let pre_iter = s3_list.iter().flat_map(|s3|{ + self.distance_calculator.get_per_system_per_type_enumerated(system,s1,s2,*s3).unwrap().into_iter() + }).flat_map(|(triplet_i,triplet)| { + let invert: &'static [bool] = { + if s1==s2 {&[false,true]} + else if types[triplet.atom_i] == s1 {&[false]} + else {&[true]} + }; + invert.iter().map(move |invert|(triplet_i,triplet,*invert)) + }).collect::>(); + + let contribution = std::rc::Rc::new(RefCell::new( + self.raw_expansion.make_contribution_buffer(do_gradients.any()) + )); + + let mut mtx_cache = BTreeMap::new(); + let mut dmtx_cache = BTreeMap::new(); + + return Ok(pre_iter.into_iter().map(move |(triplet_i,triplet,invert)| { + let vector = canonical_vector_for_single_triplet(&triplet, invert, false, &mut mtx_cache, &mut dmtx_cache).unwrap(); + let weight = if triplet.is_self_contrib {self.center_atoms_weight} else {1.0}; + self.raw_expansion.compute_coefficients(&mut *contribution.borrow_mut(), vector.vect,weight,None); + (triplet_i, invert, contribution.clone()) + })); + + } + +} + +impl CalculatorBase for SphericalExpansionForBonds { + fn name(&self) -> String { + "spherical expansion".into() + } + + fn cutoffs(&self) -> &[f64] { + &self.distance_calculator.cutoffs + } + + fn parameters(&self) -> String { + let params = SphericalExpansionForBondsParameters{ + bond_cutoff_radius: self.distance_calculator.bond_cutoff(), + center_atoms_weight: self.center_atoms_weight, + raw_spherical_expansion: self.raw_expansion.parameters().clone(), + }; + serde_json::to_string(¶ms).expect("failed to serialize to JSON") + } + + fn keys(&self, systems: &mut [System]) -> Result { + let builder = TwoCentersSingleNeighborsTypesKeys { + cutoffs: self.distance_calculator.cutoffs, + self_contributions: true, + raw_triplets: &self.distance_calculator, + }; + let keys = builder.keys(systems)?; + + let mut builder = LabelsBuilder::new(vec!["o3_lambda", "center_1_type", "center_2_type", "neighbor_type"]); + for &[center_1_type, center_2_type, neighbor_type] in keys.iter_fixed_size() { + for o3_lambda in self.raw_expansion.parameters().basis.angular_channels() { + builder.add(&[o3_lambda.into(), center_1_type, center_2_type, neighbor_type]); + } + } + + return Ok(builder.finish()); + } + + fn sample_names(&self) -> Vec<&str> { + BondCenteredSamples::sample_names() + } + + fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error> { + assert_eq!(keys.names(), ["o3_lambda", "center_1_type", "center_2_type", "neighbor_type"]); + + // only compute the samples once for each `atom_type, neighbor_type`, + // and re-use the results across `o3_lambda`. + let mut samples_per_type = BTreeMap::new(); + for [_, center_1_type, center_2_type, neighbor_type] in keys.iter_fixed_size() { + if samples_per_type.contains_key(&(center_1_type, center_2_type, neighbor_type)) { + continue; + } + + let builder = BondCenteredSamples { + cutoffs: self.distance_calculator.cutoffs, + center_1_type: AtomicTypeFilter::Single(center_1_type.i32()), + center_2_type: AtomicTypeFilter::Single(center_2_type.i32()), + neighbor_type: AtomicTypeFilter::Single(neighbor_type.i32()), + self_contributions: true, + raw_triplets: &self.distance_calculator, + }; + + samples_per_type.insert((center_1_type, center_2_type, neighbor_type), builder.samples(systems)?); + } + + let mut result = Vec::new(); + for [_, center_1_type, center_2_type, neighbor_type] in keys.iter_fixed_size() { + let samples = samples_per_type.get( + &(center_1_type, center_2_type, neighbor_type) + ).expect("missing samples"); + + result.push(samples.clone()); + } + + return Ok(result); + } + + fn supports_gradient(&self, _parameter: &str) -> bool { + false // for now, discontinuities are a pain + } + + fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [System]) -> Result, Error> { + assert_eq!(keys.names(), ["o3_lambda", "center_1_type", "center_2_type", "neighbor_type"]); + assert_eq!(keys.count(), samples.len()); + + let mut gradient_samples = Vec::new(); + for ([_, center_1_type, center_2_type, neighbor_type], samples) in keys.iter_fixed_size().zip(samples) { + // TODO: we don't need to rebuild the gradient samples for different + // o3_lambda + let builder = BondCenteredSamples { + cutoffs: self.distance_calculator.cutoffs, + center_1_type: AtomicTypeFilter::Single(center_1_type.i32()), + center_2_type: AtomicTypeFilter::Single(center_2_type.i32()), + neighbor_type: AtomicTypeFilter::Single(neighbor_type.i32()), + self_contributions: true, + raw_triplets: &self.distance_calculator, + }; + + gradient_samples.push(builder.gradients_for(systems, samples)?); + } + + return Ok(gradient_samples); + } + + fn components(&self, keys: &Labels) -> Vec> { + assert_eq!(keys.names(), ["o3_lambda", "center_1_type", "center_2_type", "neighbor_type"]); + + // only compute the components once for each `o3_lambda`, + // and re-use the results across `atom_type, neighbor_type`. + let mut component_by_l = BTreeMap::new(); + for [o3_lambda, _, _, _] in keys.iter_fixed_size() { + if component_by_l.contains_key(o3_lambda) { + continue; + } + + let mut component = LabelsBuilder::new(vec!["spherical_harmonics_m"]); + for m in -o3_lambda.i32()..=o3_lambda.i32() { + component.add(&[LabelValue::new(m)]); + } + + let components = vec![component.finish()]; + component_by_l.insert(*o3_lambda, components); + } + + let mut result = Vec::new(); + for [o3_lambda, _, _, _] in keys.iter_fixed_size() { + let components = component_by_l.get(o3_lambda).expect("missing samples"); + result.push(components.clone()); + } + return result; + } + + fn property_names(&self) -> Vec<&str> { + vec!["n"] + } + + fn properties(&self, keys: &Labels) -> Vec { + let properties = LabelsBuilder::new(self.property_names()); + let o3_col_i = keys.names().iter().position(|&name|name=="o3_lambda").unwrap(); + let params = self.raw_expansion.parameters(); + + return keys.iter().map(|label|{ + let o3_lambda = label[o3_col_i].usize(); + let radial_basis_size = match params.basis { + SphericalExpansionBasis::TensorProduct(ref basis) => basis.radial.size(), + SphericalExpansionBasis::Explicit(ref basis) => { + basis.by_angular.get(&o3_lambda).expect("missing o3_lambda").size() + }, + }; + let mut properties = properties.clone(); + for n in 0..radial_basis_size { + properties.add(&[n]); + } + properties.finish() + }).collect(); + } + + #[time_graph::instrument(name = "SphericalExpansion::compute")] + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { + assert_feature_gate(); + assert_eq!(descriptor.keys().names(), ["o3_lambda", "center_1_type", "center_2_type", "neighbor_type"]); + if descriptor.blocks().len() == 0 { + return Ok(()); + } + + //let max_angular = self.raw_expansion.parameters().max_angular; + let angular_channels = self.raw_expansion.parameters().basis.angular_channels(); + // let l_slices: Vec<_> = angular_channels.map(|l|{ + // let lsize = l*l; + // let msize = 2*l+1; + // lsize..lsize+msize + // }).collect(); + + let do_gradients = GradientsOptions { + positions: descriptor.block_by_id(0).gradient("positions").is_some(), + cell: descriptor.block_by_id(0).gradient("cell").is_some(), + strain: descriptor.block_by_id(0).gradient("strain").is_some(), + }; + if do_gradients.positions { + assert!(self.supports_gradient("positions")); + } + if do_gradients.cell { + assert!(self.supports_gradient("cell")); + } + + let radial_selection = descriptor.blocks().iter().map(|b|{ + let prop = b.properties(); + assert_eq!(prop.names(), ["n"]); + prop.iter_fixed_size().map(|&[n]|n.i32()).collect::>() + }).collect::>(); + // first, create some partial-key -> block lookup tables to avoid linear searches within blocks later + + // {(s1,s2,s3) -> i_s3} + let mut s1s2s3_to_is3: BTreeMap<(i32,i32,i32),usize> = BTreeMap::new(); + // {(s1,s2) -> [i_s3->(s3,[l->i_block])]} + let mut s1s2_to_block_ids: BTreeMap<(i32,i32),Vec<(i32,Vec)>> = BTreeMap::new(); + + for (block_i, &[l, s1,s2,s3]) in descriptor.keys().iter_fixed_size().enumerate(){ + let s1=s1.i32(); + let s2=s2.i32(); + let s3=s3.i32(); + let l=l.usize(); + let s1s2_blocks = s1s2_to_block_ids.entry((s1,s2)) + .or_insert_with(Vec::new); + let l_blocks = match s1s2s3_to_is3.entry((s1,s2,s3)) { + Entry::Occupied(i_s3_e) => { + let (s3_b, l_blocks) = & mut s1s2_blocks[*i_s3_e.get()]; + debug_assert_eq!(s3_b,&s3); + l_blocks + }, + Entry::Vacant(i_s3_e) => { + let i_s3 = s1s2_blocks.len(); + i_s3_e.insert(i_s3); + s1s2_blocks.push((s3,vec![usize::MAX; angular_channels.len()])); + &mut s1s2_blocks[i_s3].1 + }, + }; + l_blocks[l] = block_i; + } + + #[cfg(debug_assertions)]{ + for block in descriptor.blocks() { + assert_eq!(block.samples().names(), ["system", "first_atom", "second_atom", "cell_shift_a", "cell_shift_b", "cell_shift_c"]); + } + } + let mut descriptors_by_system = split_tensor_map_by_system(descriptor, systems.len()); + + systems.par_iter_mut() + .zip_eq(&mut descriptors_by_system) + .try_for_each(|(system, descriptor)| + { + //system.compute_triplet_neighbors(self.parameters.bond_cutoff(), self.parameters.third_cutoff())?; + self.distance_calculator.ensure_computed_for_system(system)?; + let triplets = self.distance_calculator.get_for_system(system)?; + let types = system.types()?; + + for ((s1,s2),s1s2_blocks) in s1s2_to_block_ids.iter() { + let (s3_list,per_s3_blocks): (Vec,Vec<&Vec<_>>) = s1s2_blocks.iter().map( + |(s3,blocks)|(*s3,blocks) + ).unzip(); + // half-assume that blocks that share s1,s2,s3 have the same sample list + #[cfg(debug_assertions)]{ + for (_s3,s3blocks) in s1s2_blocks.iter(){ + debug_assert!(s3blocks.len()>0); + let mut s3goodblocks = s3blocks.iter().filter(|b_i|(**b_i)!=usize::MAX); + let first_goodblock = s3goodblocks.next(); + debug_assert!(first_goodblock.is_some()); + + let samples_n = descriptor.block_by_id(*first_goodblock.unwrap()).samples().size(); + for lblock in s3goodblocks { + debug_assert_eq!(descriptor.block_by_id(*lblock).samples().size(), samples_n); + } + } + } + // {bond_i->(i_s3,sample_i)} + let mut s3_samples = vec![]; + let mut sample_lut: BTreeMap<(usize,usize,[i32;3]),Vec<(usize,usize)>> = BTreeMap::new(); + + // also assume that the systems are in order in the samples + for (i_s3, s3blocks) in per_s3_blocks.into_iter().enumerate() { + let first_good_block = s3blocks.iter().filter(|b_i|**b_i!=usize::MAX).next().unwrap(); + let samples = descriptor.block_by_id(*first_good_block).samples(); + for (sample_i, &[_system_i,atom_i,atom_j,cell_shift_a, cell_shift_b, cell_shift_c]) in samples.iter_fixed_size().enumerate(){ + match sample_lut.entry( + (atom_i.usize(),atom_j.usize(),[cell_shift_a.i32(),cell_shift_b.i32(),cell_shift_c.i32()]) + ) { + Entry::Vacant(e) => { + e.insert(vec![(i_s3,sample_i)]); + }, + Entry::Occupied(mut e) => { + e.get_mut().push((i_s3,sample_i)); + }, + } + } + s3_samples.push(samples); + } + for (triplet_i,inverted,contribution) in self.get_coefficients_for(system, *s1, *s2, &s3_list, do_gradients)? { + let triplet = &triplets[triplet_i]; + + let contribution = contribution.borrow(); + let these_samples = match sample_lut.get( + &(triplet.atom_i,triplet.atom_j,triplet.bond_cell_shift) + ){ + None => {continue;}, + Some(a) => a, + }; + + for (i_s3,sample_i) in these_samples.iter(){ + if s3_list[*i_s3] != types[triplet.atom_k] { + continue // this triplet does not contribute to this block + } + let sample = &s3_samples[*i_s3][*sample_i]; + let (atom_i,atom_j, ce_sh) = (sample[1].usize(),sample[2].usize(),[sample[3].i32(),sample[4].i32(),sample[5].i32()]); + if (!inverted) && ( + triplet.atom_i != atom_i || triplet.atom_j != atom_j + || triplet.bond_cell_shift != ce_sh + ){ + continue; + } else if inverted && ( + triplet.atom_i != atom_j || triplet.atom_j != atom_i + || triplet.bond_cell_shift != ce_sh.map(|x|-x) + ){ + continue; + } + + let ret_blocks = &s1s2_blocks[*i_s3].1; + for &l in angular_channels.iter() { + let block_i = ret_blocks[l]; + if block_i == usize::MAX { + continue; + } + let mut block = descriptor.block_mut_by_id(block_i); + let mut array = array_mut_for_system(block.values_mut()); + let mut value_slice = array.slice_mut(s![*sample_i,..,..]); + let input_slice = contribution.values.get(&l).expect("missing o3_lambda here"); // TODO + for (n_i,n) in radial_selection[block_i].iter().enumerate() { + let mut value_slice = value_slice.slice_mut(s![..,n_i]); + value_slice += &input_slice.slice(s![..,*n]); + } + + } + } + } + } + Ok::<_, Error>(()) + })?; + + Ok(()) + } +} + + +#[cfg(test)] +mod tests { + use ndarray::ArrayD; + use metatensor::{Labels, TensorBlock, EmptyArray, LabelsBuilder, TensorMap}; + + use crate::calculators::bondatom::set_feature_gate; + use crate::systems::test_utils::test_systems; + use crate::{Calculator, CalculationOptions, LabelsSelection}; + use crate::calculators::CalculatorBase; + + use crate::calculators::soap::{Cutoff, Smoothing}; + use crate::calculators::shared::{ + Density, DensityKind, DensityScaling, + SoapRadialBasis, SphericalExpansionBasis, TensorProductBasis + }; + use super::super::bond_atom_math::RawSphericalExpansionParameters; + use super::{SphericalExpansionForBonds, SphericalExpansionForBondsParameters}; + + + fn parameters() -> SphericalExpansionForBondsParameters { + set_feature_gate(); + SphericalExpansionForBondsParameters { + bond_cutoff_radius: 3.5, + center_atoms_weight: 10.0, + raw_spherical_expansion: RawSphericalExpansionParameters { + cutoff: Cutoff { + radius: 3.5, + smoothing: Smoothing::ShiftedCosine { width: 0.5 } + }, + density: Density { + kind: DensityKind::Gaussian { width: 0.3 }, + scaling: Some(DensityScaling::Willatt2018 { + scale: 1.5, + rate: 0.8, + exponent: 2.0 + }), + center_atom_weight: 1.0, + }, + basis: SphericalExpansionBasis::TensorProduct(TensorProductBasis { + max_angular: 6, + radial: SoapRadialBasis::Gto { max_radial: 5, radius: None }, + spline_accuracy: Some(1e-8), + }), + }, + } + } + + #[test] + fn values() { + let mut calculator = Calculator::from(Box::new(SphericalExpansionForBonds::new( + parameters() + ).unwrap()) as Box); + + let mut systems = test_systems(&["water"]); + let descriptor = calculator.compute(&mut systems, Default::default()).unwrap(); + + for l in 0..6 { + for center_1_type in [1, -42] { + for center_2_type in [1, -42] { + if center_1_type==-42 && center_2_type==-42 { + continue; + } + for neighbor_type in [1, -42] { + let block_i = descriptor.keys().position(&[ + l.into(), center_1_type.into(), center_2_type.into(), neighbor_type.into() + ]); + assert!(block_i.is_some()); + let block = &descriptor.block_by_id(block_i.unwrap()); + let array = block.values().to_array(); + assert_eq!(array.shape().len(), 3); + assert_eq!(array.shape()[1], 2 * l + 1); + } + } + } + } + + // exact values for spherical expansion are regression-tested in + // `rascaline/tests/spherical-expansion.rs` + } + + #[test] + fn compute_partial() { + let mut parameters = parameters(); + parameters.raw_spherical_expansion.basis = match parameters.raw_spherical_expansion.basis{ + SphericalExpansionBasis::TensorProduct(basis) => + SphericalExpansionBasis::TensorProduct(TensorProductBasis{max_angular: 2, ..basis}), + _ => unreachable!(), + }; + let calculator = Calculator::from(Box::new(SphericalExpansionForBonds::new( + parameters + ).unwrap()) as Box); + + let mut systems = test_systems(&["water"]); + + let properties = Labels::new(["n"], &[ + [0], + [3], + [2], + ]); + + let samples = Labels::new(["system", "first_atom", "second_atom", "cell_shift_a","cell_shift_b","cell_shift_c"], &[ + [0, 0, 2, 0,0,0], + [0, 0, 1, 0,0,0], + //[0, 1, 2, 0,0,0], // excluding this one + ]); + + let keys = Labels::new(["o3_lambda", "center_1_type", "center_2_type", "neighbor_type"], &[ + // every key that will be generated (in scrambled order) plus one + [0, -42, 1, -42], + [0, -42, -42, -42], + [2, -42, 1, -42], + [0, 1, -42, -42], + [0, 1, 1, -42], + [0, 6, 1, 1], // not part of the default keys + [1, -42, 1, -42], + [1, -42, 1, 1], + [2, -42, -42, -42], + [1, 1, -42, 1], + [0, -42, 1, 1], + [1, 1, 1, -42], + [2, -42, 1, 1], + [0, 1, 1, 1], + [2, 1, -42, -42], + [1, 1, 1, 1], + [0, -42, -42, 1], + [1, -42, -42, 1], + [2, -42, -42, 1], + [2, 1, 1, -42], + [0, 1, -42, 1], + [2, 1, -42, 1], + [2, 1, 1, 1], + [1, -42, -42, -42], + [1, 1, -42, -42], + ]); + + crate::calculators::tests_utils::compute_partial( + calculator, &mut systems, &keys, &samples, &properties + ); + } + + #[test] + fn non_existing_samples() { + let parameters = parameters(); + // stride between blocks with the same angular channel + let angular_stride = parameters.raw_spherical_expansion.basis.angular_channels().len(); + let mut calculator = Calculator::from(Box::new(SphericalExpansionForBonds::new( + parameters.clone() + ).unwrap()) as Box); + + let mut systems = test_systems(&["water"]); + + // include the three atoms in all blocks, regardless of the + // atom_type key. + let block = TensorBlock::new( + EmptyArray::new(vec![3, 1]), + &Labels::new(["system", "first_atom", "second_atom", "cell_shift_a","cell_shift_b","cell_shift_c"], &[ + [0, 0, 2, 0,0,0], + [0, 1, 2, 0,0,0], + [0, 0, 1, 0,0,0], + ]), + &[], + &Labels::single(), + ).unwrap(); + + let mut keys = LabelsBuilder::new(vec!["o3_lambda", "center_1_type", "center_2_type", "neighbor_type"]); + let mut blocks = Vec::new(); + for l in parameters.raw_spherical_expansion.basis.angular_channels() { + let l = l as isize; + for center_1_type in [1, -42] { + for center_2_type in [1, -42] { + for neighbor_type in [1, -42] { + keys.add(&[l, center_1_type, center_2_type, neighbor_type]); + blocks.push(block.as_ref().try_clone().unwrap()); + } + } + } + } + let select_all_samples = TensorMap::new(keys.finish(), blocks).unwrap(); + + let options = CalculationOptions { + selected_samples: LabelsSelection::Predefined(&select_all_samples), + ..Default::default() + }; + let descriptor = calculator.compute(&mut systems, options).unwrap(); + + // get the block for oxygen + // println!("{:?}", descriptor.keys()); + assert_eq!(descriptor.keys().names(), ["o3_lambda", "center_1_type", "center_2_type", "neighbor_type"]); + assert_eq!(descriptor.keys()[2*angular_stride], [0, -42, 1, -42]); // start with [n, -42, -42, -42], then [n, -42, -42, 1] + + let block = descriptor.block_by_id(2*angular_stride); + let block = block.data(); + + // entries centered on H atoms should be zero + assert_eq!( + *block.samples, + Labels::new(["system", "first_atom", "second_atom", "cell_shift_a","cell_shift_b","cell_shift_c"], &[ + [0, 0, 2, 0,0,0], + [0, 1, 2, 0,0,0], // the sample that doesn't exist + [0, 0, 1, 0,0,0], + ]) + ); + let array = block.values.as_array(); + assert_eq!(array.index_axis(ndarray::Axis(0), 1), ArrayD::from_elem(vec![1, 6], 0.0)); + + // get the block for hydrogen + assert_eq!(descriptor.keys().names(), ["o3_lambda", "center_1_type", "center_2_type", "neighbor_type"]); + assert_eq!(descriptor.keys()[5*angular_stride], [0, 1, -42, 1]); + + let block = descriptor.block_by_id(5*angular_stride); + let block = block.data(); + + // entries centered on O atoms should be zero + assert_eq!( + *block.samples, + Labels::new(["system", "first_atom", "second_atom", "cell_shift_a","cell_shift_b","cell_shift_c"], &[ + [0, 0, 2, 0,0,0], + [0, 1, 2, 0,0,0], + [0, 0, 1, 0,0,0], + ]) + ); + let array = block.values.as_array(); + assert_eq!(array.index_axis(ndarray::Axis(0), 0), ArrayD::from_elem(vec![1, 6], 0.0)); + } +} diff --git a/rascaline/src/calculators/dummy_calculator.rs b/featomic/src/calculators/dummy_calculator.rs similarity index 96% rename from rascaline/src/calculators/dummy_calculator.rs rename to featomic/src/calculators/dummy_calculator.rs index 89644e1f0..e9aa94738 100644 --- a/rascaline/src/calculators/dummy_calculator.rs +++ b/featomic/src/calculators/dummy_calculator.rs @@ -43,7 +43,7 @@ impl CalculatorBase for DummyCalculator { std::slice::from_ref(&self.cutoff) } - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { return CenterTypesKeys.keys(systems); } @@ -51,7 +51,7 @@ impl CalculatorBase for DummyCalculator { AtomCenteredSamples::sample_names() } - fn samples(&self, keys: &Labels, systems: &mut [Box]) -> Result, Error> { + fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error> { assert_eq!(keys.names(), ["center_type"]); let mut samples = Vec::new(); for [center_type] in keys.iter_fixed_size() { @@ -75,7 +75,7 @@ impl CalculatorBase for DummyCalculator { } } - fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [Box]) -> Result, Error> { + fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [System]) -> Result, Error> { debug_assert_eq!(keys.count(), samples.len()); let mut gradient_samples = Vec::new(); for ([center_type], samples) in keys.iter_fixed_size().zip(samples) { @@ -110,7 +110,7 @@ impl CalculatorBase for DummyCalculator { } #[time_graph::instrument(name = "DummyCalculator::compute")] - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { if self.name.contains("log-test-info:") { info!("{}", self.name); } else if self.name.contains("log-test-warn:") { diff --git a/featomic/src/calculators/lode/mod.rs b/featomic/src/calculators/lode/mod.rs new file mode 100644 index 000000000..76961d619 --- /dev/null +++ b/featomic/src/calculators/lode/mod.rs @@ -0,0 +1,8 @@ +mod radial_integral; +pub use self::radial_integral::LodeRadialIntegral; +pub use self::radial_integral::LodeRadialIntegralGto; +pub use self::radial_integral::LodeRadialIntegralSpline; + + +mod spherical_expansion; +pub use self::spherical_expansion::{LodeSphericalExpansion, LodeSphericalExpansionParameters}; diff --git a/featomic/src/calculators/lode/radial_integral/gto.rs b/featomic/src/calculators/lode/radial_integral/gto.rs new file mode 100644 index 000000000..c8ee49577 --- /dev/null +++ b/featomic/src/calculators/lode/radial_integral/gto.rs @@ -0,0 +1,288 @@ +use std::f64; + +use ndarray::{Array2, Array1, ArrayViewMut1}; + +use crate::calculators::shared::basis::radial::GtoRadialBasis; +use crate::calculators::shared::{DensityKind, LodeRadialBasis}; +use crate::math::{hyp2f1, hyp1f1, gamma}; +use crate::Error; + +use super::LodeRadialIntegral; + +/// Implementation of the LODE radial integral for GTO radial basis and Gaussian +/// atomic density. +#[derive(Debug, Clone)] +pub struct LodeRadialIntegralGto { + /// Which value of l/lambda is this radial integral for + o3_lambda: usize, + /// `1/2σ_n^2`, with `σ_n` the GTO gaussian width, i.e. `cutoff * max(√n, 1) + /// / n_max` + gto_gaussian_widths: Vec, + /// `n_max * n_max` matrix to orthonormalize the GTO + gto_orthonormalization: Array2, +} + +impl LodeRadialIntegralGto { + /// Create a new LODE radial integral + pub fn new(basis: &LodeRadialBasis, o3_lambda: usize) -> Result { + let (&max_radial, >o_radius) = if let LodeRadialBasis::Gto { max_radial, radius } = basis { + (max_radial, radius) + } else { + return Err(Error::Internal("radial basis must be GTO for the GTO radial integral".into())); + }; + + if gto_radius < 1e-16 { + return Err(Error::InvalidParameter( + "radius of GTO radial basis can not be negative".into() + )); + } else if !gto_radius.is_finite() { + return Err(Error::InvalidParameter( + "radius of GTO radial basis can not be infinite/NaN".into() + )); + } + + let basis = GtoRadialBasis { + size: max_radial + 1, + radius: gto_radius, + }; + let gto_gaussian_widths = basis.gaussian_widths(); + let gto_orthonormalization = basis.orthonormalization_matrix(); + + return Ok(LodeRadialIntegralGto { + o3_lambda: o3_lambda, + gto_gaussian_widths: gto_gaussian_widths, + gto_orthonormalization: gto_orthonormalization.t().to_owned(), + }) + } +} + +impl LodeRadialIntegral for LodeRadialIntegralGto { + fn size(&self) -> usize { + self.gto_gaussian_widths.len() + } + + #[time_graph::instrument(name = "LodeRadialIntegralGto::compute")] + fn compute( + &self, + k_norm: f64, + mut values: ArrayViewMut1, + mut gradients: Option> + ) { + assert_eq!( + values.shape(), [self.size()], + "wrong size for values array, expected [{}] but got [{}]", + self.size(), values.shape()[0] + ); + + if let Some(ref gradients) = gradients { + assert_eq!( + gradients.shape(), [self.size()], + "wrong size for gradients array, expected [{}] but got [{}]", + self.size(), gradients.shape()[0] + ); + } + + let global_factor = std::f64::consts::PI.sqrt() / std::f64::consts::SQRT_2; + + for n in 0..self.size() { + let sigma_n = self.gto_gaussian_widths[n]; + let k_sigma_n_sqrt2 = k_norm * sigma_n / std::f64::consts::SQRT_2; + // `global_factor * sqrt(2)^{n} * sigma_n^{n + 3} * (k * sigma_n / sqrt(2))^l` + let factor = global_factor + * sigma_n.powi(n as i32 + 3) * std::f64::consts::SQRT_2.powi(n as i32) + * k_sigma_n_sqrt2.powi(self.o3_lambda as i32); + + let k_norm_sigma_n_2 = - k_norm * sigma_n * sigma_n; + let z = 0.5 * k_norm * k_norm_sigma_n_2; + + double_regularized_1f1( + self.o3_lambda, n, z, &mut values[n], gradients.as_mut().map(|g| &mut g[n]) + ); + + assert!(values[n].is_finite()); + values[n] *= factor; + if let Some(ref mut gradients) = gradients { + gradients[n] *= k_norm_sigma_n_2 * factor; + gradients[n] += self.o3_lambda as f64 / k_norm * values[n]; + } + } + + // for k_norm = 0, the formula used in the calculations above yield NaN, + // which in turns breaks the SplinedGto radial integral. From the + // analytical formula, the gradient is 0 everywhere expect for l=1 + if k_norm == 0.0 { + if let Some(ref mut gradients) = gradients { + gradients.fill(0.0); + + if self.o3_lambda == 1 { + for n in 0..self.size() { + let sigma_n = self.gto_gaussian_widths[n]; + let a = 0.5 * (n + self.o3_lambda) as f64 + 1.5; + let b = 2.5; + let factor = global_factor * sigma_n.powi((n + self.o3_lambda) as i32 + 3) * std::f64::consts::SQRT_2.powi(n as i32 - self.o3_lambda as i32); + + gradients[n] = gamma(a) / gamma(b) * factor; + } + } + } + } + + values.assign(&values.dot(&self.gto_orthonormalization)); + if let Some(ref mut gradients) = gradients { + gradients.assign(&gradients.dot(&self.gto_orthonormalization)); + } + } + + fn get_center_contribution(&self, density: DensityKind) -> Result, Error> { + let radial_size = self.gto_gaussian_widths.len(); + + let (smearing, exponent) = match density { + DensityKind::SmearedPowerLaw { smearing, exponent } => { + (smearing, exponent as f64) + } + _ => { + return Err(Error::InvalidParameter( + "Only 'SmearedPowerLaw' density is supported in LODE".into() + )); + } + }; + + let mut contrib = Array1::from_elem(radial_size, 0.0); + + + let n_eff: Vec = (0..radial_size) + .map(|n| 0.5 * (3.0 + n as f64)) + .collect(); + + if exponent == 0.0 { + let factor = std::f64::consts::PI.powf(-0.25) / (smearing * smearing).powf(0.75); + + for n in 0..radial_size { + let alpha = 0.5 * (1.0 / (smearing * smearing) + + 1.0 / (self.gto_gaussian_widths[n] * self.gto_gaussian_widths[n])); + contrib[n] = factor * gamma(n_eff[n]) / alpha.powf(n_eff[n]); + } + } else { + let factor = 2.0 * f64::sqrt(4.0 * std::f64::consts::PI) + / gamma(exponent / 2.0) + / exponent; + + for n in 0..radial_size { + let s = smearing / self.gto_gaussian_widths[n]; + let hyparg = 1.0 / (1.0 + s * s); + + contrib[n] = factor + * f64::powf(2.0, (1.0 + n as f64 - exponent) / 2.0) + * smearing.powi(3 + n as i32 - exponent as i32) + * gamma(n_eff[n]) + * hyp2f1(1.0, n_eff[n], (exponent + 2.0) / 2.0, hyparg) + * hyparg.powf(n_eff[n]); + } + } + + return Ok(contrib.dot(&self.gto_orthonormalization)); + } +} + +#[inline] +fn hyp1f1_derivative(a: f64, b: f64, x: f64) -> f64 { + a / b * hyp1f1(a + 1.0, b + 1.0, x) +} + +#[inline] +#[allow(clippy::many_single_char_names)] +/// Compute `G(a, b, z) = Gamma(a) / Gamma(b) 1F1(a, b, z)` for +/// `a = 1/2 (n + l + 3)` and `b = l + 3/2`. +/// +/// This is similar (but not the exact same) to the G function defined in +/// appendix A in . +/// +/// The function is called "double regularized 1F1" by reference to the +/// "regularized 1F1" function (i.e. `1F1(a, b, z) / Gamma(b)`) +fn double_regularized_1f1(l: usize, n: usize, z: f64, value: &mut f64, gradient: Option<&mut f64>) { + let (a, b) = (0.5 * (n + l + 3) as f64, l as f64 + 1.5); + let ratio = gamma(a) / gamma(b); + + *value = ratio * hyp1f1(a, b, z); + if let Some(gradient) = gradient { + *gradient = ratio * hyp1f1_derivative(a, b, z); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use approx::assert_relative_eq; + + #[test] + fn gradients_near_zero() { + let radial_size = 8; + for o3_lambda in [0, 1, 3, 5, 8] { + let gto = LodeRadialIntegralGto::new( + &LodeRadialBasis::Gto { max_radial: (radial_size - 1), radius: 5.0 }, + o3_lambda + ).unwrap(); + + let mut values = Array1::from_elem(radial_size, 0.0); + let mut gradients = Array1::from_elem(radial_size, 0.0); + let mut gradients_plus = Array1::from_elem(radial_size, 0.0); + gto.compute(0.0, values.view_mut(), Some(gradients.view_mut())); + gto.compute(1e-12, values.view_mut(), Some(gradients_plus.view_mut())); + + assert_relative_eq!( + gradients, gradients_plus, epsilon=1e-9, max_relative=1e-6, + ); + } + } + + #[test] + fn finite_differences() { + let k = 3.4; + let delta = 1e-6; + + let radial_size = 8; + + for o3_lambda in [0, 1, 3, 5, 8] { + let gto = LodeRadialIntegralGto::new( + &LodeRadialBasis::Gto { max_radial: (radial_size - 1), radius: 5.0 }, + o3_lambda + ).unwrap(); + + let mut values = Array1::from_elem(radial_size, 0.0); + let mut values_delta = Array1::from_elem(radial_size, 0.0); + let mut gradients = Array1::from_elem(radial_size, 0.0); + gto.compute(k, values.view_mut(), Some(gradients.view_mut())); + gto.compute(k + delta, values_delta.view_mut(), None); + + let finite_differences = (&values_delta - &values) / delta; + + assert_relative_eq!( + finite_differences, gradients, max_relative=1e-4 + ); + } + } + + #[test] + fn central_atom_contribution() { + let potential_exponents = [0, 1, 2, 6]; + + // Reference values taken from pyLODE + let reference_vals = [ + [7.09990773e-01, 6.13767550e-01, 3.34161655e-01, 8.35301652e-02, 1.78439072e-02, -3.44944648e-05], + [1.69193719, 2.02389574, 2.85086136, 3.84013091, 1.62869125, 7.03338899], + [1.00532822, 1.10024472, 1.34843326, 1.19816598, 0.69150744, 1.2765415], + [0.03811939, 0.03741200, 0.03115835, 0.01364843, 0.00534184, 0.00205973]]; + + for (i, &p) in potential_exponents.iter().enumerate() { + let gto = LodeRadialIntegralGto::new( + &LodeRadialBasis::Gto { max_radial: 5, radius: 5.0 }, 0 + ).unwrap(); + + let density = DensityKind::SmearedPowerLaw { smearing: 1.0, exponent: p }; + + let center_contrib = gto.get_center_contribution(density).unwrap(); + assert_relative_eq!(center_contrib, ndarray::arr1(&reference_vals[i]), max_relative=3e-6); + }; + } +} diff --git a/featomic/src/calculators/lode/radial_integral/mod.rs b/featomic/src/calculators/lode/radial_integral/mod.rs new file mode 100644 index 000000000..dc4d1ea5c --- /dev/null +++ b/featomic/src/calculators/lode/radial_integral/mod.rs @@ -0,0 +1,180 @@ +use std::collections::BTreeMap; + +use ndarray::{Array1, ArrayViewMut1}; + +use crate::Error; +use crate::calculators::shared::{DensityKind, LodeRadialBasis, SphericalExpansionBasis}; + +/// A `LodeRadialIntegral` computes the LODE radial integral for all radial +/// basis functions and a single spherical harmonic `l` channel +/// +/// See equations 5 to 8 of [this paper](https://doi.org/10.1063/5.0044689) for +/// mor information on the radial integral. +/// +/// `std::panic::RefUnwindSafe` is a required super-trait to enable passing +/// radial integrals across the C API. `Send` is a required super-trait to +/// enable passing radial integrals between threads. +pub trait LodeRadialIntegral: std::panic::RefUnwindSafe + Send { + /// Compute the LODE radial integral for a single k-vector `norm` and store + /// the resulting data in the `values` array. If `gradients` is `Some`, also + /// compute and store gradients there. + fn compute(&self, k_norm: f64, values: ArrayViewMut1, gradients: Option>); + + /// Get how many basis functions are part of this integral. This is the + /// shape to use for the `values` and `gradients` parameters to `compute`. + fn size(&self) -> usize; + + /// Compute the contribution of the central atom to the final `` + /// coefficients. By symmetry, only l=0 is non-zero, so this function + /// returns a 1-D array containing the different `` coefficients. + /// + /// This function differs from the rest of LODE calculation because it goes + /// straight from atom => n l m, without using k-space projection in the + /// middle. + fn get_center_contribution(&self, density: DensityKind) -> Result, Error>; +} + +mod gto; +pub use self::gto::LodeRadialIntegralGto; + +mod spline; +pub use self::spline::LodeRadialIntegralSpline; + +/// Store together a Radial integral implementation and cached allocation for +/// values/gradients. +pub struct LodeRadialIntegralCache { + /// Implementations of the radial integral + implementation: Box, + /// Cache for the radial integral values + pub(crate) values: Array1, + /// Cache for the radial integral gradient + pub(crate) gradients: Array1, +} + +impl LodeRadialIntegralCache { + fn new( + o3_lambda: usize, + radial: &LodeRadialBasis, + density: DensityKind, + k_cutoff: f64, + spline_accuracy: Option, + ) -> Result { + let implementation = match radial { + LodeRadialBasis::Gto { .. } => { + let gto = LodeRadialIntegralGto::new(radial, o3_lambda)?; + + if let Some(accuracy) = spline_accuracy { + let do_center_contribution = o3_lambda == 0; + Box::new(LodeRadialIntegralSpline::with_accuracy( + gto, density, k_cutoff, accuracy, do_center_contribution + )?) + } else { + Box::new(gto) as Box + } + }, + LodeRadialBasis::Tabulated(ref tabulated) => { + Box::new(LodeRadialIntegralSpline::from_tabulated( + tabulated.clone(), + density, + )) as Box + } + }; + + let size = implementation.size(); + let values = Array1::from_elem(size, 0.0); + let gradients = Array1::from_elem(size, 0.0); + + return Ok(LodeRadialIntegralCache { + implementation, + values, + gradients, + }); + } + + /// Run the calculation, the results are stored inside `self.values` and + /// `self.gradients` + pub fn compute(&mut self, k_norm: f64, do_gradients: bool) { + let gradient_view = if do_gradients { + Some(self.gradients.view_mut()) + } else { + None + }; + + self.implementation.compute(k_norm, self.values.view_mut(), gradient_view); + } + + /// Get the size of this radial integral (i.e. number of radial basis) + pub fn size(&self) -> usize { + return self.implementation.size() + } + + /// Get the size of this radial integral (i.e. number of radial basis) + pub fn get_center_contribution(&self, density: DensityKind) -> Result, Error> { + return self.implementation.get_center_contribution(density); + } +} + +/// Store all `LodeRadialIntegralCache` for different angular channels +pub struct LodeRadialIntegralCacheByAngular { + pub(crate) by_angular: BTreeMap, +} + +impl LodeRadialIntegralCacheByAngular { + /// Create a new `LodeRadialIntegralCacheByAngular` for the given radial basis & parameters + pub fn new( + density: DensityKind, + basis: &SphericalExpansionBasis, + k_cutoff: f64 + ) -> Result { + match basis { + SphericalExpansionBasis::TensorProduct(basis) => { + let mut by_angular = BTreeMap::new(); + for o3_lambda in 0..=basis.max_angular { + let cache = LodeRadialIntegralCache::new( + o3_lambda, + &basis.radial, + density, + k_cutoff, + basis.spline_accuracy + )?; + by_angular.insert(o3_lambda, cache); + } + + return Ok(LodeRadialIntegralCacheByAngular { + by_angular + }); + } + SphericalExpansionBasis::Explicit(basis) => { + let mut by_angular = BTreeMap::new(); + for (&o3_lambda, radial) in &*basis.by_angular { + let cache = LodeRadialIntegralCache::new( + o3_lambda, + radial, + density, + k_cutoff, + basis.spline_accuracy + )?; + by_angular.insert(o3_lambda, cache); + } + return Ok(LodeRadialIntegralCacheByAngular { + by_angular + }); + } + } + } + + /// Run the calculation, the results are accessible with `get` + pub fn compute(&mut self, distance: f64, do_gradients: bool) { + self.by_angular.iter_mut().for_each(|(_, cache)| cache.compute(distance, do_gradients)); + } + + /// Get one of the individual cache, corresponding to the `o3_lambda` + /// angular channel + pub fn get(&self, o3_lambda: usize) -> Option<&LodeRadialIntegralCache> { + self.by_angular.get(&o3_lambda) + } + + pub(crate) fn get_mut(&mut self, o3_lambda: usize) -> Option<&mut LodeRadialIntegralCache> { + self.by_angular.get_mut(&o3_lambda) + } +} diff --git a/featomic/src/calculators/lode/radial_integral/spline.rs b/featomic/src/calculators/lode/radial_integral/spline.rs new file mode 100644 index 000000000..753e1c5f0 --- /dev/null +++ b/featomic/src/calculators/lode/radial_integral/spline.rs @@ -0,0 +1,159 @@ +use std::sync::Arc; + +use ndarray::{Array1, ArrayViewMut1}; + +use super::LodeRadialIntegral; +use crate::calculators::shared::basis::radial::LodeTabulated; +use crate::calculators::shared::DensityKind; +use crate::math::{HermitCubicSpline, SplineParameters}; +use crate::Error; + +/// `LodeRadialIntegralSpline` allows to evaluate another radial integral +/// implementation using [cubic Hermit spline][splines-wiki]. +/// +/// This can be much faster than using the actual radial integral +/// implementation. +/// +/// [splines-wiki]: https://en.wikipedia.org/wiki/Cubic_Hermite_spline +pub struct LodeRadialIntegralSpline { + spline: Arc>, + density: DensityKind, + center_contribution: Option>, +} + +impl LodeRadialIntegralSpline { + /// Create a new `LodeRadialIntegralSpline` taking values from the given + /// `radial_integral`. Points are added to the spline until the requested + /// accuracy is reached. We consider that the accuracy is reached when + /// either the mean absolute error or the mean relative error gets below the + /// `accuracy` threshold. + #[time_graph::instrument(name = "LodeRadialIntegralSpline::with_accuracy")] + pub fn with_accuracy( + radial_integral: impl LodeRadialIntegral, + density: DensityKind, + k_cutoff: f64, + accuracy: f64, + with_center_contribution: bool, + ) -> Result { + let size = radial_integral.size(); + let spline_parameters = SplineParameters { + start: 0.0, + stop: k_cutoff, + shape: vec![size], + }; + + let spline = HermitCubicSpline::with_accuracy( + accuracy, + spline_parameters, + |x| { + let mut values = Array1::from_elem(size, 0.0); + let mut derivatives = Array1::from_elem(size, 0.0); + radial_integral.compute(x, values.view_mut(), Some(derivatives.view_mut())); + (values, derivatives) + }, + )?; + + let mut center_contribution = None; + if with_center_contribution { + center_contribution = Some(radial_integral.get_center_contribution(density)?); + } + + return Ok(LodeRadialIntegralSpline { + spline: Arc::new(spline), + density: density, + center_contribution: center_contribution, + }); + } + + /// Create a new `LodeRadialIntegralSpline` with user-defined spline points. + /// + /// The density/`tabulated.center_contribution` are assumed to match each + /// other + pub fn from_tabulated(tabulated: LodeTabulated, density: DensityKind) -> LodeRadialIntegralSpline { + return LodeRadialIntegralSpline { + spline: tabulated.spline, + density: density, + center_contribution: tabulated.center_contribution, + }; + } +} + +impl LodeRadialIntegral for LodeRadialIntegralSpline { + fn size(&self) -> usize { + self.spline.points[0].values.shape()[0] + } + + #[time_graph::instrument(name = "SplinedRadialIntegral::compute")] + fn compute(&self, x: f64, values: ArrayViewMut1, gradients: Option>) { + self.spline.compute(x, values, gradients); + } + + fn get_center_contribution(&self, density: DensityKind) -> Result, Error> { + if density != self.density { + return Err(Error::InvalidParameter("mismatched atomic density in splined LODE radial integral".into())); + } + + if self.center_contribution.is_none() { + return Err(Error::InvalidParameter( + "`center_contribution` must be defined for the Tabulated radial \ + basis used with the L=0 angular channel".into() + )); + } + + return Ok(self.center_contribution.clone().expect("just checked")); + } +} + +#[cfg(test)] +mod tests { + use approx::assert_relative_eq; + + use crate::calculators::LodeRadialBasis; + + use super::*; + use super::super::LodeRadialIntegralGto; + + #[test] + fn high_accuracy() { + // Check that even with high accuracy and large domain MAX_SPLINE_SIZE is enough + let basis = LodeRadialBasis::Gto { max_radial: 15, radius: 5.0 }; + let gto = LodeRadialIntegralGto::new(&basis, 3).unwrap(); + + let accuracy = 5e-10; + let k_cutoff = 10.0; + let density = DensityKind::SmearedPowerLaw { smearing: 0.5, exponent: 1 }; + + // this test only check that this code runs without crashing + LodeRadialIntegralSpline::with_accuracy(gto, density, k_cutoff, accuracy, true).unwrap(); + } + + #[test] + fn finite_difference() { + let radial_size = 8; + let basis = LodeRadialBasis::Gto { max_radial: (radial_size - 1), radius: 5.0 }; + let gto = LodeRadialIntegralGto::new(&basis, 3).unwrap(); + + let accuracy = 1e-2; + let k_cutoff = 10.0; + let density = DensityKind::SmearedPowerLaw { smearing: 0.5, exponent: 1 }; + + // even with very bad accuracy, we want the gradients of the spline to match the + // values produces by the spline, and not necessarily the actual GTO gradients. + let spline = LodeRadialIntegralSpline::with_accuracy(gto, density, k_cutoff, accuracy, false).unwrap(); + + let rij = 3.4; + let delta = 1e-9; + + let mut values = Array1::from_elem(radial_size, 0.0); + let mut values_delta = Array1::from_elem(radial_size, 0.0); + let mut gradients = Array1::from_elem(radial_size, 0.0); + spline.compute(rij, values.view_mut(), Some(gradients.view_mut())); + spline.compute(rij + delta, values_delta.view_mut(), None); + + let finite_differences = (&values_delta - &values) / delta; + assert_relative_eq!( + finite_differences, gradients, + epsilon=delta, max_relative=5e-6 + ); + } +} diff --git a/rascaline/src/calculators/lode/spherical_expansion.rs b/featomic/src/calculators/lode/spherical_expansion.rs similarity index 74% rename from rascaline/src/calculators/lode/spherical_expansion.rs rename to featomic/src/calculators/lode/spherical_expansion.rs index 6599458cd..92bdb004d 100644 --- a/rascaline/src/calculators/lode/spherical_expansion.rs +++ b/featomic/src/calculators/lode/spherical_expansion.rs @@ -8,6 +8,8 @@ use ndarray::{Array1, Array2, Array3, s}; use metatensor::TensorMap; use metatensor::{LabelsBuilder, Labels, LabelValue}; +use crate::calculators::shared::DensityKind::SmearedPowerLaw; +use crate::calculators::shared::{Density, SphericalExpansionBasis}; use crate::{Error, System, Vector3D}; use crate::systems::UnitCell; @@ -20,10 +22,10 @@ use crate::math::SphericalHarmonicsCache; use crate::math::{KVector, compute_k_vectors}; use crate::math::{expi, erfc, gamma}; -use crate::calculators::radial_basis::RadialBasis; -use super::radial_integral::{LodeRadialIntegralCache, LodeRadialIntegralParameters}; +use super::radial_integral::LodeRadialIntegralCacheByAngular; -use super::super::{split_tensor_map_by_system, array_mut_for_system}; +use super::super::shared::descriptors_by_systems::{split_tensor_map_by_system, array_mut_for_system}; +use super::super::shared::LodeRadialBasis; /// Parameters for LODE spherical expansion calculator. /// @@ -34,41 +36,27 @@ use super::super::{split_tensor_map_by_system, array_mut_for_system}; #[derive(Debug, Clone)] #[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] pub struct LodeSphericalExpansionParameters { - /// Spherical real space cutoff to use for atomic environments. - /// Note that this cutoff is only used for the projection of the density. - /// In contrast to SOAP, LODE also takes atoms outside of this cutoff into - /// account for the density. - pub cutoff: f64, - /// Spherical reciprocal cutoff. If `k_cutoff` is `None` a cutoff of `1.2 π - /// / atomic_gaussian_width`, which is a reasonable value for most systems, - /// is used. + /// Spherical reciprocal cutoff. If `k_cutoff` is `None`, a cutoff of + /// `1.2 π / SmearedPowerLaw.width`, which is a reasonable value for most + /// systems, is used. pub k_cutoff: Option, - /// Number of radial basis function to use in the expansion - pub max_radial: usize, - /// Number of spherical harmonics to use in the expansion - pub max_angular: usize, - /// Width of the atom-centered gaussian used to create the atomic density. - pub atomic_gaussian_width: f64, - /// Weight of the central atom contribution in the central image to the - /// features. If `1` the center atom contribution is weighted the same - /// as any other contribution. If `0` the central atom does not - /// contribute to the features at all. - pub center_atom_weight: f64, - /// Radial basis to use for the radial integral - pub radial_basis: RadialBasis, - /// Potential exponent of the decorated atom density. Currently only - /// implemented for `potential_exponent < 10`. Some exponents can be - /// connected to SOAP or physics-based quantities: p=0 uses Gaussian - /// densities as in SOAP, p=1 uses 1/r Coulomb like densities, p=6 uses - /// 1/r^6 dispersion like densities." - pub potential_exponent: usize, + /// Definition of the density arising from atoms in the whole system + pub density: Density, + /// Definition of the basis functions used to expand the atomic density in + /// local environments + pub basis: SphericalExpansionBasis, } impl LodeSphericalExpansionParameters { /// Get the value of the k-space cutoff (either provided by the user or a /// default). pub fn get_k_cutoff(&self) -> f64 { - return self.k_cutoff.unwrap_or(1.2 * std::f64::consts::PI / self.atomic_gaussian_width); + match self.density.kind { + SmearedPowerLaw { smearing, .. } => { + return self.k_cutoff.unwrap_or(1.2 * std::f64::consts::PI / smearing); + }, + _ => unreachable!() + } } } @@ -80,11 +68,11 @@ pub struct LodeSphericalExpansion { /// implementation + cached allocation to compute spherical harmonics spherical_harmonics: ThreadLocal>, /// implementation + cached allocation to compute the radial integral - radial_integral: ThreadLocal>, + radial_integral: ThreadLocal>, /// Cached allocations for the k-vector to nlm projection coefficients. - /// The vector contains different l values, and the Array is indexed by + /// The map contains different l values, and the Array is indexed by /// `m, n, k`. - k_vector_to_m_n: ThreadLocal>>>, + k_vector_to_m_n: ThreadLocal>>>, /// Cached allocation for everything that only depends on the k vector k_dependent_values: ThreadLocal>>, } @@ -198,24 +186,33 @@ fn resize_array1(array: &mut ndarray::Array1, shape: usize) { impl LodeSphericalExpansion { pub fn new(parameters: LodeSphericalExpansionParameters) -> Result { - if parameters.potential_exponent >= 10 { + match parameters.density.kind { + SmearedPowerLaw { exponent, .. } => { + if exponent >= 10 { + return Err(Error::InvalidParameter( + "LODE is only implemented for potential_exponent < 10".into() + )); + } + } + _ => { + return Err(Error::InvalidParameter( + "only SmearedPowerLaw density can be used with LODE".into() + )); + } + } + + if parameters.density.scaling.is_some() { return Err(Error::InvalidParameter( - "LODE is only implemented for potential_exponent < 10".into() + "LODE does not support custom density scaling".into() )); } // validate the parameters once here, so we are sure we can construct // more radial integrals later - LodeRadialIntegralCache::new( - parameters.radial_basis.clone(), - LodeRadialIntegralParameters { - max_radial: parameters.max_radial, - max_angular: parameters.max_angular, - atomic_gaussian_width: parameters.atomic_gaussian_width, - cutoff: parameters.cutoff, - k_cutoff: parameters.get_k_cutoff(), - potential_exponent: parameters.potential_exponent, - } + LodeRadialIntegralCacheByAngular::new( + parameters.density.kind, + ¶meters.basis, + parameters.get_k_cutoff() )?; return Ok(LodeSphericalExpansion { @@ -228,54 +225,51 @@ impl LodeSphericalExpansion { } fn project_k_to_nlm(&self, k_vectors: &[KVector]) { - let mut k_vector_to_m_n = self.k_vector_to_m_n.get_or(|| { - let mut k_vector_to_m_n = Vec::new(); - for _ in 0..=self.parameters.max_angular { - k_vector_to_m_n.push(Array3::from_elem((0, 0, 0), 0.0)); - } - - return RefCell::new(k_vector_to_m_n); - }).borrow_mut(); - - for o3_lambda in 0..=self.parameters.max_angular { - let shape = (2 * o3_lambda + 1, self.parameters.max_radial, k_vectors.len()); - resize_array3(&mut k_vector_to_m_n[o3_lambda], shape); - } - let mut radial_integral = self.radial_integral.get_or(|| { - let radial_integral = LodeRadialIntegralCache::new( - self.parameters.radial_basis.clone(), - LodeRadialIntegralParameters { - max_radial: self.parameters.max_radial, - max_angular: self.parameters.max_angular, - atomic_gaussian_width: self.parameters.atomic_gaussian_width, - cutoff: self.parameters.cutoff, - k_cutoff: self.parameters.get_k_cutoff(), - potential_exponent: self.parameters.potential_exponent, - } + let radial_integral = LodeRadialIntegralCacheByAngular::new( + self.parameters.density.kind, + &self.parameters.basis, + self.parameters.get_k_cutoff() ).expect("could not create a radial integral"); return RefCell::new(radial_integral); }).borrow_mut(); let mut spherical_harmonics = self.spherical_harmonics.get_or(|| { - let spherical_harmonics = SphericalHarmonicsCache::new(self.parameters.max_angular); - return RefCell::new(spherical_harmonics); + let max_angular = self.parameters.basis.angular_channels().into_iter().max().unwrap_or(0); + RefCell::new(SphericalHarmonicsCache::new(max_angular)) + }).borrow_mut(); + + let mut k_vector_to_m_n = self.k_vector_to_m_n.get_or(|| { + let mut k_vector_to_m_n = BTreeMap::new(); + for o3_lambda in self.parameters.basis.angular_channels() { + k_vector_to_m_n.insert(o3_lambda, Array3::from_elem((0, 0, 0), 0.0)); + } + + return RefCell::new(k_vector_to_m_n); }).borrow_mut(); + for o3_lambda in self.parameters.basis.angular_channels() { + let radial_size = radial_integral.get(o3_lambda).expect("missing o3_lambda").size(); + let shape = (2 * o3_lambda + 1, radial_size, k_vectors.len()); + resize_array3(k_vector_to_m_n.get_mut(&o3_lambda).expect("missing o3_lambda"), shape); + } + for (ik, k_vector) in k_vectors.iter().enumerate() { // we don't need the gradients of spherical harmonics/radial // integral w.r.t. k-vectors until we implement gradients w.r.t cell radial_integral.compute(k_vector.norm, false); spherical_harmonics.compute(k_vector.direction, false); - for l in 0..=self.parameters.max_angular { - let spherical_harmonics = spherical_harmonics.values.slice(l as isize); - let radial_integral = radial_integral.values.slice(s![l, ..]); + for o3_lambda in self.parameters.basis.angular_channels() { + let spherical_harmonics = spherical_harmonics.values.angular_slice(o3_lambda); + let radial_integral = radial_integral.get(o3_lambda).expect("missing o3_lambda"); + let radial_integral = &radial_integral.values; + let array = k_vector_to_m_n.get_mut(&o3_lambda).expect("missing o3_lambda"); for (m, sph_value) in spherical_harmonics.iter().enumerate() { for (n, ri_value) in radial_integral.iter().enumerate() { - k_vector_to_m_n[l][[m, n, ik]] = ri_value * sph_value; + array[[m, n, ik]] = ri_value * sph_value; } } } @@ -284,11 +278,14 @@ impl LodeSphericalExpansion { #[allow(clippy::float_cmp)] fn compute_density_fourier(&self, k_vectors: &[KVector]) -> Array1 { - let mut fourier = Vec::with_capacity(k_vectors.len()); - - let potential_exponent = self.parameters.potential_exponent as f64; - let smearing_squared = self.parameters.atomic_gaussian_width * self.parameters.atomic_gaussian_width; + let (potential_exponent, smearing_squared) = match self.parameters.density.kind { + SmearedPowerLaw { smearing, exponent } => { + (exponent as f64, smearing * smearing) + }, + _ => unreachable!() + }; + let mut fourier = Vec::with_capacity(k_vectors.len()); if potential_exponent == 0.0 { let factor = (4.0 * std::f64::consts::PI * smearing_squared).powf(0.75); @@ -346,53 +343,50 @@ impl LodeSphericalExpansion { /// Compute k = 0 contributions. /// - /// Values are only non zero for `potential_exponent` = 0 and > 3. + /// Values are only non zero for `exponent` = 0 and > 3. fn compute_k0_contributions(&self) -> Array1 { - let atomic_gaussian_width = self.parameters.atomic_gaussian_width; + let (exponent, smearing) = match self.parameters.density.kind { + SmearedPowerLaw { exponent, smearing } => { + (exponent, smearing) + }, + _ => unreachable!() + }; - let mut k0_contrib = Vec::with_capacity(self.parameters.max_radial); - let factor = if self.parameters.potential_exponent == 0 { - let smearing_squared = atomic_gaussian_width * atomic_gaussian_width; + let factor = if exponent == 0 { + let smearing_squared = smearing * smearing; (2.0 * std::f64::consts::PI * smearing_squared).powf(1.5) / (std::f64::consts::PI * smearing_squared).powf(0.75) / f64::sqrt(4.0 * std::f64::consts::PI) - } else if self.parameters.potential_exponent > 3 { - let potential_exponent = self.parameters.potential_exponent; - let p_eff = 3. - potential_exponent as f64; + } else if exponent > 3 { + let p_eff = 3.0 - exponent as f64; 0.5 * std::f64::consts::PI * 2.0_f64.powf(p_eff) - / gamma(0.5 * potential_exponent as f64) - * 2.0_f64.powf((potential_exponent as f64 - 1.0) / 2.0) / -p_eff - * atomic_gaussian_width.powf(-p_eff) - / atomic_gaussian_width.powf(2.0 * potential_exponent as f64 - 6.0) + / gamma(0.5 * exponent as f64) + * 2.0_f64.powf((exponent as f64 - 1.0) / 2.0) / -p_eff + * smearing.powf(-p_eff) + / smearing.powf(2.0 * exponent as f64 - 6.0) } else { 0.0 }; - let mut radial_integral = self.radial_integral.get_or(|| { - let radial_integral = LodeRadialIntegralCache::new( - self.parameters.radial_basis.clone(), - LodeRadialIntegralParameters { - max_radial: self.parameters.max_radial, - max_angular: self.parameters.max_angular, - atomic_gaussian_width: self.parameters.atomic_gaussian_width, - cutoff: self.parameters.cutoff, - k_cutoff: self.parameters.get_k_cutoff(), - potential_exponent: self.parameters.potential_exponent, - } + let mut radial_integral = self.radial_integral.get_or(|| { + let radial_integral = LodeRadialIntegralCacheByAngular::new( + self.parameters.density.kind, + &self.parameters.basis, + self.parameters.get_k_cutoff() ).expect("could not create a radial integral"); return RefCell::new(radial_integral); }).borrow_mut(); + let radial_integral = radial_integral.get_mut(0) + .expect("k0 contributions can't be done when o3_lambda=0 is missing"); + radial_integral.compute(0.0, false); - for n in 0..self.parameters.max_radial { - k0_contrib.push(factor * radial_integral.values[[0, n]]); - } - return k0_contrib.into(); + return factor * radial_integral.values.clone(); } /// Compute center atom contribution. @@ -400,25 +394,23 @@ impl LodeSphericalExpansion { /// By symmetry, this only affects the (l, m) = (0, 0) components of the /// projection coefficients and only the neighbor type blocks that agrees /// with the center atom. - fn do_center_contribution(&mut self, systems: &mut[Box], descriptor: &mut TensorMap) -> Result<(), Error> { - let mut radial_integral = self.radial_integral.get_or(|| { - let radial_integral = LodeRadialIntegralCache::new( - self.parameters.radial_basis.clone(), - LodeRadialIntegralParameters { - max_radial: self.parameters.max_radial, - max_angular: self.parameters.max_angular, - atomic_gaussian_width: self.parameters.atomic_gaussian_width, - cutoff: self.parameters.cutoff, - k_cutoff: self.parameters.get_k_cutoff(), - potential_exponent: self.parameters.potential_exponent, - } + fn do_center_contribution(&mut self, systems: &mut[System], descriptor: &mut TensorMap) -> Result<(), Error> { + if !self.parameters.basis.angular_channels().contains(&0) { + // o3_lambda is not part of the output, skip self contributions + return Ok(()); + } + + let radial_integral = self.radial_integral.get_or(|| { + let radial_integral = LodeRadialIntegralCacheByAngular::new( + self.parameters.density.kind, &self.parameters.basis, self.parameters.get_k_cutoff() ).expect("could not create a radial integral"); return RefCell::new(radial_integral); }).borrow_mut(); - radial_integral.compute_center_contribution(); - let central_atom_contrib = &radial_integral.center_contribution; + let central_atom_contrib = &radial_integral.get(0) + .expect("missing o3_lambda") + .get_center_contribution(self.parameters.density.kind)?; for (system_i, system) in systems.iter_mut().enumerate() { let types = system.types()?; @@ -448,7 +440,7 @@ impl LodeSphericalExpansion { for (property_i, [n]) in block.properties.iter_fixed_size().enumerate() { let n = n.usize(); - array[[sample_i, 0, property_i]] -= (1.0 - self.parameters.center_atom_weight) * central_atom_contrib[n]; + array[[sample_i, 0, property_i]] -= (1.0 - self.parameters.density.center_atom_weight) * central_atom_contrib[n]; } } } @@ -470,13 +462,13 @@ impl CalculatorBase for LodeSphericalExpansion { &[] } - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { let builder = AllTypesPairsKeys {}; let keys = builder.keys(systems)?; let mut builder = LabelsBuilder::new(vec!["o3_lambda", "o3_sigma", "center_type", "neighbor_type"]); for &[center_type, neighbor_type] in keys.iter_fixed_size() { - for o3_lambda in 0..=self.parameters.max_angular { + for o3_lambda in self.parameters.basis.angular_channels() { builder.add(&[o3_lambda.into(), 1.into(), center_type, neighbor_type]); } } @@ -488,7 +480,7 @@ impl CalculatorBase for LodeSphericalExpansion { LongRangeSamplesPerAtom::sample_names() } - fn samples(&self, keys: &Labels, systems: &mut [Box]) -> Result, Error> { + fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error> { assert_eq!(keys.names(), ["o3_lambda", "o3_sigma", "center_type", "neighbor_type"]); // only compute the samples once for each `center_type, neighbor_type`, @@ -527,7 +519,7 @@ impl CalculatorBase for LodeSphericalExpansion { } } - fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [Box]) -> Result, Error> { + fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [System]) -> Result, Error> { assert_eq!(keys.names(), ["o3_lambda", "o3_sigma", "center_type", "neighbor_type"]); assert_eq!(keys.count(), samples.len()); @@ -578,17 +570,36 @@ impl CalculatorBase for LodeSphericalExpansion { } fn properties(&self, keys: &Labels) -> Vec { - let mut properties = LabelsBuilder::new(self.property_names()); - for n in 0..self.parameters.max_radial { - properties.add(&[n]); - } - let properties = properties.finish(); + assert_eq!(keys.names(), ["o3_lambda", "o3_sigma", "center_type", "neighbor_type"]); + + match self.parameters.basis { + SphericalExpansionBasis::TensorProduct(ref basis) => { + let mut properties = LabelsBuilder::new(self.property_names()); + for n in 0..basis.radial.size() { + properties.add(&[n]); + } - return vec![properties; keys.count()]; + return vec![properties.finish(); keys.count()]; + } + SphericalExpansionBasis::Explicit(ref basis) => { + let mut result = Vec::new(); + for [o3_lambda, _, _, _] in keys.iter_fixed_size() { + let mut properties = LabelsBuilder::new(self.property_names()); + + let radial = basis.by_angular.get(&o3_lambda.usize()).expect("missing o3_lambda"); + for n in 0..radial.size() { + properties.add(&[n]); + } + + result.push(properties.finish()); + } + return result; + } + } } #[time_graph::instrument(name = "LodeSphericalExpansion::compute")] - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { assert_eq!(descriptor.keys().names(), ["o3_lambda", "o3_sigma", "center_type", "neighbor_type"]); self.do_center_contribution(systems, descriptor)?; @@ -611,6 +622,11 @@ impl CalculatorBase for LodeSphericalExpansion { }; let n_systems = systems.len(); + let potential_exponent = match self.parameters.density.kind { + SmearedPowerLaw { exponent, .. } => exponent, + _ => unreachable!() + }; + systems.par_iter_mut() .zip_eq(&mut descriptors_by_system) .with_min_len(if parallel_systems {1} else {n_systems}) @@ -640,7 +656,8 @@ impl CalculatorBase for LodeSphericalExpansion { let global_factor = 4.0 * std::f64::consts::PI / cell.volume(); // Add k = 0 contributions for (m, l) = (0, 0) - if self.parameters.potential_exponent == 0 || self.parameters.potential_exponent > 3 { + let has_lambda_0 = self.parameters.basis.angular_channels().contains(&0); + if has_lambda_0 && (potential_exponent == 0 || potential_exponent > 3) { let k0_contrib = &self.compute_k0_contributions(); for &neighbor_type in types { for center_i in 0..system.size()? { @@ -700,7 +717,7 @@ impl CalculatorBase for LodeSphericalExpansion { sf_per_center_imag }; - let k_vector_to_m_n = &k_vector_to_m_n[o3_lambda]; + let k_vector_to_m_n = k_vector_to_m_n.get(&o3_lambda).expect("missing o3_lambda"); let data = block.data_mut(); let samples = &*data.samples; @@ -866,9 +883,10 @@ impl CalculatorBase for LodeSphericalExpansion { #[cfg(test)] mod tests { + use crate::calculators::shared::ExplicitBasis; use crate::Calculator; - use crate::calculators::CalculatorBase; - use crate::systems::test_utils::test_system; + use crate::calculators::{CalculatorBase, DensityKind, LodeRadialBasis, TensorProductBasis}; + use crate::systems::test_utils::{test_system, test_systems}; use Vector3D; use approx::assert_relative_eq; @@ -883,14 +901,20 @@ mod tests { for p in 0..=6 { let calculator = Calculator::from(Box::new(LodeSphericalExpansion::new( LodeSphericalExpansionParameters { - cutoff: 1.0, k_cutoff: None, - max_radial: 4, - max_angular: 4, - atomic_gaussian_width: 1.0, - center_atom_weight: 1.0, - radial_basis: RadialBasis::splined_gto(1e-8), - potential_exponent: p, + density: Density { + kind: DensityKind::SmearedPowerLaw { + smearing: 1.0, + exponent: p, + }, + scaling: None, + center_atom_weight: 1.0, + }, + basis: SphericalExpansionBasis::TensorProduct(TensorProductBasis { + max_angular: 3, + radial: LodeRadialBasis::Gto { max_radial: 3, radius: 1.0 }, + spline_accuracy: Some(1e-8), + }), } ).unwrap()) as Box); @@ -907,14 +931,20 @@ mod tests { fn compute_partial() { let calculator = Calculator::from(Box::new(LodeSphericalExpansion::new( LodeSphericalExpansionParameters { - cutoff: 1.0, k_cutoff: None, - max_radial: 4, - max_angular: 2, - atomic_gaussian_width: 1.0, - center_atom_weight: 1.0, - radial_basis: RadialBasis::splined_gto(1e-8), - potential_exponent: 1, + density: Density { + kind: DensityKind::SmearedPowerLaw { + smearing: 1.0, + exponent: 1, + }, + scaling: None, + center_atom_weight: 1.0, + }, + basis: SphericalExpansionBasis::TensorProduct(TensorProductBasis { + max_angular: 2, + radial: LodeRadialBasis::Gto { max_radial: 3, radius: 1.0 }, + spline_accuracy: Some(1e-8), + }), } ).unwrap()) as Box); @@ -949,14 +979,16 @@ mod tests { ]); crate::calculators::tests_utils::compute_partial( - calculator, &mut [Box::new(system)], &keys, &samples, &properties + calculator, &mut [System::new(system)], &keys, &samples, &properties ); } #[test] fn compute_density_fourier() { - let k_vectors = [KVector{direction: Vector3D::zero(), norm: 1e-12}, - KVector{direction: Vector3D::zero(), norm: 1e-11}]; + let k_vectors = [ + KVector { direction: Vector3D::zero(), norm: 1e-12 }, + KVector { direction: Vector3D::zero(), norm: 1e-11 } + ]; // Reference values taken from pyLODE let reference_vals = [ @@ -968,14 +1000,20 @@ mod tests { for (i, &p) in [0, 4, 6].iter().enumerate(){ let spherical_expansion = LodeSphericalExpansion::new( LodeSphericalExpansionParameters { - cutoff: 3.5, k_cutoff: None, - max_radial: 6, - max_angular: 6, - atomic_gaussian_width: 1.0, - center_atom_weight: 1.0, - radial_basis: RadialBasis::splined_gto(1e-8), - potential_exponent: p, + density: Density { + kind: DensityKind::SmearedPowerLaw { + smearing: 1.0, + exponent: p, + }, + scaling: None, + center_atom_weight: 1.0, + }, + basis: SphericalExpansionBasis::TensorProduct(TensorProductBasis { + max_angular: 5, + radial: LodeRadialBasis::Gto { max_radial: 5, radius: 3.5 }, + spline_accuracy: Some(1e-8), + }), } ).unwrap(); @@ -987,38 +1025,24 @@ mod tests { } } - #[test] - fn default_k_cutoff() { - let atomic_gaussian_width = 0.4; - let parameters = LodeSphericalExpansionParameters { - cutoff: 3.5, - k_cutoff: None, - max_radial: 6, - max_angular: 6, - atomic_gaussian_width: atomic_gaussian_width, - center_atom_weight: 1.0, - potential_exponent: 1, - radial_basis: RadialBasis::splined_gto(1e-8), - }; - - assert_eq!( - parameters.get_k_cutoff(), - 1.2 * std::f64::consts::PI / atomic_gaussian_width - ); - } - #[test] fn compute_k0_contributions_p0() { let spherical_expansion = LodeSphericalExpansion::new( LodeSphericalExpansionParameters { - cutoff: 3.5, k_cutoff: None, - max_radial: 6, - max_angular: 6, - atomic_gaussian_width: 0.8, - center_atom_weight: 1.0, - potential_exponent: 0, - radial_basis: RadialBasis::splined_gto(1e-8), + density: Density { + kind: DensityKind::SmearedPowerLaw { + smearing: 0.8, + exponent: 0, + }, + scaling: None, + center_atom_weight: 1.0, + }, + basis: SphericalExpansionBasis::TensorProduct(TensorProductBasis { + max_angular: 5, + radial: LodeRadialBasis::Gto { max_radial: 5, radius: 3.5 }, + spline_accuracy: Some(1e-8), + }), } ).unwrap(); @@ -1031,16 +1055,24 @@ mod tests { #[test] fn compute_k0_contributions_p6() { - let spherical_expansion = LodeSphericalExpansion::new(LodeSphericalExpansionParameters { - cutoff: 3.5, - k_cutoff: None, - max_radial: 6, - max_angular: 6, - atomic_gaussian_width: 0.8, - center_atom_weight: 1.0, - potential_exponent: 6, - radial_basis: RadialBasis::splined_gto(1e-8), - }).unwrap(); + let spherical_expansion = LodeSphericalExpansion::new( + LodeSphericalExpansionParameters { + k_cutoff: None, + density: Density { + kind: DensityKind::SmearedPowerLaw { + smearing: 0.8, + exponent: 6, + }, + scaling: None, + center_atom_weight: 1.0, + }, + basis: SphericalExpansionBasis::TensorProduct(TensorProductBasis { + max_angular: 5, + radial: LodeRadialBasis::Gto { max_radial: 5, radius: 3.5 }, + spline_accuracy: Some(1e-8), + }), + } + ).unwrap(); assert_relative_eq!( spherical_expansion.compute_k0_contributions(), @@ -1048,4 +1080,43 @@ mod tests { max_relative=1e-4 ); } + + #[test] + fn explicit_basis() { + let mut by_angular = BTreeMap::new(); + by_angular.insert(1, LodeRadialBasis::Gto { max_radial: 5, radius: 5.5 }); + by_angular.insert(12, LodeRadialBasis::Gto { max_radial: 3, radius: 3.4 }); + + let mut calculator = Calculator::from(Box::new(LodeSphericalExpansion::new( + LodeSphericalExpansionParameters { + k_cutoff: None, + density: Density { + kind: DensityKind::SmearedPowerLaw { + smearing: 0.8, + exponent: 1, + }, + scaling: None, + center_atom_weight: 1.0, + }, + basis: SphericalExpansionBasis::Explicit(ExplicitBasis { + by_angular: by_angular.into(), + spline_accuracy: Some(1e-8), + }), + } + ).unwrap()) as Box); + + let mut systems = test_systems(&["water"]); + + let descriptor = calculator.compute(&mut systems, Default::default()).unwrap(); + + for (key, block) in &descriptor { + if key[0] == 1 { + assert_eq!(block.properties().count(), 6); + } else if key[0] == 12 { + assert_eq!(block.properties().count(), 4); + } else { + panic!("unexpected o3_lambda value"); + } + } + } } diff --git a/rascaline/src/calculators/mod.rs b/featomic/src/calculators/mod.rs similarity index 78% rename from rascaline/src/calculators/mod.rs rename to featomic/src/calculators/mod.rs index 5da33e0cf..7fdf81cec 100644 --- a/rascaline/src/calculators/mod.rs +++ b/featomic/src/calculators/mod.rs @@ -2,6 +2,21 @@ use metatensor::{TensorMap, Labels}; use crate::{Error, System}; + +/// Which gradients are we computing +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(self) struct GradientsOptions { + pub positions: bool, + pub cell: bool, + pub strain: bool, +} + +impl GradientsOptions { + pub fn any(self) -> bool { + return self.positions || self.cell || self.strain; + } +} + /// The `CalculatorBase` trait is the interface shared by all calculator /// implementations; and used by [`crate::Calculator`] to run the calculation. /// @@ -22,14 +37,14 @@ pub trait CalculatorBase: std::panic::RefUnwindSafe { fn cutoffs(&self) -> &[f64]; /// Get the set of keys for this calculator and the given systems - fn keys(&self, systems: &mut [Box]) -> Result; + fn keys(&self, systems: &mut [System]) -> Result; /// Get the names used for sample labels by this calculator fn sample_names(&self) -> Vec<&str>; /// Get the full list of samples this calculator would create for the given /// systems. This function should return one set of samples for each key. - fn samples(&self, keys: &Labels, systems: &mut [Box]) -> Result, Error>; + fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error>; /// Can this calculator compute gradients with respect to the `parameter`? /// Right now, `parameter` can be either `"positions"`, `"strain"` or @@ -43,7 +58,7 @@ pub trait CalculatorBase: std::panic::RefUnwindSafe { /// /// If the gradients with respect to positions are not available, this /// function should return an error. - fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [Box]) -> Result, Error>; + fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [System]) -> Result, Error>; /// Get the components this calculator computes for each key. fn components(&self, keys: &Labels) -> Vec>; @@ -66,7 +81,7 @@ pub trait CalculatorBase: std::panic::RefUnwindSafe { /// block if they are supported according to /// [`CalculatorBase::supports_gradient`], and the users requested them as /// part of the calculation options. - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error>; + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error>; } @@ -85,17 +100,19 @@ pub use self::sorted_distances::SortedDistances; mod neighbor_list; pub use self::neighbor_list::NeighborList; -mod radial_basis; -pub use self::radial_basis::{RadialBasis, GtoRadialBasis}; - -mod descriptors_by_systems; -pub(crate) use self::descriptors_by_systems::{array_mut_for_system, split_tensor_map_by_system}; +mod shared; +pub use self::shared::{Density, DensityKind, DensityScaling}; +pub use self::shared::{SphericalExpansionBasis, TensorProductBasis}; +pub use self::shared::{SoapRadialBasis, LodeRadialBasis}; pub mod soap; pub use self::soap::{SphericalExpansionByPair, SphericalExpansionParameters}; pub use self::soap::SphericalExpansion; -pub use self::soap::{SoapPowerSpectrum, PowerSpectrumParameters}; pub use self::soap::{SoapRadialSpectrum, RadialSpectrumParameters}; +pub use self::soap::{SoapPowerSpectrum, PowerSpectrumParameters}; pub mod lode; pub use self::lode::{LodeSphericalExpansion, LodeSphericalExpansionParameters}; + +mod bondatom; +pub use self::bondatom::{SphericalExpansionForBondsParameters, SphericalExpansionForBonds}; diff --git a/rascaline/src/calculators/neighbor_list.rs b/featomic/src/calculators/neighbor_list.rs similarity index 97% rename from rascaline/src/calculators/neighbor_list.rs rename to featomic/src/calculators/neighbor_list.rs index 668d96424..5fc5dbadf 100644 --- a/rascaline/src/calculators/neighbor_list.rs +++ b/featomic/src/calculators/neighbor_list.rs @@ -65,7 +65,7 @@ impl CalculatorBase for NeighborList { std::slice::from_ref(&self.cutoff) } - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { assert!(self.cutoff > 0.0 && self.cutoff.is_finite()); if self.full_neighbor_list { @@ -85,7 +85,7 @@ impl CalculatorBase for NeighborList { return vec!["system", "first_atom", "second_atom", "cell_shift_a", "cell_shift_b", "cell_shift_c"]; } - fn samples(&self, keys: &Labels, systems: &mut [Box]) -> Result, Error> { + fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error> { assert!(self.cutoff > 0.0 && self.cutoff.is_finite()); if self.full_neighbor_list { @@ -109,7 +109,7 @@ impl CalculatorBase for NeighborList { } } - fn positions_gradient_samples(&self, _keys: &Labels, samples: &[Labels], _systems: &mut [Box]) -> Result, Error> { + fn positions_gradient_samples(&self, _keys: &Labels, samples: &[Labels], _systems: &mut [System]) -> Result, Error> { let mut results = Vec::new(); for block_samples in samples { @@ -147,7 +147,7 @@ impl CalculatorBase for NeighborList { } #[time_graph::instrument(name = "NeighborList::compute")] - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { if self.full_neighbor_list { FullNeighborList { cutoff: self.cutoff, @@ -171,7 +171,7 @@ struct HalfNeighborList { } impl HalfNeighborList { - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { let mut all_types_pairs = BTreeSet::new(); for system in systems { system.compute_neighbors(self.cutoff)?; @@ -199,7 +199,7 @@ impl HalfNeighborList { return Ok(keys.finish()); } - fn samples(&self, keys: &Labels, systems: &mut [Box]) -> Result, Error> { + fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error> { let mut results = Vec::new(); for [first_atom_type, second_atom_type] in keys.iter_fixed_size() { @@ -267,7 +267,7 @@ impl HalfNeighborList { return Ok(results); } - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { for (system_i, system) in systems.iter_mut().enumerate() { system.compute_neighbors(self.cutoff)?; let types = system.types()?; @@ -372,7 +372,7 @@ pub struct FullNeighborList { impl FullNeighborList { /// Get the list of keys for these systems (list of pair types present in the systems) - pub(crate) fn keys(&self, systems: &mut [Box]) -> Result { + pub(crate) fn keys(&self, systems: &mut [System]) -> Result { let mut all_types_pairs = BTreeSet::new(); for system in systems { system.compute_neighbors(self.cutoff)?; @@ -400,7 +400,7 @@ impl FullNeighborList { return Ok(keys.finish()); } - pub(crate) fn samples(&self, keys: &Labels, systems: &mut [Box]) -> Result, Error> { + pub(crate) fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error> { let mut results = Vec::new(); for &[first_atom_type, second_atom_type] in keys.iter_fixed_size() { @@ -492,7 +492,7 @@ impl FullNeighborList { } #[allow(clippy::too_many_lines)] - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { for (system_i, system) in systems.iter_mut().enumerate() { system.compute_neighbors(self.cutoff)?; let types = system.types()?; diff --git a/featomic/src/calculators/shared/basis/mod.rs b/featomic/src/calculators/shared/basis/mod.rs new file mode 100644 index 000000000..245ce5ce1 --- /dev/null +++ b/featomic/src/calculators/shared/basis/mod.rs @@ -0,0 +1,116 @@ +pub(crate) mod radial; + +use std::collections::BTreeMap; + +pub use self::radial::{SoapRadialBasis, LodeRadialBasis}; + +/// Possible Basis functions to use for the SOAP or LODE spherical expansion. +/// +/// The basis is made of radial and angular parts, that can be combined in +/// various ways. +#[derive(Debug, Clone)] +#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +#[serde(deny_unknown_fields)] +#[serde(tag = "type")] +pub enum SphericalExpansionBasis { + /// This defines a tensor product basis, combining all possible radial basis + /// functions with all possible angular basis functions. + TensorProduct(TensorProductBasis), + /// This defines an explicit basis, where only a specific subset of angular + /// basis can be used, and every angular basis can use a different radial + /// basis. + Explicit(ExplicitBasis), +} + +impl SphericalExpansionBasis { + pub fn angular_channels(&self) -> Vec { + match self { + SphericalExpansionBasis::TensorProduct(basis) => { + return (0..=basis.max_angular).collect(); + } + SphericalExpansionBasis::Explicit(basis) => { + return basis.by_angular.keys().copied().collect(); + } + } + } +} + +#[allow(clippy::unnecessary_wraps)] +fn serde_default_spline_accuracy() -> Option { Some(1e-8) } + +/// Information about "tensor product" spherical expansion basis functions +#[derive(Debug, Clone)] +#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct TensorProductBasis { + /// Maximal value (inclusive) of the angular moment (quantum number `l`) to + /// use for the spherical harmonics basis functions + pub max_angular: usize, + /// Definition of the radial basis functions + pub radial: RadialBasis, + /// Accuracy for splining the radial integral. Using splines is typically + /// faster than analytical implementations. If this is None, no splining is + /// done. + /// + /// The number of control points in the spline is automatically determined + /// to ensure the average absolute error is close to the requested accuracy. + #[serde(default = "serde_default_spline_accuracy")] + pub spline_accuracy: Option, +} + +#[derive(Debug, Clone)] +#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +#[serde(deny_unknown_fields)] +// work around https://github.com/serde-rs/serde/issues/1183 +#[serde(try_from = "BTreeMap")] +pub struct ByAngular(BTreeMap); + +impl std::ops::Deref for ByAngular { + type Target = BTreeMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl TryFrom> for ByAngular { + type Error = ::Err; + + fn try_from(value: BTreeMap) -> Result { + let mut result = BTreeMap::new(); + for (angular, radial) in value { + let angular: usize = angular.parse()?; + result.insert(angular, radial); + } + Ok(ByAngular(result)) + } +} + +impl From> for ByAngular { + fn from(value: BTreeMap) -> Self { + ByAngular(value) + } +} + +/// Information about "explicit" spherical expansion basis functions +#[derive(Debug, Clone)] +#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct ExplicitBasis { + /// A map of radial basis to use for the specified angular channels. + /// + /// Only angular channels included in this map will be included in the + /// output. Different angular channels are allowed to use completely + /// different radial basis functions. + #[schemars(extend("x-key-type" = "integer"))] + #[schemars(with = "BTreeMap")] + pub by_angular: ByAngular, + /// Accuracy for splining the radial integral. Using splines is typically + /// faster than analytical implementations. If this is None, no splining is + /// done. + /// + /// The number of control points in the spline is automatically determined + /// to ensure the average absolute error is close to the requested accuracy. + #[serde(default = "serde_default_spline_accuracy")] + pub spline_accuracy: Option, +} diff --git a/rascaline/src/calculators/radial_basis/gto.rs b/featomic/src/calculators/shared/basis/radial/gto.rs similarity index 79% rename from rascaline/src/calculators/radial_basis/gto.rs rename to featomic/src/calculators/shared/basis/radial/gto.rs index 2279fd90e..55a45b72a 100644 --- a/rascaline/src/calculators/radial_basis/gto.rs +++ b/featomic/src/calculators/shared/basis/radial/gto.rs @@ -2,28 +2,26 @@ use ndarray::{Array1, Array2}; use crate::math::gamma; -#[derive(Debug, Clone, Copy)] -#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] /// Use a radial basis similar to Gaussian-Type Orbitals. /// /// The basis is defined as `R_n(r) ∝ r^n e^{- r^2 / (2 σ_n^2)}`, where `σ_n /// = cutoff * \sqrt{n} / n_max` +#[derive(Debug, Clone, Copy)] pub struct GtoRadialBasis { - pub max_radial: usize, - pub cutoff: f64, + pub size: usize, + pub radius: f64, } impl GtoRadialBasis { /// Get the overlap matrix between non-orthonormalized GTO basis function pub fn overlap(&self) -> Array2 { let gaussian_widths = self.gaussian_widths(); - let n_max = self.max_radial; - let mut overlap = Array2::from_elem((n_max, n_max), 0.0); - for n1 in 0..n_max { + let mut overlap = Array2::from_elem((self.size, self.size), 0.0); + for n1 in 0..self.size { let sigma1 = gaussian_widths[n1]; let sigma1_sq = sigma1 * sigma1; - for n2 in n1..n_max { + for n2 in n1..self.size { let sigma2 = gaussian_widths[n2]; let sigma2_sq = sigma2 * sigma2; @@ -44,28 +42,28 @@ impl GtoRadialBasis { /// Get the vector of GTO Gaussian width, i.e. `cutoff * max(√n, 1) / n_max` pub fn gaussian_widths(&self) -> Vec { - return (0..self.max_radial).map(|n| { + return (0..self.size).map(|n| { let n = n as f64; - let n_max = self.max_radial as f64; - self.cutoff * f64::max(f64::sqrt(n), 1.0) / n_max + let n_max = self.size as f64; + self.radius * f64::max(f64::sqrt(n), 1.0) / n_max }).collect(); } /// Get the matrix to orthonormalize the GTO basis pub fn orthonormalization_matrix(&self) -> Array2 { let normalization = self.gaussian_widths().iter() - .zip(0..self.max_radial) + .zip(0..self.size) .map(|(sigma, n)| f64::sqrt(2.0 / (sigma.powi(2 * n as i32 + 3) * gamma(n as f64 + 1.5)))) .collect::>(); let overlap = self.overlap(); // compute overlap^-1/2 through its eigendecomposition let mut eigen = crate::math::SymmetricEigen::new(overlap); - for n in 0..self.max_radial { + for n in 0..self.size { if eigen.eigenvalues[n] <= f64::EPSILON { panic!( "radial overlap matrix is singular, try with a lower \ - max_radial (current value is {})", self.max_radial + max_radial (current value is {})", self.size - 1 ); } eigen.eigenvalues[n] = 1.0 / f64::sqrt(eigen.eigenvalues[n]); @@ -87,14 +85,14 @@ mod tests { fn gto_overlap() { // some basic sanity checks on the overlap matrix let basis = GtoRadialBasis { - max_radial: 8, - cutoff: 6.3, + size: 8, + radius: 6.3, }; let overlap = basis.overlap(); - for i in 0..basis.max_radial { - for j in 0..basis.max_radial { + for i in 0..basis.size { + for j in 0..basis.size { if i == j { assert_ulps_eq!(overlap[(i, j)], 1.0, max_ulps=10); } else { diff --git a/featomic/src/calculators/shared/basis/radial/mod.rs b/featomic/src/calculators/shared/basis/radial/mod.rs new file mode 100644 index 000000000..7df0f9282 --- /dev/null +++ b/featomic/src/calculators/shared/basis/radial/mod.rs @@ -0,0 +1,88 @@ +mod gto; +pub use self::gto::GtoRadialBasis; + +mod tabulated; +pub use self::tabulated::{Tabulated, LodeTabulated}; + + +/// The different kinds of radial basis supported by SOAP calculators +#[derive(Debug, Clone)] +#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +#[serde(deny_unknown_fields)] +#[serde(tag = "type")] +pub enum SoapRadialBasis { + /// Use a radial basis similar to Gaussian-Type Orbitals. + /// + /// The basis is defined as `R_n(r) ∝ r^n e^{- r^2 / (2 σ_n^2)}`, where `σ_n + /// = cutoff * \sqrt{n} / n_max` + Gto { + /// Maximal value of `n` to include in the radial basis function + /// definition. The overall basis will have `max_radial + 1` basis + /// functions, indexed from `0` to `max_radial` (inclusive). + max_radial: usize, + + #[doc(hidden)] + /// + radius: Option, + }, + /// Use pre-tabulated radial basis. + /// + /// This enables the calculation of the overall radial integral using + /// user-defined splines. + /// + /// The easiest way to create such a tabulated basis is the corresponding + /// functions in featomic's Python API. + #[schemars(with = "tabulated::TabulatedSerde")] + Tabulated(Tabulated) +} + +impl SoapRadialBasis { + /// Get the size (number of basis function) for the current basis + pub fn size(&self) -> usize { + match self { + SoapRadialBasis::Gto { max_radial, .. } => max_radial + 1, + SoapRadialBasis::Tabulated(tabulated) => tabulated.size(), + } + } +} + + +/// The different kinds of radial basis supported LODE calculators +#[derive(Debug, Clone)] +#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +#[serde(deny_unknown_fields)] +#[serde(tag = "type")] +pub enum LodeRadialBasis { + /// Use a radial basis similar to Gaussian-Type Orbitals. + /// + /// The basis is defined as `R_n(r) ∝ r^n e^{- r^2 / (2 σ_n^2)}`, where `σ_n + /// = radius * \sqrt{n} / n_max` + Gto { + /// Maximal value of `n` to include in the radial basis function + /// definition. The overall basis will have `max_radial + 1` basis + /// functions, indexed from `0` to `max_radial` (inclusive). + max_radial: usize, + /// Radius of the Gto basis, i.e. how far should the local LODE field be + /// integrated. + radius: f64, + }, + /// Use pre-tabulated radial basis. + /// + /// This enables the calculation of the overall radial integral using + /// user-defined splines. + /// + /// The easiest way to create such a tabulated basis is the corresponding + /// functions in featomic's Python API. + #[schemars(with = "tabulated::LodeTabulatedSerde")] + Tabulated(LodeTabulated) +} + +impl LodeRadialBasis { + /// Get the size (number of basis function) for the current basis + pub fn size(&self) -> usize { + match self { + LodeRadialBasis::Gto { max_radial, .. } => max_radial + 1, + LodeRadialBasis::Tabulated(tabulated) => tabulated.size(), + } + } +} diff --git a/featomic/src/calculators/shared/basis/radial/tabulated.rs b/featomic/src/calculators/shared/basis/radial/tabulated.rs new file mode 100644 index 000000000..506a3de20 --- /dev/null +++ b/featomic/src/calculators/shared/basis/radial/tabulated.rs @@ -0,0 +1,210 @@ +use std::sync::Arc; + +use ndarray::Array1; + +use crate::math::{HermitCubicSpline, HermitSplinePoint, SplineParameters}; +use crate::Error; + +/// A tabulated radial basis. +#[derive(Debug, Clone)] +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(try_from = "TabulatedSerde")] +#[serde(into = "TabulatedSerde")] +pub struct Tabulated { + pub(crate) spline: Arc>, +} + +impl Tabulated { + /// Get the size of the tabulated functions (i.e. how many functions are + /// simultaneously tabulated). + pub fn size(&self) -> usize { + return self.spline.shape()[0] + } +} + +/// A tabulated radial basis for LODE +#[derive(Debug, Clone)] +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(try_from = "LodeTabulatedSerde")] +#[serde(into = "LodeTabulatedSerde")] +pub struct LodeTabulated { + pub(crate) spline: Arc>, + pub(crate) center_contribution: Option>, +} + +impl LodeTabulated { + /// Get the size of the tabulated functions (i.e. how many functions are + /// simultaneously tabulated). + pub fn size(&self) -> usize { + return self.spline.shape()[0] + } +} + +/// Serde-compatible struct, used to serialize/deserialize splines +#[derive(Debug, Clone)] +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct TabulatedSerde { + /// Points defining the spline + pub points: Vec, +} + +/// Serde-compatible struct, used to serialize/deserialize splines +#[derive(Debug, Clone)] +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct LodeTabulatedSerde { + /// Points defining the spline + pub points: Vec, + /// The `center_contribution` is defined as `c_n = \sqrt{4π} \int dr r^2 + /// R_n(r) g(r)` where `g(r)` is a radially symmetric density function, + /// `R_n(r)` the radial basis function and `n` the current radial channel. + /// + /// It is required for using tabulated basis with LODE, since we can not + /// compute it using only the spline. This should be defined for the + /// `lambda=0` angular channel. + pub center_contribution: Option>, +} + +/// A single point entering a spline used for the tabulated radial integrals. +#[derive(Debug, Clone)] +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct SplinePoint { + /// Position of the point + pub position: f64, + /// Array of values for the tabulated radial integral + pub values: Vec, + /// Array of derivatives for the tabulated radial integral + pub derivatives: Vec, +} + +impl TryFrom for Tabulated { + type Error = Error; + + fn try_from(tabulated: TabulatedSerde) -> Result { + let spline = spline_from_tabulated(tabulated.points)?; + return Ok(Tabulated { spline }); + } +} + +impl From for TabulatedSerde { + fn from(tabulated: Tabulated) -> TabulatedSerde { + let spline = &tabulated.spline; + + let mut points = Vec::new(); + for point in &spline.points { + points.push(SplinePoint { + position: point.position, + values: point.values.to_vec(), + derivatives: point.derivatives.to_vec(), + }); + } + + return TabulatedSerde { points }; + } +} + + +impl TryFrom for LodeTabulated { + type Error = Error; + + fn try_from(tabulated: LodeTabulatedSerde) -> Result { + let spline = spline_from_tabulated(tabulated.points)?; + + let mut center_contribution = None; + if let Some(vector) = tabulated.center_contribution { + if vector.len() != spline.shape()[0] { + return Err(Error::InvalidParameter(format!( + "expected the 'center_contribution' in 'Tabulated' \ + radial basis to have the same number of basis function as \ + the spline, got {} and {}", + vector.len(), spline.shape()[0] + ))); + } + center_contribution = Some(Array1::from(vector)); + } + + return Ok(LodeTabulated { spline, center_contribution }); + } +} + +impl From for LodeTabulatedSerde { + fn from(tabulated: LodeTabulated) -> LodeTabulatedSerde { + let spline = &tabulated.spline; + + let mut points = Vec::new(); + for point in &spline.points { + points.push(SplinePoint { + position: point.position, + values: point.values.to_vec(), + derivatives: point.derivatives.to_vec(), + }); + } + + let center_contribution = tabulated.center_contribution.as_ref().map(Array1::to_vec); + return LodeTabulatedSerde { points, center_contribution }; + } +} + +fn spline_from_tabulated(points: Vec) -> Result>, Error> { + let points = check_spline_points(points)?; + + let spline_parameters = SplineParameters { + start: points[0].position, + stop: points[points.len() - 1].position, + shape: vec![points[0].values.len()], + }; + + let mut new_spline_points = Vec::new(); + for point in points { + new_spline_points.push( + HermitSplinePoint{ + position: point.position, + values: Array1::from(point.values), + derivatives: Array1::from(point.derivatives), + } + ); + } + let spline = Arc::new(HermitCubicSpline::new(spline_parameters, new_spline_points)); + + return Ok(spline); +} + +fn check_spline_points(mut points: Vec) -> Result, Error> { + if points.len() < 2 { + return Err(Error::InvalidParameter( + "we need at least two points to define a 'Tabulated' radial basis".into() + )); + } + let size = points[0].values.len(); + + for point in &points { + if !point.position.is_finite() { + return Err(Error::InvalidParameter(format!( + "expected all points 'position' in 'Tabulated' \ + radial basis to be finite numbers, got {}", + point.position + ))); + } + + if point.values.len() != size { + return Err(Error::InvalidParameter(format!( + "expected all points 'values' in 'Tabulated' \ + radial basis to have the same size, got {} and {}", + point.values.len(), size + ))); + } + + if point.derivatives.len() != size { + return Err(Error::InvalidParameter(format!( + "expected all points 'derivatives' in 'Tabulated' \ + radial basis to have the same size, got {} and {}", + point.derivatives.len(), size + ))); + } + } + + points.sort_unstable_by(|a, b| a.position.total_cmp(&b.position)); + return Ok(points); +} diff --git a/featomic/src/calculators/shared/density.rs b/featomic/src/calculators/shared/density.rs new file mode 100644 index 000000000..46edaeb95 --- /dev/null +++ b/featomic/src/calculators/shared/density.rs @@ -0,0 +1,129 @@ +use crate::Error; + + +/// Definition of the (atomic) density to expand on a basis +#[derive(Debug, Clone, Copy)] +#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +pub struct Density { + #[serde(flatten)] // because of this flatten, we can not use deny_unknown_fields + pub kind: DensityKind, + /// radial scaling can be used to reduce the importance of neighbor atoms + /// further away from the center, usually improving the performance of the + /// model + #[serde(default)] + pub scaling: Option, + /// Weight of the central atom contribution to the density. If `1` the + /// center atom contribution is weighted the same as any other contribution. + /// If `0` the central atom does not contribute to the density at all. + #[serde(default = "serde_default_center_atom_weight")] + pub center_atom_weight: f64, +} + +fn serde_default_center_atom_weight() -> f64 { + return 1.0; +} + + +/// Different available kinds of atomic density to use in featomic +#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +#[serde(tag = "type")] +pub enum DensityKind { + /// Dirac delta atomic density + DiracDelta, + /// Gaussian atomic density `exp(-r^2/width^2)` + Gaussian { + /// Width of the gaussian, the same width is used for all atoms + width: f64 + }, + /// Smeared power law density, that behaves like `1 / r^p` as `r` goes to + /// infinity, while removing any singularity at `r=0` and ensuring the + /// density is differentiable everywhere. + /// + /// The density functional form is `f(r) = 1 / Γ(p/2) * γ(p/2, r^2/(2 σ^2)) + /// / r^p`, with σ the smearing width, Γ the Gamma function and γ the lower + /// incomplete gamma function. + /// + /// For more information about the derivation of this density, see + /// and section D of the + /// supplementary information. + SmearedPowerLaw { + /// Smearing width of the density (`σ`) + smearing: f64, + /// Exponent of the density (`p`) + exponent: usize + }, +} + +/// Implemented options for radial scaling of the atomic density around an atom +#[derive(Debug, Clone, Copy)] +#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +#[serde(deny_unknown_fields)] +#[serde(tag = "type")] +pub enum DensityScaling { + /// Use a long-range algebraic decay and smooth behavior at `r → 0` as + /// introduced in : + /// `f(r) = rate / (rate + (r / scale) ^ exponent)` + Willatt2018 { + /// see in the formula + scale: f64, + /// see in the formula + rate: f64, + /// see in the formula + exponent: f64, + }, +} + + +impl DensityScaling { + pub fn validate(&self) -> Result<(), Error> { + match self { + DensityScaling::Willatt2018 { scale, rate, exponent } => { + if *scale <= 0.0 { + return Err(Error::InvalidParameter(format!( + "expected positive scale for Willatt2018 radial scaling function, got {}", + scale + ))); + } + + if *rate <= 0.0 { + return Err(Error::InvalidParameter(format!( + "expected positive rate for Willatt2018 radial scaling function, got {}", + rate + ))); + } + + if *exponent <= 0.0 { + return Err(Error::InvalidParameter(format!( + "expected positive exponent for Willatt2018 radial scaling function, got {}", + exponent + ))); + } + } + } + return Ok(()); + } + + /// Evaluate the radial scaling function at the distance `r` + pub fn compute(&self, r: f64) -> f64 { + match self { + DensityScaling::Willatt2018 { rate, scale, exponent } => { + rate / (rate + (r / scale).powf(*exponent)) + } + } + } + + /// Evaluate the gradient of the radial scaling function at the distance `r` + pub fn gradient(&self, r: f64) -> f64 { + match self { + DensityScaling::Willatt2018 { scale, rate, exponent } => { + let rs = r / scale; + let rs_m1 = rs.powf(exponent - 1.0); + let rs_m = rs * rs_m1; + let factor = - rate * exponent / scale; + + factor * rs_m1 / ((rate + rs_m) * (rate + rs_m)) + } + } + } +} diff --git a/rascaline/src/calculators/descriptors_by_systems.rs b/featomic/src/calculators/shared/descriptors_by_systems.rs similarity index 97% rename from rascaline/src/calculators/descriptors_by_systems.rs rename to featomic/src/calculators/shared/descriptors_by_systems.rs index 95e253ec6..1861e3a46 100644 --- a/rascaline/src/calculators/descriptors_by_systems.rs +++ b/featomic/src/calculators/shared/descriptors_by_systems.rs @@ -132,12 +132,13 @@ pub fn split_tensor_map_by_system(descriptor: &mut TensorMap, n_systems: usize) let mut system_per_sample = vec![LabelValue::new(-1); block_data.samples.count()]; let system_start = *system_end; - for (sample_i, &[system, atom]) in block_data.samples.iter_fixed_size().enumerate().skip(system_start) { + for (sample_i, sample_label) in block_data.samples.iter().enumerate().skip(system_start) { + let system = sample_label[0]; system_per_sample[sample_i] = system; if system.usize() == system_i { // this sample is part of to the current system - samples.add(&[system, atom]); + samples.add(sample_label); let new_sample = samples_mapping.len(); samples_mapping.insert(sample_i, new_sample); diff --git a/featomic/src/calculators/shared/mod.rs b/featomic/src/calculators/shared/mod.rs new file mode 100644 index 000000000..5bfa59a91 --- /dev/null +++ b/featomic/src/calculators/shared/mod.rs @@ -0,0 +1,12 @@ +//! This module contains type definition shared between the SOAP and LODE +//! spherical expansions: atomic density, radial and angular basis, as well as +//! some parallelization helpers. + +mod density; +pub use self::density::{Density, DensityKind, DensityScaling}; + +pub(crate) mod basis; +pub use self::basis::{SphericalExpansionBasis, TensorProductBasis, ExplicitBasis}; +pub use self::basis::{SoapRadialBasis, LodeRadialBasis}; + +pub mod descriptors_by_systems; diff --git a/featomic/src/calculators/soap/cutoff.rs b/featomic/src/calculators/soap/cutoff.rs new file mode 100644 index 000000000..de6f1e50a --- /dev/null +++ b/featomic/src/calculators/soap/cutoff.rs @@ -0,0 +1,113 @@ +use crate::Error; + +/// Definition of a local environment for SOAP calculations +#[derive(Debug, Clone, Copy)] +#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Cutoff { + /// Radius of the spherical cutoff to use for atomic environments + pub radius: f64, + /// Cutoff function used to smooth the behavior around the cutoff radius + pub smoothing: Smoothing, +} + +/// Possible values for the smoothing cutoff function +#[derive(Debug, Clone, Copy)] +#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +#[serde(deny_unknown_fields)] +#[serde(tag = "type")] +pub enum Smoothing { + /// Shifted cosine smoothing function + /// `f(r) = 1/2 * (1 + cos(π (r - cutoff + width) / width ))` + ShiftedCosine { + /// Width of the switching function + width: f64, + }, + /// Step smoothing function (i.e. no smoothing). This is 1 inside the cutoff + /// and 0 outside, with a sharp step at the boundary. + Step, +} + +impl Cutoff { + pub fn validate(&self) -> Result<(), Error> { + match self.smoothing { + Smoothing::Step => {}, + Smoothing::ShiftedCosine { width } => { + if width <= 0.0 || !width.is_finite() { + return Err(Error::InvalidParameter(format!( + "expected positive width for shifted cosine cutoff function, got {}", + width + ))); + } + } + } + return Ok(()); + } + + /// Evaluate the smoothing function at the distance `r` + pub fn smoothing(&self, r: f64) -> f64 { + match self.smoothing { + Smoothing::Step => { + if r >= self.radius { 0.0 } else { 1.0 } + }, + Smoothing::ShiftedCosine { width } => { + if r <= (self.radius - width) { + 1.0 + } else if r >= self.radius { + 0.0 + } else { + let s = std::f64::consts::PI * (r - self.radius + width) / width; + 0.5 * (1. + f64::cos(s)) + } + } + } + } + + /// Evaluate the gradient of the smoothing function at the distance `r` + pub fn smoothing_gradient(&self, r: f64) -> f64 { + match self.smoothing { + Smoothing::Step => 0.0, + Smoothing::ShiftedCosine { width } => { + if r <= (self.radius - width) || r >= self.radius { + 0.0 + } else { + let s = std::f64::consts::PI * (r - self.radius + width) / width; + return -0.5 * std::f64::consts::PI * f64::sin(s) / width; + } + } + } + } +} + + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn no_smoothing() { + let cutoff = Cutoff { radius: 4.0, smoothing: Smoothing::Step}; + + assert_eq!(cutoff.smoothing(2.0), 1.0); + assert_eq!(cutoff.smoothing(5.0), 0.0); + + assert_eq!(cutoff.smoothing_gradient(2.0), 0.0); + assert_eq!(cutoff.smoothing_gradient(5.0), 0.0); + } + + #[test] + fn shifted_cosine() { + let cutoff = Cutoff { radius: 4.0, smoothing: Smoothing::ShiftedCosine { width: 0.5 }}; + + assert_eq!(cutoff.smoothing(2.0), 1.0); + assert_eq!(cutoff.smoothing(3.5), 1.0); + assert_eq!(cutoff.smoothing(3.8), 0.34549150281252683); + assert_eq!(cutoff.smoothing(4.0), 0.0); + assert_eq!(cutoff.smoothing(5.0), 0.0); + + assert_eq!(cutoff.smoothing_gradient(2.0), 0.0); + assert_eq!(cutoff.smoothing_gradient(3.5), 0.0); + assert_eq!(cutoff.smoothing_gradient(3.8), -2.987832164741557); + assert_eq!(cutoff.smoothing_gradient(4.0), 0.0); + assert_eq!(cutoff.smoothing_gradient(5.0), 0.0); + } +} diff --git a/rascaline/src/calculators/soap/mod.rs b/featomic/src/calculators/soap/mod.rs similarity index 52% rename from rascaline/src/calculators/soap/mod.rs rename to featomic/src/calculators/soap/mod.rs index 8afa7d866..156b1f358 100644 --- a/rascaline/src/calculators/soap/mod.rs +++ b/featomic/src/calculators/soap/mod.rs @@ -1,13 +1,10 @@ -mod radial_integral; -pub use self::radial_integral::SoapRadialIntegral; -pub use self::radial_integral::{SoapRadialIntegralGto, SoapRadialIntegralGtoParameters}; -pub use self::radial_integral::{SoapRadialIntegralSpline, SoapRadialIntegralSplineParameters}; +mod cutoff; +pub use self::cutoff::Cutoff; +pub use self::cutoff::Smoothing; -pub use self::radial_integral::{SoapRadialIntegralCache, SoapRadialIntegralParameters}; -mod cutoff; -pub use self::cutoff::CutoffFunction; -pub use self::cutoff::RadialScaling; +mod radial_integral; +pub use self::radial_integral::{SoapRadialIntegral, SoapRadialIntegralCacheByAngular}; mod spherical_expansion_pair; pub use self::spherical_expansion_pair::{SphericalExpansionByPair, SphericalExpansionParameters}; @@ -15,8 +12,8 @@ pub use self::spherical_expansion_pair::{SphericalExpansionByPair, SphericalExpa mod spherical_expansion; pub use self::spherical_expansion::SphericalExpansion; -mod power_spectrum; -pub use self::power_spectrum::{SoapPowerSpectrum, PowerSpectrumParameters}; - mod radial_spectrum; pub use self::radial_spectrum::{SoapRadialSpectrum, RadialSpectrumParameters}; + +mod power_spectrum; +pub use self::power_spectrum::{SoapPowerSpectrum, PowerSpectrumParameters}; diff --git a/rascaline/src/calculators/soap/power_spectrum.rs b/featomic/src/calculators/soap/power_spectrum.rs similarity index 91% rename from rascaline/src/calculators/soap/power_spectrum.rs rename to featomic/src/calculators/soap/power_spectrum.rs index 33662f023..e4b14c08d 100644 --- a/rascaline/src/calculators/soap/power_spectrum.rs +++ b/featomic/src/calculators/soap/power_spectrum.rs @@ -9,9 +9,8 @@ use crate::calculators::CalculatorBase; use crate::{CalculationOptions, Calculator, LabelsSelection}; use crate::{Error, System}; -use super::SphericalExpansionParameters; -use super::{SphericalExpansion, CutoffFunction, RadialScaling}; -use crate::calculators::radial_basis::RadialBasis; +use super::{Cutoff, SphericalExpansionParameters, SphericalExpansion}; +use crate::calculators::shared::{Density, SoapRadialBasis, SphericalExpansionBasis}; use crate::labels::{AtomicTypeFilter, SamplesBuilder}; use crate::labels::AtomCenteredSamples; @@ -34,28 +33,13 @@ use crate::labels::{KeysBuilder, CenterTwoNeighborsTypesKeys}; #[derive(Debug, Clone)] #[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] pub struct PowerSpectrumParameters { - /// Spherical cutoff to use for atomic environments - pub cutoff: f64, - /// Number of radial basis function to use - pub max_radial: usize, - /// Number of spherical harmonics to use - pub max_angular: usize, - /// Width of the atom-centered gaussian creating the atomic density - pub atomic_gaussian_width: f64, - /// Weight of the central atom contribution to the - /// features. If `1.0` the center atom contribution is weighted the same - /// as any other contribution. If `0.0` the central atom does not - /// contribute to the features at all. - pub center_atom_weight: f64, - /// radial basis to use for the radial integral - pub radial_basis: RadialBasis, - /// cutoff function used to smooth the behavior around the cutoff radius - pub cutoff_function: CutoffFunction, - /// radial scaling can be used to reduce the importance of neighbor atoms - /// further away from the center, usually improving the performance of the - /// model - #[serde(default)] - pub radial_scaling: RadialScaling, + /// Definition of the atomic environment within a cutoff, and how + /// neighboring atoms enter and leave the environment. + pub cutoff: Cutoff, + /// Definition of the density arising from atoms in the local environment. + pub density: Density, + /// Definition of the basis functions used to expand the atomic density + pub basis: SphericalExpansionBasis, } /// Calculator implementing the Smooth Overlap of Atomic Position (SOAP) power @@ -75,13 +59,8 @@ impl SoapPowerSpectrum { pub fn new(parameters: PowerSpectrumParameters) -> Result { let expansion_parameters = SphericalExpansionParameters { cutoff: parameters.cutoff, - max_radial: parameters.max_radial, - max_angular: parameters.max_angular, - atomic_gaussian_width: parameters.atomic_gaussian_width, - center_atom_weight: parameters.center_atom_weight, - radial_basis: parameters.radial_basis.clone(), - cutoff_function: parameters.cutoff_function, - radial_scaling: parameters.radial_scaling, + density: parameters.density, + basis: parameters.basis.clone(), }; let spherical_expansion = SphericalExpansion::new(expansion_parameters)?; @@ -190,7 +169,7 @@ impl SoapPowerSpectrum { // selection let mut missing_keys = BTreeSet::new(); for &[center, neighbor_1, neighbor_2] in descriptor.keys().iter_fixed_size() { - for o3_lambda in 0..=(self.parameters.max_angular) { + for o3_lambda in self.parameters.basis.angular_channels() { if !requested_o3_lambda.contains(&o3_lambda) { missing_keys.insert([o3_lambda.into(), 1.into(), center, neighbor_1]); missing_keys.insert([o3_lambda.into(), 1.into(), center, neighbor_2]); @@ -441,9 +420,9 @@ impl CalculatorBase for SoapPowerSpectrum { self.spherical_expansion.cutoffs() } - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { let builder = CenterTwoNeighborsTypesKeys { - cutoff: self.parameters.cutoff, + cutoff: self.parameters.cutoff.radius, self_pairs: true, symmetric: true, }; @@ -454,13 +433,13 @@ impl CalculatorBase for SoapPowerSpectrum { AtomCenteredSamples::sample_names() } - fn samples(&self, keys: &metatensor::Labels, systems: &mut [Box]) -> Result, Error> { + fn samples(&self, keys: &metatensor::Labels, systems: &mut [System]) -> Result, Error> { assert_eq!(keys.names(), ["center_type", "neighbor_1_type", "neighbor_2_type"]); let mut result = Vec::new(); for [center_type, neighbor_1_type, neighbor_2_type] in keys.iter_fixed_size() { let builder = AtomCenteredSamples { - cutoff: self.parameters.cutoff, + cutoff: self.parameters.cutoff.radius, center_type: AtomicTypeFilter::Single(center_type.i32()), // we only want center with both neighbor types present neighbor_type: AtomicTypeFilter::AllOf( @@ -478,14 +457,14 @@ impl CalculatorBase for SoapPowerSpectrum { return Ok(result); } - fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [Box]) -> Result, Error> { + fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [System]) -> Result, Error> { assert_eq!(keys.names(), ["center_type", "neighbor_1_type", "neighbor_2_type"]); assert_eq!(keys.count(), samples.len()); let mut gradient_samples = Vec::new(); for ([center_type, neighbor_1_type, neighbor_2_type], samples) in keys.iter_fixed_size().zip(samples) { let builder = AtomCenteredSamples { - cutoff: self.parameters.cutoff, + cutoff: self.parameters.cutoff.radius, center_type: AtomicTypeFilter::Single(center_type.i32()), // gradients samples should contain either neighbor types neighbor_type: AtomicTypeFilter::OneOf(vec![ @@ -517,22 +496,37 @@ impl CalculatorBase for SoapPowerSpectrum { } fn properties(&self, keys: &metatensor::Labels) -> Vec { - let mut properties = LabelsBuilder::new(self.property_names()); - for l in 0..=self.parameters.max_angular { - for n1 in 0..self.parameters.max_radial { - for n2 in 0..self.parameters.max_radial { - properties.add(&[l, n1, n2]); + match self.parameters.basis { + SphericalExpansionBasis::TensorProduct(ref basis) => { + let mut properties = LabelsBuilder::new(self.property_names()); + for l in 0..=basis.max_angular { + for n1 in 0..basis.radial.size() { + for n2 in 0..basis.radial.size() { + properties.add(&[l, n1, n2]); + } + } } + + return vec![properties.finish(); keys.count()]; } - } - let properties = properties.finish(); + SphericalExpansionBasis::Explicit(ref basis) => { + let mut properties = LabelsBuilder::new(self.property_names()); + for (&l, radial) in &*basis.by_angular { + for n1 in 0..radial.size() { + for n2 in 0..radial.size() { + properties.add(&[l, n1, n2]); + } + } + } - return vec![properties; keys.count()]; + return vec![properties.finish(); keys.count()]; + }, + } } #[time_graph::instrument(name = "SoapPowerSpectrum::compute")] #[allow(clippy::too_many_lines)] - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { assert!(descriptor.keys().count() > 0); let mut gradients = Vec::new(); @@ -784,16 +778,31 @@ mod tests { use super::*; use crate::calculators::CalculatorBase; + use crate::calculators::soap::{Cutoff, Smoothing}; + use crate::calculators::shared::{Density, DensityKind}; + use crate::calculators::shared::{SoapRadialBasis, SphericalExpansionBasis, TensorProductBasis}; + + + fn basis() -> TensorProductBasis { + TensorProductBasis { + max_angular: 6, + radial: SoapRadialBasis::Gto { max_radial: 6, radius: None }, + spline_accuracy: Some(1e-8), + } + } + fn parameters() -> PowerSpectrumParameters { PowerSpectrumParameters { - cutoff: 8.0, - max_radial: 6, - max_angular: 6, - atomic_gaussian_width: 0.3, - center_atom_weight: 1.0, - radial_basis: RadialBasis::splined_gto(1e-8), - radial_scaling: RadialScaling::None {}, - cutoff_function: CutoffFunction::ShiftedCosine { width: 0.5 }, + cutoff: Cutoff { + radius: 8.0, + smoothing: Smoothing::ShiftedCosine { width: 0.5 } + }, + density: Density { + kind: DensityKind::Gaussian { width: 0.3 }, + scaling: None, + center_atom_weight: 1.0, + }, + basis: SphericalExpansionBasis::TensorProduct(basis()), } } @@ -828,7 +837,7 @@ mod tests { )); // exact values for power spectrum are regression-tested in - // `rascaline/tests/soap-power-spectrum.rs` + // `featomic/tests/soap-power-spectrum.rs` } #[test] @@ -984,16 +993,30 @@ mod tests { fn center_atom_weight() { let system = &mut test_systems(&["CH"]); - let mut parameters = parameters(); - parameters.cutoff = 0.5; - parameters.center_atom_weight = 1.0; + let mut parameters = PowerSpectrumParameters { + cutoff: Cutoff { + radius: 0.5, + smoothing: Smoothing::ShiftedCosine { width: 0.5 } + }, + density: Density { + kind: DensityKind::Gaussian { width: 0.3 }, + scaling: None, + center_atom_weight: 1.0, + }, + basis: SphericalExpansionBasis::TensorProduct( TensorProductBasis { + max_angular: 6, + radial: SoapRadialBasis::Gto { max_radial: 6, radius: None }, + spline_accuracy: Some(1e-8), + }), + }; + parameters.density.center_atom_weight = 1.0; let mut calculator = Calculator::from(Box::new( SoapPowerSpectrum::new(parameters.clone()).unwrap(), ) as Box); let descriptor = calculator.compute(system, Default::default()).unwrap(); - parameters.center_atom_weight = 0.5; + parameters.density.center_atom_weight = 0.5; let mut calculator = Calculator::from(Box::new( SoapPowerSpectrum::new(parameters).unwrap(), ) as Box); diff --git a/featomic/src/calculators/soap/radial_integral/gto.rs b/featomic/src/calculators/soap/radial_integral/gto.rs new file mode 100644 index 000000000..30f7bcbce --- /dev/null +++ b/featomic/src/calculators/soap/radial_integral/gto.rs @@ -0,0 +1,292 @@ +use std::f64; + +use ndarray::{Array2, ArrayViewMut1}; + +use crate::calculators::shared::basis::radial::GtoRadialBasis; +use crate::calculators::shared::{DensityKind, SoapRadialBasis}; +use crate::math::{gamma, hyp1f1}; +use crate::Error; + +use super::SoapRadialIntegral; + +/// Implementation of the radial integral for GTO radial basis and gaussian +/// atomic density. +#[derive(Debug, Clone)] +pub struct SoapRadialIntegralGto { + /// Which value of l/lambda is this radial integral for + o3_lambda: usize, + /// `σ^2`, with σ the atomic density gaussian width + atomic_gaussian_width_2: f64, + /// `1/2σ^2`, with σ the atomic density gaussian width + atomic_gaussian_constant: f64, + /// `1/2σ_n^2`, with `σ_n` the GTO gaussian width, i.e. `cutoff * max(√n, 1) + /// / n_max` + gto_gaussian_constants: Vec, + /// `n_max * n_max` matrix to orthonormalize the GTO + gto_orthonormalization: Array2, +} + + +impl SoapRadialIntegralGto { + /// Create a new SOAP radial integral + pub fn new(cutoff: f64, density: DensityKind, basis: &SoapRadialBasis, o3_lambda: usize) -> Result { + let gaussian_width = if let DensityKind::Gaussian { width } = density { + width + } else { + return Err(Error::Internal("density must be Gaussian for the GTO radial integral".into())); + }; + + let &max_radial = if let SoapRadialBasis::Gto { max_radial, radius } = basis { + if let Some(radius) = radius { + #[allow(clippy::float_cmp)] + if *radius != cutoff { + return Err(Error::Internal( + "GTO radius must be the same as the cutoff radius in SOAP, \ + or should not provided".into() + )); + } + } + max_radial + } else { + return Err(Error::Internal("radial basis must be GTO for the GTO radial integral".into())); + }; + + // these should be checked before we reach this function + assert!(gaussian_width > 1e-16 && gaussian_width.is_finite()); + + let basis = GtoRadialBasis { + size: max_radial + 1, + radius: cutoff, + }; + let gto_gaussian_widths = basis.gaussian_widths(); + let gto_orthonormalization = basis.orthonormalization_matrix(); + + let gto_gaussian_constants = gto_gaussian_widths.iter() + .map(|&sigma| 1.0 / (2.0 * sigma * sigma)) + .collect::>(); + + let atomic_gaussian_width_2 = gaussian_width * gaussian_width; + let atomic_gaussian_constant = 1.0 / (2.0 * atomic_gaussian_width_2); + + return Ok(SoapRadialIntegralGto { + o3_lambda: o3_lambda, + atomic_gaussian_width_2: atomic_gaussian_width_2, + atomic_gaussian_constant: atomic_gaussian_constant, + gto_gaussian_constants: gto_gaussian_constants, + gto_orthonormalization: gto_orthonormalization.t().to_owned(), + }) + } +} + + +#[inline] +fn hyp1f1_derivative(a: f64, b: f64, x: f64) -> f64 { + a / b * hyp1f1(a + 1.0, b + 1.0, x) +} + +#[inline] +#[allow(clippy::many_single_char_names)] +/// Compute `G(a, b, z) = Gamma(a) / Gamma(b) 1F1(a, b, z)` for +/// `a = 1/2 (n + l + 3)` and `b = l + 3/2`. +/// +/// This is similar (but not the exact same) to the G function defined in +/// appendix A in . +/// +/// The function is called "double regularized 1F1" by reference to the +/// "regularized 1F1" function (i.e. `1F1(a, b, z) / Gamma(b)`) +fn double_regularized_1f1(l: usize, n: usize, z: f64, value: &mut f64, gradient: Option<&mut f64>) { + let (a, b) = (0.5 * (n + l + 3) as f64, l as f64 + 1.5); + let ratio = gamma(a) / gamma(b); + + *value = ratio * hyp1f1(a, b, z); + if let Some(gradient) = gradient { + *gradient = ratio * hyp1f1_derivative(a, b, z); + } +} + + + +impl SoapRadialIntegral for SoapRadialIntegralGto { + fn size(&self) -> usize { + self.gto_gaussian_constants.len() + } + + #[time_graph::instrument(name = "GtoRadialIntegral::compute")] + fn compute( + &self, + distance: f64, + mut values: ArrayViewMut1, + mut gradients: Option> + ) { + assert_eq!( + values.shape(), [self.size()], + "wrong size for values array, expected [{}] but got [{}]", + self.size(), values.shape()[0] + ); + + if let Some(ref gradients) = gradients { + assert_eq!( + gradients.shape(), [self.size()], + "wrong size for gradients array, expected [{}] but got [{}]", + self.size(), gradients.shape()[0] + ); + } + + // Define global factor of radial integral arising from three parts: + // - a global 4 pi factor coming from integration of the angular part of + // the radial integral (see the docs for `SoapRadialIntegral`) + // - a global factor of sqrt(pi)/4 from the calculation of the integral + // of GTO basis functions and gaussian density + // - the normalization constant of the atomic Gaussian density. We use a + // factor of 1/(pi*sigma^2)^0.75 which leads to Gaussian densities + // that are normalized in the L2-sense, i.e. integral_{R^3} |g(r)|^2 + // d^3r = 1. + // + // These three factors simplify to (pi/sigma^2)^3/4 + let global_factor = (std::f64::consts::PI / self.atomic_gaussian_width_2).powf(0.75); + + let c = self.atomic_gaussian_constant; + let c_rij = c * distance; + let c_rij_l = c_rij.powi(self.o3_lambda as i32); + let exp_c_rij = f64::exp(-distance * c_rij); + + // `global_factor * exp(-c rij^2) * (c * rij)^l` + let factor = global_factor * exp_c_rij * c_rij_l; + + for n in 0..self.size() { + let gto_constant = self.gto_gaussian_constants[n]; + + let z = c_rij * c_rij / (c + gto_constant); + // Calculate Gamma(a) / Gamma(b) 1F1(a, b, z) + double_regularized_1f1(self.o3_lambda, n, z, &mut values[n], gradients.as_mut().map(|g| &mut g[n])); + if !values[n].is_finite() { + panic!( + "Failed to compute radial integral with GTO basis. \ + Try increasing decreasing the `cutoff`, or increasing \ + the Gaussian's `width`." + ); + } + + let n_l_3_over_2 = 0.5 * (n + self.o3_lambda) as f64 + 1.5; + let c_dn = (c + gto_constant).powf(-n_l_3_over_2); + + values[n] *= c_dn * factor; + if let Some(ref mut gradients) = gradients { + gradients[n] *= c_dn * factor * 2.0 * z / distance; + gradients[n] += values[n] * (self.o3_lambda as f64 / distance - 2.0 * c_rij); + } + } + + // for r = 0, the formula used in the calculations above yield NaN, + // which in turns breaks the SplinedGto radial integral. From the + // analytical formula, the gradient is 0 everywhere expect for l=1 + if distance == 0.0 { + if let Some(ref mut gradients) = gradients { + if self.o3_lambda == 1 { + for n in 0..self.size() { + let gto_constant = self.gto_gaussian_constants[n]; + let a = 0.5 * (n + self.o3_lambda) as f64 + 1.5; + let b = 2.5; + let c_dn = (c + gto_constant).powf(-a); + let factor = global_factor * c * c_dn; + + gradients[n] = gamma(a) / gamma(b) * factor; + } + } else { + gradients.fill(0.0); + } + } + } + + values.assign(&values.dot(&self.gto_orthonormalization)); + if let Some(ref mut gradients) = gradients { + gradients.assign(&gradients.dot(&self.gto_orthonormalization)); + } + } +} + +#[cfg(test)] +mod tests { + use approx::assert_relative_eq; + + use super::*; + use super::super::SoapRadialIntegral; + use ndarray::Array1; + + #[test] + #[should_panic = "radial overlap matrix is singular, try with a lower max_radial (current value is 30)"] + fn ill_conditioned_orthonormalization() { + let density = DensityKind::Gaussian { width: 0.4 }; + let basis = SoapRadialBasis::Gto { max_radial: 30, radius: None }; + SoapRadialIntegralGto::new(5.0, density, &basis, 0).unwrap(); + } + + #[test] + #[should_panic = "wrong size for values array, expected [4] but got [3]"] + fn values_array_size() { + let density = DensityKind::Gaussian { width: 0.4 }; + let basis = SoapRadialBasis::Gto { max_radial: 3, radius: None }; + let gto = SoapRadialIntegralGto::new(5.0, density, &basis, 0).unwrap(); + let mut values = Array1::from_elem(3, 0.0); + + gto.compute(1.0, values.view_mut(), None); + } + + #[test] + #[should_panic = "wrong size for gradients array, expected [4] but got [3]"] + fn gradient_array_size() { + let density = DensityKind::Gaussian { width: 0.5 }; + let basis = SoapRadialBasis::Gto { max_radial: 3, radius: None }; + let gto = SoapRadialIntegralGto::new(5.0, density, &basis, 0).unwrap(); + + let mut values = Array1::from_elem(4, 0.0); + let mut gradients = Array1::from_elem(3, 0.0); + + gto.compute(1.0, values.view_mut(), Some(gradients.view_mut())); + } + + #[test] + fn gradients_near_zero() { + let density = DensityKind::Gaussian { width: 0.5 }; + let basis = SoapRadialBasis::Gto { max_radial: 7, radius: None }; + + for l in 0..4 { + let gto_ri = SoapRadialIntegralGto::new(3.4, density, &basis, l).unwrap(); + + let mut values = Array1::from_elem(8, 0.0); + let mut gradients = Array1::from_elem(8, 0.0); + let mut gradients_plus = Array1::from_elem(8, 0.0); + gto_ri.compute(0.0, values.view_mut(), Some(gradients.view_mut())); + gto_ri.compute(1e-12, values.view_mut(), Some(gradients_plus.view_mut())); + + assert_relative_eq!( + gradients, gradients_plus, epsilon=1e-11, max_relative=1e-6, + ); + } + } + + #[test] + fn finite_differences() { + let density = DensityKind::Gaussian { width: 0.5 }; + let basis = SoapRadialBasis::Gto { max_radial: 7, radius: None }; + + let x = 3.4; + let delta = 1e-9; + + for l in 0..=8 { + let gto_ri = SoapRadialIntegralGto::new(5.0, density, &basis, l).unwrap(); + + let mut values = Array1::from_elem(8, 0.0); + let mut values_delta = Array1::from_elem(8, 0.0); + let mut gradients = Array1::from_elem(8, 0.0); + gto_ri.compute(x, values.view_mut(), Some(gradients.view_mut())); + gto_ri.compute(x + delta, values_delta.view_mut(), None); + + let finite_differences = (&values_delta - &values) / delta; + + assert_relative_eq!( + finite_differences, gradients, max_relative=1e-4 + ); + } + } +} diff --git a/featomic/src/calculators/soap/radial_integral/mod.rs b/featomic/src/calculators/soap/radial_integral/mod.rs new file mode 100644 index 000000000..2c1492509 --- /dev/null +++ b/featomic/src/calculators/soap/radial_integral/mod.rs @@ -0,0 +1,199 @@ +use std::collections::BTreeMap; + +use ndarray::{Array1, ArrayViewMut1}; + +use crate::calculators::shared::DensityKind; +use crate::calculators::shared::{SphericalExpansionBasis, SoapRadialBasis}; +use crate::Error; + +/// A `SoapRadialIntegral` computes the SOAP radial integral for all radial +/// basis functions and a single spherical harmonic `l` channel +/// +/// See equations 5 to 8 of [this paper](https://doi.org/10.1063/5.0044689) for +/// mor information on the radial integral. +/// +/// `std::panic::RefUnwindSafe` is a required super-trait to enable passing +/// radial integrals across the C API. `Send` is a required super-trait to +/// enable passing radial integrals between threads. +#[allow(clippy::doc_markdown)] +pub trait SoapRadialIntegral: std::panic::RefUnwindSafe + Send { + /// Compute the radial integral for a single `distance` between two atoms + /// and store the resulting data in the `values` array. If `gradients` is + /// `Some`, also compute and store gradients there. + /// + /// The radial integral $I_{nl}$ is defined as "the non-spherical harmonics + /// part of the spherical expansion". Depending on the atomic density, + /// different expressions can be used. + /// + /// For a delta density, the radial integral is simply the radial basis + /// function $R_{nl}$ evaluated at the pair distance: + /// + /// $$ I_{nl}(r_{ij}) = R_{nl}(r_{ij}) $$ + /// + /// For a Gaussian atomic density with a width of $\sigma$, the radial + /// integral reduces to: + /// + /// $$ + /// I_{nl}(r_{ij}) = \frac{4\pi}{(\pi \sigma^2)^{3/4}} e^{-\frac{r_{ij}^2}{2\sigma^2}} + /// \int_0^\infty \mathrm{d}r r^2 R_{nl}(r) e^{-\frac{r^2}{2\sigma^2}} i_l\left(\frac{rr_{ij}}{\sigma^2}\right) + /// $$ + /// + /// where $i_l$ is the modified spherical Bessel function of the first kind + /// of order $l$. + /// + /// Finally, for an arbitrary spherically symmetric atomic density `g(r)`, + /// the radial integral is + /// + /// $$ + /// I_{nl}(r_{ij}) = 2\pi \int_0^\infty \mathrm{d}r r^2 R_{nl}(r) + /// \int_{-1}^1 \mathrm{d}u P_l(u) g(\sqrt{r^2+r_{ij}^2-2rr_{ij}u}) + /// $$ + /// + /// where $P_l$ is the l-th Legendre polynomial. + fn compute(&self, distance: f64, values: ArrayViewMut1, gradients: Option>); + + /// Get how many basis functions are part of this integral. This is the + /// shape to use for the `values` and `gradients` parameters to `compute`. + fn size(&self) -> usize; +} + +mod gto; +pub use self::gto::SoapRadialIntegralGto; + +mod spline; +pub use self::spline::SoapRadialIntegralSpline; + +/// Store together a radial integral implementation and cached allocation for +/// values/gradients. +pub struct SoapRadialIntegralCache { + /// Implementation of the radial integral + implementation: Box, + /// Cache for the radial integral values + pub(crate) values: Array1, + /// Cache for the radial integral gradient + pub(crate) gradients: Array1, +} + +impl SoapRadialIntegralCache { + fn new( + o3_lambda: usize, + radial: &SoapRadialBasis, + density: DensityKind, + cutoff: f64, + spline_accuracy: Option, + ) -> Result { + // We only support some specific combinations of density and basis + let implementation = match (density, radial) { + // Gaussian density + GTO basis + (DensityKind::Gaussian {..}, &SoapRadialBasis::Gto { .. }) => { + let gto = SoapRadialIntegralGto::new(cutoff, density, radial, o3_lambda)?; + + if let Some(accuracy) = spline_accuracy { + Box::new(SoapRadialIntegralSpline::with_accuracy( + gto, cutoff, accuracy + )?) + } else { + Box::new(gto) as Box + } + }, + // Dirac density + tabulated basis (also used for + // tabulated radial integral with a different density) + (DensityKind::DiracDelta, SoapRadialBasis::Tabulated(tabulated)) => { + Box::new(SoapRadialIntegralSpline::from_tabulated( + tabulated.clone() + )) as Box + } + // Everything else is an error + _ => { + return Err(Error::InvalidParameter( + "this combination of basis and density is not supported in SOAP".into() + )) + } + }; + + let size = implementation.size(); + let values = Array1::from_elem(size, 0.0); + let gradients = Array1::from_elem(size, 0.0); + + return Ok(SoapRadialIntegralCache { + implementation, + values, + gradients, + }); + } + + /// Run the calculation, the results are stored inside `self.values` and + /// `self.gradients` + pub fn compute(&mut self, distance: f64, do_gradients: bool) { + let gradient_view = if do_gradients { + Some(self.gradients.view_mut()) + } else { + None + }; + + self.implementation.compute(distance, self.values.view_mut(), gradient_view); + } +} + +/// Store all `SoapRadialIntegralCache` for different angular channels +pub struct SoapRadialIntegralCacheByAngular { + pub(crate) by_angular: BTreeMap, +} + +impl SoapRadialIntegralCacheByAngular { + /// Create a new `SoapRadialIntegralCacheByAngular` for the given radial basis & density + pub fn new( + cutoff: f64, + density: DensityKind, + basis: &SphericalExpansionBasis + ) -> Result { + match basis { + SphericalExpansionBasis::TensorProduct(basis) => { + let mut by_angular = BTreeMap::new(); + for o3_lambda in 0..=basis.max_angular { + let cache = SoapRadialIntegralCache::new( + o3_lambda, + &basis.radial, + density, + cutoff, + basis.spline_accuracy + )?; + by_angular.insert(o3_lambda, cache); + } + + return Ok(SoapRadialIntegralCacheByAngular { by_angular }); + } + SphericalExpansionBasis::Explicit(basis) => { + let mut by_angular = BTreeMap::new(); + for (&o3_lambda, radial) in &*basis.by_angular { + let cache = SoapRadialIntegralCache::new( + o3_lambda, + radial, + density, + cutoff, + basis.spline_accuracy + )?; + by_angular.insert(o3_lambda, cache); + } + return Ok(SoapRadialIntegralCacheByAngular { + by_angular + }); + } + } + } + + /// Run the calculation, the results are accessible with `get` + pub fn compute(&mut self, distance: f64, do_gradients: bool) { + self.by_angular.iter_mut().for_each(|(_, cache)| cache.compute(distance, do_gradients)); + } + + /// Get one of the individual cache, corresponding to the `o3_lambda` + /// angular channel + pub fn get(&self, o3_lambda: usize) -> Option<&SoapRadialIntegralCache> { + self.by_angular.get(&o3_lambda) + } + + pub(crate) fn get_mut(&mut self, o3_lambda: usize) -> Option<&mut SoapRadialIntegralCache> { + self.by_angular.get_mut(&o3_lambda) + } +} diff --git a/featomic/src/calculators/soap/radial_integral/spline.rs b/featomic/src/calculators/soap/radial_integral/spline.rs new file mode 100644 index 000000000..6b2d0b3e5 --- /dev/null +++ b/featomic/src/calculators/soap/radial_integral/spline.rs @@ -0,0 +1,121 @@ +use std::sync::Arc; + +use ndarray::{Array1, ArrayViewMut1}; + +use super::SoapRadialIntegral; +use crate::calculators::shared::basis::radial::Tabulated; +use crate::math::{HermitCubicSpline, SplineParameters}; +use crate::Error; + +/// `SoapRadialIntegralSpline` allows to evaluate another radial integral +/// implementation using [cubic Hermit spline][splines-wiki]. +/// +/// This can be much faster than using analytical radial integral +/// implementations. +/// +/// [splines-wiki]: https://en.wikipedia.org/wiki/Cubic_Hermite_spline +pub struct SoapRadialIntegralSpline { + spline: Arc>, +} + +impl SoapRadialIntegralSpline { + /// Create a new `SoapRadialIntegralSpline` taking values from the given + /// `radial_integral`. Points are added to the spline between 0 and `cutoff` + /// until the requested `accuracy` is reached. We consider that the accuracy + /// is reached when either the mean absolute error or the mean relative + /// error gets below the `accuracy` threshold. + #[time_graph::instrument(name = "SoapRadialIntegralSpline::with_accuracy")] + pub fn with_accuracy( + radial_integral: impl SoapRadialIntegral, + cutoff: f64, + accuracy: f64, + ) -> Result { + let size = radial_integral.size(); + let spline_parameters = SplineParameters { + start: 0.0, + stop: cutoff, + shape: vec![size], + }; + + let spline = HermitCubicSpline::with_accuracy( + accuracy, + spline_parameters, + |x| { + let mut values = Array1::from_elem(size, 0.0); + let mut derivatives = Array1::from_elem(size, 0.0); + radial_integral.compute(x, values.view_mut(), Some(derivatives.view_mut())); + (values, derivatives) + }, + )?; + + return Ok(SoapRadialIntegralSpline { spline: Arc::new(spline) }); + } + + /// Create a new `SoapRadialIntegralSpline` with user-defined spline points. + pub fn from_tabulated(tabulated: Tabulated) -> SoapRadialIntegralSpline { + return SoapRadialIntegralSpline { + spline: tabulated.spline + }; + } +} + +impl SoapRadialIntegral for SoapRadialIntegralSpline { + fn size(&self) -> usize { + self.spline.points[0].values.shape()[0] + } + + #[time_graph::instrument(name = "SplinedRadialIntegral::compute")] + fn compute(&self, x: f64, values: ArrayViewMut1, gradients: Option>) { + self.spline.compute(x, values, gradients); + } +} + +#[cfg(test)] +mod tests { + use approx::assert_relative_eq; + + use crate::calculators::shared::{DensityKind, SoapRadialBasis}; + + use super::*; + use super::super::SoapRadialIntegralGto; + + #[test] + fn high_accuracy() { + // Check that even with high accuracy and large domain MAX_SPLINE_SIZE + // is enough + let density = DensityKind::Gaussian { width: 0.5 }; + let basis = SoapRadialBasis::Gto { max_radial: 15, radius: None }; + let gto_ri = SoapRadialIntegralGto::new(12.0, density, &basis, 0).unwrap(); + + // this test only check that this code runs without crashing + SoapRadialIntegralSpline::with_accuracy(gto_ri, 12.0, 1e-10).unwrap(); + } + + #[test] + fn finite_difference() { + let density = DensityKind::Gaussian { width: 0.5 }; + let basis = SoapRadialBasis::Gto { max_radial: 8, radius: None }; + let gto_ri = SoapRadialIntegralGto::new(5.0, density, &basis, 0).unwrap(); + + // even with very bad accuracy, we want the gradients of the spline to + // match the values produces by the spline, and not necessarily the + // actual GTO gradients. + let spline = SoapRadialIntegralSpline::with_accuracy(gto_ri, 5.0, 1e-2).unwrap(); + + let x = 3.4; + let delta = 1e-9; + + let size = spline.size(); + let mut values = Array1::from_elem(size, 0.0); + let mut values_delta = Array1::from_elem(size, 0.0); + let mut gradients = Array1::from_elem(size, 0.0); + spline.compute(x, values.view_mut(), Some(gradients.view_mut())); + spline.compute(x + delta, values_delta.view_mut(), None); + + let finite_differences = (&values_delta - &values) / delta; + assert_relative_eq!( + finite_differences, gradients, + epsilon=delta, max_relative=1e-6 + ); + } +} diff --git a/rascaline/src/calculators/soap/radial_spectrum.rs b/featomic/src/calculators/soap/radial_spectrum.rs similarity index 81% rename from rascaline/src/calculators/soap/radial_spectrum.rs rename to featomic/src/calculators/soap/radial_spectrum.rs index 0670cd2ae..4614e77e6 100644 --- a/rascaline/src/calculators/soap/radial_spectrum.rs +++ b/featomic/src/calculators/soap/radial_spectrum.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use metatensor::{EmptyArray, TensorBlock, TensorMap}; use metatensor::{LabelValue, Labels, LabelsBuilder}; @@ -5,9 +7,14 @@ use crate::calculators::CalculatorBase; use crate::{CalculationOptions, Calculator, LabelsSelection}; use crate::{Error, System}; -use super::SphericalExpansionParameters; -use super::{CutoffFunction, RadialScaling, SphericalExpansion}; -use crate::calculators::radial_basis::RadialBasis; +use super::{Cutoff, SphericalExpansionParameters, SphericalExpansion}; +use crate::calculators::shared::{ + Density, + SoapRadialBasis, + SphericalExpansionBasis, + ExplicitBasis +}; + use crate::labels::AtomCenteredSamples; use crate::labels::{CenterSingleNeighborsTypesKeys, KeysBuilder}; @@ -25,28 +32,35 @@ use crate::labels::{SamplesBuilder, AtomicTypeFilter}; #[derive(Debug, Clone)] #[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] pub struct RadialSpectrumParameters { - /// Spherical cutoff to use for atomic environments - pub cutoff: f64, - /// Number of radial basis function to use - pub max_radial: usize, - /// Width of the atom-centered gaussian creating the atomic density - pub atomic_gaussian_width: f64, - /// Weight of the central atom contribution to the - /// features. If `1` the center atom contribution is weighted the same - /// as any other contribution. If `0` the central atom does not - /// contribute to the features at all. - pub center_atom_weight: f64, - /// radial basis to use for the radial integral - pub radial_basis: RadialBasis, - /// cutoff function used to smooth the behavior around the cutoff radius - pub cutoff_function: CutoffFunction, - /// radial scaling can be used to reduce the importance of neighbor atoms - /// further away from the center, usually improving the performance of the - /// model - #[serde(default)] - pub radial_scaling: RadialScaling, + /// Definition of the atomic environment within a cutoff, and how + /// neighboring atoms enter and leave the environment. + pub cutoff: Cutoff, + /// Definition of the density arising from atoms in the local environment. + pub density: Density, + /// Definition of the basis functions used to expand the atomic density + pub basis: RadialSpectrumBasis, +} + +/// Information about radial spectrum basis functions +#[derive(Debug, Clone)] +#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct RadialSpectrumBasis { + /// Definition of the radial basis functions + pub radial: SoapRadialBasis, + /// Accuracy for splining the radial integral. Using splines is typically + /// faster than analytical implementations. If this is None, no splining is + /// done. + /// + /// The number of control points in the spline is automatically determined + /// to ensure the average absolute error is close to the requested accuracy. + #[serde(default = "serde_default_spline_accuracy")] + pub spline_accuracy: Option, } +#[allow(clippy::unnecessary_wraps)] +fn serde_default_spline_accuracy() -> Option { Some(1e-8) } + /// Calculator implementing the Radial /// spectrum representation of atomistic systems. pub struct SoapRadialSpectrum { @@ -62,15 +76,17 @@ impl std::fmt::Debug for SoapRadialSpectrum { impl SoapRadialSpectrum { pub fn new(parameters: RadialSpectrumParameters) -> Result { + // radial spectrum only needs a single angular basis function + let mut by_angular = BTreeMap::new(); + by_angular.insert(0, parameters.basis.radial.clone()); + let expansion_parameters = SphericalExpansionParameters { cutoff: parameters.cutoff, - max_radial: parameters.max_radial, - max_angular: 0, - atomic_gaussian_width: parameters.atomic_gaussian_width, - center_atom_weight: parameters.center_atom_weight, - radial_basis: parameters.radial_basis.clone(), - cutoff_function: parameters.cutoff_function, - radial_scaling: parameters.radial_scaling, + density: parameters.density, + basis: SphericalExpansionBasis::Explicit(ExplicitBasis { + by_angular: by_angular.into(), + spline_accuracy: parameters.basis.spline_accuracy, + }) }; let spherical_expansion = SphericalExpansion::new(expansion_parameters)?; @@ -130,9 +146,9 @@ impl CalculatorBase for SoapRadialSpectrum { self.spherical_expansion.cutoffs() } - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { let builder = CenterSingleNeighborsTypesKeys { - cutoff: self.parameters.cutoff, + cutoff: self.parameters.cutoff.radius, self_pairs: true, }; return builder.keys(systems); @@ -145,13 +161,13 @@ impl CalculatorBase for SoapRadialSpectrum { fn samples( &self, keys: &metatensor::Labels, - systems: &mut [Box], + systems: &mut [System], ) -> Result, Error> { assert_eq!(keys.names(), ["center_type", "neighbor_type"]); let mut result = Vec::new(); for [center_type, neighbor_type] in keys.iter_fixed_size() { let builder = AtomCenteredSamples { - cutoff: self.parameters.cutoff, + cutoff: self.parameters.cutoff.radius, center_type: AtomicTypeFilter::Single(center_type.i32()), neighbor_type: AtomicTypeFilter::Single(neighbor_type.i32()), self_pairs: true, @@ -170,14 +186,14 @@ impl CalculatorBase for SoapRadialSpectrum { } } - fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [Box]) -> Result, Error> { + fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [System]) -> Result, Error> { assert_eq!(keys.names(), ["center_type", "neighbor_type"]); assert_eq!(keys.count(), samples.len()); let mut gradient_samples = Vec::new(); for ([center_type, neighbor_type], samples) in keys.iter_fixed_size().zip(samples) { let builder = AtomCenteredSamples { - cutoff: self.parameters.cutoff, + cutoff: self.parameters.cutoff.radius, center_type: AtomicTypeFilter::Single(center_type.i32()), neighbor_type: AtomicTypeFilter::Single(neighbor_type.i32()), self_pairs: true, @@ -199,7 +215,7 @@ impl CalculatorBase for SoapRadialSpectrum { fn properties(&self, keys: &metatensor::Labels) -> Vec { let mut properties = LabelsBuilder::new(self.property_names()); - for n in 0..self.parameters.max_radial { + for n in 0..self.parameters.basis.radial.size() { properties.add(&[n]); } let properties = properties.finish(); @@ -208,7 +224,7 @@ impl CalculatorBase for SoapRadialSpectrum { } #[time_graph::instrument(name = "SoapRadialSpectrum::compute")] - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { assert_eq!(descriptor.keys().names(), ["center_type", "neighbor_type"]); assert!(descriptor.keys().count() > 0); @@ -313,15 +329,28 @@ mod tests { use super::*; use crate::calculators::CalculatorBase; + use crate::calculators::soap::{Cutoff, Smoothing}; + use crate::calculators::shared::{Density, DensityKind}; + + fn basis() -> RadialSpectrumBasis { + RadialSpectrumBasis { + radial: SoapRadialBasis::Gto { max_radial: 5, radius: None }, + spline_accuracy: Some(1e-8), + } + } + fn parameters() -> RadialSpectrumParameters { RadialSpectrumParameters { - cutoff: 3.5, - max_radial: 6, - atomic_gaussian_width: 0.3, - center_atom_weight: 1.0, - radial_basis: RadialBasis::splined_gto(1e-8), - radial_scaling: RadialScaling::None {}, - cutoff_function: CutoffFunction::ShiftedCosine { width: 0.5 }, + cutoff: Cutoff { + radius: 3.5, + smoothing: Smoothing::ShiftedCosine { width: 0.5 } + }, + density: Density { + kind: DensityKind::Gaussian { width: 0.3 }, + scaling: None, + center_atom_weight: 1.0, + }, + basis: basis(), } } diff --git a/rascaline/src/calculators/soap/spherical_expansion.rs b/featomic/src/calculators/soap/spherical_expansion.rs similarity index 69% rename from rascaline/src/calculators/soap/spherical_expansion.rs rename to featomic/src/calculators/soap/spherical_expansion.rs index b703f833f..23e842ba1 100644 --- a/rascaline/src/calculators/soap/spherical_expansion.rs +++ b/featomic/src/calculators/soap/spherical_expansion.rs @@ -11,12 +11,13 @@ use crate::{Error, System}; use crate::labels::{SamplesBuilder, AtomicTypeFilter, AtomCenteredSamples}; use crate::labels::{KeysBuilder, CenterSingleNeighborsTypesKeys}; -use super::super::CalculatorBase; +use crate::calculators::{CalculatorBase,GradientsOptions}; use super::{SphericalExpansionByPair, SphericalExpansionParameters}; -use super::spherical_expansion_pair::{GradientsOptions, PairContribution}; +use super::spherical_expansion_pair::PairContribution; -use super::super::{split_tensor_map_by_system, array_mut_for_system}; +use crate::calculators::shared::SphericalExpansionBasis; +use super::super::shared::descriptors_by_systems::{array_mut_for_system, split_tensor_map_by_system}; /// The actual calculator used to compute SOAP spherical expansion coefficients @@ -31,8 +32,8 @@ pub struct SphericalExpansion { impl SphericalExpansion { /// Create a new `SphericalExpansion` calculator with the given parameters pub fn new(parameters: SphericalExpansionParameters) -> Result { - let m_1_pow_l = (0..=parameters.max_angular) - .map(|l| f64::powi(-1.0, l as i32)) + let max_angular = parameters.basis.angular_channels().into_iter().max().expect("there should be at least one angular channel"); + let m_1_pow_l = (0..=max_angular).map(|l| f64::powi(-1.0, l as i32)) .collect::>(); return Ok(SphericalExpansion { @@ -44,9 +45,13 @@ impl SphericalExpansion { /// Accumulate the self contribution to the spherical expansion /// coefficients, i.e. the contribution arising from the density of the /// center atom around itself. - fn do_self_contributions(&mut self, systems: &[Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn do_self_contributions(&mut self, systems: &[System], descriptor: &mut TensorMap) -> Result<(), Error> { debug_assert_eq!(descriptor.keys().names(), ["o3_lambda", "o3_sigma", "center_type", "neighbor_type"]); + if !self.by_pair.parameters.basis.angular_channels().contains(&0) { + // o3_lambda is not part of the output, skip self contributions + return Ok(()); + } let self_contribution = self.by_pair.self_contribution(); for (key, mut block) in descriptor { @@ -82,7 +87,7 @@ impl SphericalExpansion { } for (property_i, &[n]) in block.properties.iter_fixed_size().enumerate() { - array[[sample_i, 0, property_i]] += self_contribution.values[[0, n.usize()]]; + array[[sample_i, 0, property_i]] += self_contribution[n.usize()]; } } } @@ -95,7 +100,7 @@ impl SphericalExpansion { #[allow(clippy::too_many_lines)] fn accumulate_all_pairs( &self, - system: &dyn System, + system: &System, do_gradients: GradientsOptions, requested_atoms: &BTreeSet, ) -> Result { @@ -122,42 +127,56 @@ impl SphericalExpansion { }).collect::>(); - let max_angular = self.by_pair.parameters().max_angular; - let max_radial = self.by_pair.parameters().max_radial; - let mut contribution = PairContribution::new(max_radial, max_angular, do_gradients.any()); + let radial_sizes = match self.by_pair.parameters.basis { + SphericalExpansionBasis::TensorProduct(ref basis) => { + vec![basis.radial.size(); basis.max_angular + 1] + }, + SphericalExpansionBasis::Explicit(ref basis) => { + basis.by_angular.values().map(|radial| radial.size()).collect() + }, + }; + let angular_channels = self.by_pair.parameters.basis.angular_channels(); + + let mut contribution = PairContribution::new( + &angular_channels, + &radial_sizes, + do_gradients.any(), + ); - // total number of joined (l, m) indices - let lm_shape = (max_angular + 1) * (max_angular + 1); let mut result = PairAccumulationResult { - values: ndarray::Array4::from_elem( - (types_mapping.len(), requested_atoms.len(), lm_shape, max_radial), - 0.0 - ), + values: angular_channels.iter().zip(&radial_sizes).map(|(&o3_lambda, &radial_size)| { + let shape = (types_mapping.len(), requested_atoms.len(), 2 * o3_lambda + 1, radial_size); + (o3_lambda, ndarray::Array4::from_elem(shape, 0.0)) + }).collect(), positions_gradient_by_pair: if do_gradients.positions { - let shape = (pairs_count, 3, lm_shape, max_radial); - Some(ndarray::Array4::from_elem(shape, 0.0)) + Some(angular_channels.iter().zip(&radial_sizes).map(|(&o3_lambda, &radial_size)| { + let shape = (pairs_count, 3, 2 * o3_lambda + 1, radial_size); + (o3_lambda, ndarray::Array4::from_elem(shape, 0.0)) + }).collect()) } else { None }, self_positions_gradients: if do_gradients.positions { - let shape = (types_mapping.len(), requested_atoms.len(), 3, lm_shape, max_radial); - Some(ndarray::Array5::from_elem(shape, 0.0)) + Some(angular_channels.iter().zip(&radial_sizes).map(|(&o3_lambda, &radial_size)| { + let shape = (types_mapping.len(), requested_atoms.len(), 3, 2 * o3_lambda + 1, radial_size); + (o3_lambda, ndarray::Array5::from_elem(shape, 0.0)) + }).collect()) } else { None }, cell_gradients: if do_gradients.cell { - Some(ndarray::Array6::from_elem( - (types_mapping.len(), requested_atoms.len(), 3, 3, lm_shape, max_radial), - 0.0) - ) + Some(angular_channels.iter().zip(&radial_sizes).map(|(&o3_lambda, &radial_size)| { + let shape = (types_mapping.len(), requested_atoms.len(), 3, 3, 2 * o3_lambda + 1, radial_size); + (o3_lambda, ndarray::Array6::from_elem(shape, 0.0)) + }).collect()) } else { None }, strain_gradients: if do_gradients.strain { - Some(ndarray::Array6::from_elem( - (types_mapping.len(), requested_atoms.len(), 3, 3, lm_shape, max_radial), - 0.0) - ) + Some(angular_channels.iter().zip(&radial_sizes).map(|(&o3_lambda, &radial_size)| { + let shape = (types_mapping.len(), requested_atoms.len(), 3, 3, 2 * o3_lambda + 1, radial_size); + (o3_lambda, ndarray::Array6::from_elem(shape, 0.0)) + }).collect()) } else { None }, @@ -183,68 +202,69 @@ impl SphericalExpansion { .push(pair_id); let neighbor_type_i = result.types_mapping[&types[neighbor_i]]; - let mut values = result.values.slice_mut(s![neighbor_type_i, mapped_center, .., ..]); - values += &contribution.values; + for (o3_lambda, &radial_size) in angular_channels.iter().zip(&radial_sizes) { + let values = result.values.get_mut(o3_lambda).expect("missing o3_lambda"); + let mut values = values.slice_mut(s![neighbor_type_i, mapped_center, .., ..]); + values += contribution.values.get(o3_lambda).expect("missing o3_lambda"); - if let Some(ref contribution_gradients) = contribution.gradients { - if let Some(ref mut positions_gradients) = result.positions_gradient_by_pair { - let gradients = &mut positions_gradients.slice_mut(s![pair_id, .., .., ..]); - gradients.assign(contribution_gradients); - } + if let Some(ref contribution_gradients) = contribution.gradients { + let contribution_gradients = contribution_gradients.get(o3_lambda).expect("missing o3_lambda"); - if pair.first != pair.second { - if let Some(ref mut positions_gradients) = result.self_positions_gradients { - let mut gradients = positions_gradients.slice_mut(s![neighbor_type_i, mapped_center, .., .., ..]); - gradients -= contribution_gradients; + if let Some(ref mut positions_gradients) = result.positions_gradient_by_pair { + let positions_gradients = positions_gradients.get_mut(o3_lambda).expect("missing o3_lambda"); + let gradients = &mut positions_gradients.slice_mut(s![pair_id, .., .., ..]); + gradients.assign(contribution_gradients); + } + + if pair.first != pair.second { + if let Some(ref mut positions_gradients) = result.self_positions_gradients { + let positions_gradients = positions_gradients.get_mut(o3_lambda).expect("missing o3_lambda"); + let mut gradients = positions_gradients.slice_mut(s![neighbor_type_i, mapped_center, .., .., ..]); + gradients -= contribution_gradients; + } } - } - if let Some(ref mut cell_gradients) = result.cell_gradients { - let mut cell_gradients = cell_gradients.slice_mut( - s![neighbor_type_i, mapped_center, .., .., .., ..] - ); - - for abc in 0..3 { - let shift = pair.cell_shift_indices[abc] as f64; - for xyz in 0..3 { - let mut lm_index = 0; - for o3_lambda in 0..=max_angular { - for _m in 0..(2 * o3_lambda + 1) { - for n in 0..max_radial { + if let Some(ref mut cell_gradients) = result.cell_gradients { + let cell_gradients = cell_gradients.get_mut(o3_lambda).expect("missing o3_lambda"); + let mut cell_gradients = cell_gradients.slice_mut( + s![neighbor_type_i, mapped_center, .., .., .., ..] + ); + + for abc in 0..3 { + let shift = pair.cell_shift_indices[abc] as f64; + for xyz in 0..3 { + for m in 0..(2 * o3_lambda + 1) { + for n in 0..radial_size { // SAFETY: we are doing in-bounds access, and removing the bounds // checks is a significant speed-up for this code. The bounds are // still checked in debug mode unsafe { - let out = cell_gradients.uget_mut([abc, xyz, lm_index, n]); - *out += shift * contribution_gradients.uget([xyz, lm_index, n]); + let out = cell_gradients.uget_mut([abc, xyz, m, n]); + *out += shift * contribution_gradients.uget([xyz, m, n]); } } - lm_index += 1; } } } } - } - if let Some(ref mut strain_gradients) = result.strain_gradients { - let mut strain_gradients = strain_gradients.slice_mut( - s![neighbor_type_i, mapped_center, .., .., .., ..] - ); - - for xyz_1 in 0..3 { - for xyz_2 in 0..3 { - let mut lm_index = 0; - for o3_lambda in 0..=max_angular { - for _m in 0..(2 * o3_lambda + 1) { - for n in 0..max_radial { + if let Some(ref mut strain_gradients) = result.strain_gradients { + let strain_gradients = strain_gradients.get_mut(o3_lambda).expect("missing o3_lambda"); + let mut strain_gradients = strain_gradients.slice_mut( + s![neighbor_type_i, mapped_center, .., .., .., ..] + ); + + for xyz_1 in 0..3 { + for xyz_2 in 0..3 { + for m in 0..(2 * o3_lambda + 1) { + for n in 0..radial_size { // SAFETY: same as above unsafe { - let out = strain_gradients.uget_mut([xyz_1, xyz_2, lm_index, n]); - *out += pair.vector[xyz_1] * contribution_gradients.uget([xyz_2, lm_index, n]); + let out = strain_gradients.uget_mut([xyz_1, xyz_2, m, n]); + *out += pair.vector[xyz_1] * contribution_gradients.uget([xyz_2, m, n]); } } - lm_index += 1; } } } @@ -266,65 +286,65 @@ impl SphericalExpansion { let neighbor_type_i = result.types_mapping[&types[neighbor_i]]; - let mut values = result.values.slice_mut(s![neighbor_type_i, mapped_center, .., ..]); - values += &contribution.values; + for (o3_lambda, &radial_size) in angular_channels.iter().zip(&radial_sizes) { + let values = result.values.get_mut(o3_lambda).expect("missing o3_lambda"); + let mut values = values.slice_mut(s![neighbor_type_i, mapped_center, .., ..]); + values += contribution.values.get(o3_lambda).expect("missing o3_lambda"); + - if let Some(ref contribution_gradients) = contribution.gradients { - // we don't add second->first pair to positions_gradient_by_pair, - // instead handling this in position_gradients_to_metatensor + if let Some(ref contribution_gradients) = contribution.gradients { + let contribution_gradients = contribution_gradients.get(o3_lambda).expect("missing o3_lambda"); + // we don't add second->first pair to positions_gradient_by_pair, + // instead handling this in position_gradients_to_metatensor - if pair.first != pair.second { - if let Some(ref mut positions_gradients) = result.self_positions_gradients { - let mut gradients = positions_gradients.slice_mut(s![neighbor_type_i, mapped_center, .., .., ..]); - gradients -= contribution_gradients; + if pair.first != pair.second { + if let Some(ref mut positions_gradients) = result.self_positions_gradients { + let positions_gradients = positions_gradients.get_mut(o3_lambda).expect("missing o3_lambda"); + let mut gradients = positions_gradients.slice_mut(s![neighbor_type_i, mapped_center, .., .., ..]); + gradients -= contribution_gradients; + } } - } - if let Some(ref mut cell_gradients) = result.cell_gradients { - let mut cell_gradients = cell_gradients.slice_mut( - s![neighbor_type_i, mapped_center, .., .., .., ..] - ); - - for abc in 0..3 { - let shift = pair.cell_shift_indices[abc] as f64; - for xyz in 0..3 { - let mut lm_index = 0; - for o3_lambda in 0..=max_angular { - for _m in 0..(2 * o3_lambda + 1) { - for n in 0..max_radial { + if let Some(ref mut cell_gradients) = result.cell_gradients { + let cell_gradients = cell_gradients.get_mut(o3_lambda).expect("missing o3_lambda"); + let mut cell_gradients = cell_gradients.slice_mut( + s![neighbor_type_i, mapped_center, .., .., .., ..] + ); + + for abc in 0..3 { + let shift = pair.cell_shift_indices[abc] as f64; + for xyz in 0..3 { + for m in 0..(2 * o3_lambda + 1) { + for n in 0..radial_size { // SAFETY: we are doing in-bounds access, and removing the bounds // checks is a significant speed-up for this code. The bounds are // still checked in debug mode unsafe { - let out = cell_gradients.uget_mut([abc, xyz, lm_index, n]); - *out += -shift * contribution_gradients.uget([xyz, lm_index, n]); + let out = cell_gradients.uget_mut([abc, xyz, m, n]); + *out += -shift * contribution_gradients.uget([xyz, m, n]); } } - lm_index += 1; } } } } - } - if let Some(ref mut strain_gradients) = result.strain_gradients { - let mut strain_gradients = strain_gradients.slice_mut( - s![neighbor_type_i, mapped_center, .., .., .., ..] - ); - - for xyz_1 in 0..3 { - for xyz_2 in 0..3 { - let mut lm_index = 0; - for o3_lambda in 0..=max_angular { - for _m in 0..(2 * o3_lambda + 1) { - for n in 0..max_radial { + if let Some(ref mut strain_gradients) = result.strain_gradients { + let strain_gradients = strain_gradients.get_mut(o3_lambda).expect("missing o3_lambda"); + let mut strain_gradients = strain_gradients.slice_mut( + s![neighbor_type_i, mapped_center, .., .., .., ..] + ); + + for xyz_1 in 0..3 { + for xyz_2 in 0..3 { + for m in 0..(2 * o3_lambda + 1) { + for n in 0..radial_size { // SAFETY: as above unsafe { - let out = strain_gradients.uget_mut([xyz_1, xyz_2, lm_index, n]); - *out += -pair.vector[xyz_1] * contribution_gradients.uget([xyz_2, lm_index, n]); + let out = strain_gradients.uget_mut([xyz_1, xyz_2, m, n]); + *out += -pair.vector[xyz_1] * contribution_gradients.uget([xyz_2, m, n]); } } - lm_index += 1; } } } @@ -344,7 +364,7 @@ impl SphericalExpansion { &self, key: &[LabelValue], block: &mut TensorBlockRefMut, - system: &dyn System, + system: &System, result: &PairAccumulationResult, ) -> Result<(), Error> { let types = system.types()?; @@ -354,7 +374,6 @@ impl SphericalExpansion { let center_type = key[2]; let neighbor_type = key[3]; - let lm_start = o3_lambda * o3_lambda; let neighbor_type_i = if let Some(s) = result.types_mapping.get(&neighbor_type.i32()) { *s } else { @@ -364,6 +383,7 @@ impl SphericalExpansion { let block = block.data_mut(); let mut array = array_mut_for_system(block.values); + let values = result.values.get(&o3_lambda).expect("missing o3_lambda"); for (sample_i, [_, atom_i]) in block.samples.iter_fixed_size().enumerate() { // samples might contain entries for atoms that should not be part @@ -381,7 +401,7 @@ impl SphericalExpansion { // mode. unsafe { let out = array.uget_mut([sample_i, m, property_i]); - *out += *result.values.uget([neighbor_type_i, mapped_center, lm_start + m, n.usize()]); + *out += *values.uget([neighbor_type_i, mapped_center, m, n.usize()]); } } } @@ -407,7 +427,7 @@ impl SphericalExpansion { &self, key: &[LabelValue], block: &mut TensorBlockRefMut, - system: &dyn System, + system: &System, result: &PairAccumulationResult, ) -> Result<(), Error> { let positions_gradients = if let Some(ref data) = result.positions_gradient_by_pair { @@ -416,7 +436,6 @@ impl SphericalExpansion { // no positions gradients, return early return Ok(()); }; - let self_positions_gradients = result.self_positions_gradients.as_ref().expect("missing self gradients"); let o3_lambda = key[0].usize(); @@ -431,11 +450,13 @@ impl SphericalExpansion { return Ok(()); }; + let self_positions_gradients = self_positions_gradients.get(&o3_lambda).expect("missing o3_lambda"); + let positions_gradients = positions_gradients.get(&o3_lambda).expect("missing o3_lambda"); + let types = system.types()?; let pairs = system.pairs()?; let system_size = system.size()?; - let lm_start = o3_lambda * o3_lambda; let m_1_pow_l = self.m_1_pow_l[o3_lambda]; let values_samples = block.samples(); @@ -465,7 +486,7 @@ impl SphericalExpansion { unsafe { let out = array.uget_mut([grad_sample_i, xyz, m, property_i]); *out = *self_positions_gradients.uget( - [neighbor_type_i, mapped_center, xyz, lm_start + m, n.usize()] + [neighbor_type_i, mapped_center, xyz, m, n.usize()] ); } } @@ -491,7 +512,7 @@ impl SphericalExpansion { // SAFETY: same as above unsafe { let out = array.uget_mut([grad_sample_i, xyz, m, property_i]); - *out += factor * *positions_gradients.uget([pair_id, xyz, lm_start + m, n.usize()]); + *out += factor * *positions_gradients.uget([pair_id, xyz, m, n.usize()]); } } } @@ -511,10 +532,10 @@ impl SphericalExpansion { key: &[LabelValue], parameter: &str, block: &mut TensorBlockRefMut, - system: &dyn System, + system: &System, result: &PairAccumulationResult, ) -> Result<(), Error> { - let contributions = if parameter == "strain" { + let gradients = if parameter == "strain" { if let Some(ref data) = result.strain_gradients { data } else { @@ -539,7 +560,8 @@ impl SphericalExpansion { let center_type = key[2]; let neighbor_type = key[3]; - let lm_start = o3_lambda * o3_lambda; + let gradients = gradients.get(&o3_lambda).expect("missing o3_lambda"); + let neighbor_type_i = if let Some(s) = result.types_mapping.get(&neighbor_type.i32()) { *s } else { @@ -570,7 +592,7 @@ impl SphericalExpansion { // SAFETY: same as above unsafe { let out = array.uget_mut([grad_sample_i, xyz_1, xyz_2, m, property_i]); - *out += *contributions.uget([neighbor_type_i, mapped_center, xyz_1, xyz_2, lm_start + m, n.usize()]); + *out += *gradients.uget([neighbor_type_i, mapped_center, xyz_1, xyz_2, m, n.usize()]); } } } @@ -586,29 +608,29 @@ impl SphericalExpansion { struct PairAccumulationResult { /// values of the spherical expansion /// - /// the shape is `[neighbor_type, mapped_center, lm_index, n]` - values: ndarray::Array4, + /// the shape is `l => [neighbor_type, mapped_center, 2 l + 1, n]` + values: BTreeMap>, /// Gradients w.r.t. positions associated with each pair used in the /// calculation. This is used for gradients of a given center representation /// with respect to one of the neighbors /// - /// the shape is `[pair_id, xyz, lm_index, n]` - positions_gradient_by_pair: Option>, + /// the shape is `l => [pair_id, xyz, 2 l + 1, n]` + positions_gradient_by_pair: Option>>, /// gradient of spherical expansion w.r.t. the position of the central atom /// /// this is separate from `positions_gradient_by_pair` because it can be /// summed while computing each pair contributions. /// - /// the shape is `[neighbor_types, mapped_center, xyz, lm_index, n]` - self_positions_gradients: Option>, + /// the shape is `l => [neighbor_types, mapped_center, xyz, 2 l + 1, n]` + self_positions_gradients: Option>>, /// gradients of the spherical expansion w.r.t. cell /// - /// the shape is `[neighbor_type, mapped_center, xyz_1, xyz_2, lm_index, n]` - cell_gradients: Option>, + /// the shape is `l => [neighbor_type, mapped_center, xyz_1, xyz_2, 2 l + 1, n]` + cell_gradients: Option>>, /// gradients of the spherical expansion w.r.t. strain /// - /// the shape is `[neighbor_type, mapped_center, xyz_1, xyz_2, lm_index, n]` - strain_gradients: Option>, + /// the shape is `l => [neighbor_type, mapped_center, xyz_1, xyz_2, 2 l + 1, n]` + strain_gradients: Option>>, /// Mapping from atomic types to the first dimension of values/cell /// gradients/strain gradients @@ -637,16 +659,16 @@ impl CalculatorBase for SphericalExpansion { self.by_pair.cutoffs() } - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { let builder = CenterSingleNeighborsTypesKeys { - cutoff: self.by_pair.parameters().cutoff, + cutoff: self.by_pair.parameters().cutoff.radius, self_pairs: true, }; let keys = builder.keys(systems)?; let mut builder = LabelsBuilder::new(vec!["o3_lambda", "o3_sigma", "center_type", "neighbor_type"]); for &[center_type, neighbor_type] in keys.iter_fixed_size() { - for o3_lambda in 0..=self.by_pair.parameters().max_angular { + for o3_lambda in self.by_pair.parameters().basis.angular_channels() { builder.add(&[o3_lambda.into(), 1.into(), center_type, neighbor_type]); } } @@ -658,7 +680,7 @@ impl CalculatorBase for SphericalExpansion { AtomCenteredSamples::sample_names() } - fn samples(&self, keys: &Labels, systems: &mut [Box]) -> Result, Error> { + fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error> { assert_eq!(keys.names(), ["o3_lambda", "o3_sigma", "center_type", "neighbor_type"]); // only compute the samples once for each `center_type, neighbor_type`, @@ -670,7 +692,7 @@ impl CalculatorBase for SphericalExpansion { } let builder = AtomCenteredSamples { - cutoff: self.by_pair.parameters().cutoff, + cutoff: self.by_pair.parameters().cutoff.radius, center_type: AtomicTypeFilter::Single(center_type.i32()), neighbor_type: AtomicTypeFilter::Single(neighbor_type.i32()), self_pairs: true, @@ -698,7 +720,7 @@ impl CalculatorBase for SphericalExpansion { } } - fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [Box]) -> Result, Error> { + fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [System]) -> Result, Error> { assert_eq!(keys.names(), ["o3_lambda", "o3_sigma", "center_type", "neighbor_type"]); assert_eq!(keys.count(), samples.len()); @@ -707,7 +729,7 @@ impl CalculatorBase for SphericalExpansion { // TODO: we don't need to rebuild the gradient samples for different // o3_lambda let builder = AtomCenteredSamples { - cutoff: self.by_pair.parameters().cutoff, + cutoff: self.by_pair.parameters().cutoff.radius, center_type: AtomicTypeFilter::Single(center_type.i32()), neighbor_type: AtomicTypeFilter::Single(neighbor_type.i32()), self_pairs: true, @@ -752,17 +774,36 @@ impl CalculatorBase for SphericalExpansion { } fn properties(&self, keys: &Labels) -> Vec { - let mut properties = LabelsBuilder::new(self.property_names()); - for n in 0..self.by_pair.parameters().max_radial { - properties.add(&[n]); - } - let properties = properties.finish(); + assert_eq!(keys.names(), ["o3_lambda", "o3_sigma", "center_type", "neighbor_type"]); + + match self.by_pair.parameters.basis { + SphericalExpansionBasis::TensorProduct(ref basis) => { + let mut properties = LabelsBuilder::new(self.property_names()); + for n in 0..basis.radial.size() { + properties.add(&[n]); + } + + return vec![properties.finish(); keys.count()]; + } + SphericalExpansionBasis::Explicit(ref basis) => { + let mut result = Vec::new(); + for [o3_lambda, _, _, _] in keys.iter_fixed_size() { + let mut properties = LabelsBuilder::new(self.property_names()); + + let radial = basis.by_angular.get(&o3_lambda.usize()).expect("missing o3_lambda"); + for n in 0..radial.size() { + properties.add(&[n]); + } - return vec![properties; keys.count()]; + result.push(properties.finish()); + } + return result; + } + } } #[time_graph::instrument(name = "SphericalExpansion::compute")] - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { assert_eq!(descriptor.keys().names(), ["o3_lambda", "o3_sigma", "center_type", "neighbor_type"]); assert!(descriptor.keys().count() > 0); @@ -777,8 +818,8 @@ impl CalculatorBase for SphericalExpansion { systems.par_iter_mut() .zip_eq(&mut descriptors_by_system) .try_for_each(|(system, descriptor)| { - system.compute_neighbors(self.by_pair.parameters().cutoff)?; - let system = &**system; + system.compute_neighbors(self.by_pair.parameters().cutoff.radius)?; + let system = &*system; // we will only run the calculation on pairs where one of the // atom is part of the requested samples @@ -811,6 +852,8 @@ impl CalculatorBase for SphericalExpansion { #[cfg(test)] mod tests { + use std::collections::BTreeMap; + use ndarray::ArrayD; use metatensor::{Labels, TensorBlock, EmptyArray, LabelsBuilder, TensorMap}; @@ -819,20 +862,35 @@ mod tests { use crate::calculators::CalculatorBase; use super::{SphericalExpansion, SphericalExpansionParameters}; - use super::super::{CutoffFunction, RadialScaling}; - use crate::calculators::radial_basis::RadialBasis; + use crate::calculators::soap::{Cutoff, Smoothing}; + use crate::calculators::shared::{Density, DensityKind, DensityScaling, ExplicitBasis}; + use crate::calculators::shared::{SoapRadialBasis, SphericalExpansionBasis, TensorProductBasis}; + fn basis() -> TensorProductBasis { + TensorProductBasis { + max_angular: 6, + radial: SoapRadialBasis::Gto { max_radial: 5, radius: None }, + spline_accuracy: Some(1e-8), + } + } + fn parameters() -> SphericalExpansionParameters { SphericalExpansionParameters { - cutoff: 7.3, - max_radial: 6, - max_angular: 6, - atomic_gaussian_width: 0.3, - center_atom_weight: 1.0, - radial_basis: RadialBasis::splined_gto(1e-8), - radial_scaling: RadialScaling::Willatt2018 { scale: 1.5, rate: 0.8, exponent: 2.0}, - cutoff_function: CutoffFunction::ShiftedCosine { width: 0.5 }, + cutoff: Cutoff { + radius: 7.3, + smoothing: Smoothing::ShiftedCosine { width: 0.5 } + }, + density: Density { + kind: DensityKind::Gaussian { width: 0.3 }, + scaling: Some(DensityScaling::Willatt2018 { + scale: 1.5, + rate: 0.8, + exponent: 2.0 + }), + center_atom_weight: 1.0, + }, + basis: SphericalExpansionBasis::TensorProduct(basis()), } } @@ -861,7 +919,7 @@ mod tests { } // exact values for spherical expansion are regression-tested in - // `rascaline/tests/spherical-expansion.rs` + // `featomic/tests/spherical-expansion.rs` } #[test] @@ -913,7 +971,10 @@ mod tests { fn compute_partial() { let calculator = Calculator::from(Box::new(SphericalExpansion::new( SphericalExpansionParameters { - max_angular: 2, + basis: SphericalExpansionBasis::TensorProduct(TensorProductBasis { + max_angular: 2, + ..basis() + }), ..parameters() } ).unwrap()) as Box); @@ -971,10 +1032,10 @@ mod tests { let mut keys = LabelsBuilder::new(vec!["o3_lambda", "o3_sigma", "center_type", "neighbor_type"]); let mut blocks = Vec::new(); - for l in 0..(parameters().max_angular + 1) as isize { + for o3_lambda in parameters().basis.angular_channels() { for center_type in [1, -42] { for neighbor_type in [1, -42] { - keys.add(&[l, 1, center_type, neighbor_type]); + keys.add(&[o3_lambda as i32, 1, center_type, neighbor_type]); blocks.push(block.as_ref().try_clone().unwrap()); } } @@ -1012,4 +1073,36 @@ mod tests { let array = block.values.as_array(); assert_eq!(array.index_axis(ndarray::Axis(0), 0), ArrayD::from_elem(vec![1, 6], 0.0)); } + + #[test] + fn explicit_basis() { + let mut by_angular = BTreeMap::new(); + by_angular.insert(1, SoapRadialBasis::Gto { max_radial: 5, radius: None }); + by_angular.insert(12, SoapRadialBasis::Gto { max_radial: 3, radius: None }); + + let mut calculator = Calculator::from(Box::new(SphericalExpansion::new( + SphericalExpansionParameters { + basis: SphericalExpansionBasis::Explicit(ExplicitBasis { + by_angular: by_angular.into(), + spline_accuracy: None, + + }), + ..parameters() + } + ).unwrap()) as Box); + + let mut systems = test_systems(&["water"]); + + let descriptor = calculator.compute(&mut systems, Default::default()).unwrap(); + + for (key, block) in &descriptor { + if key[0] == 1 { + assert_eq!(block.properties().count(), 6); + } else if key[0] == 12 { + assert_eq!(block.properties().count(), 4); + } else { + panic!("unexpected o3_lambda value"); + } + } + } } diff --git a/rascaline/src/calculators/soap/spherical_expansion_pair.rs b/featomic/src/calculators/soap/spherical_expansion_pair.rs similarity index 67% rename from rascaline/src/calculators/soap/spherical_expansion_pair.rs rename to featomic/src/calculators/soap/spherical_expansion_pair.rs index b4a78fab1..a9328bd44 100644 --- a/rascaline/src/calculators/soap/spherical_expansion_pair.rs +++ b/featomic/src/calculators/soap/spherical_expansion_pair.rs @@ -2,72 +2,58 @@ use std::collections::{BTreeMap, BTreeSet}; use std::collections::btree_map::Entry; use std::cell::RefCell; -use ndarray::s; use thread_local::ThreadLocal; use metatensor::{Labels, LabelsBuilder, LabelValue, TensorMap, TensorBlockRefMut}; use crate::{Error, System, Vector3D}; +use super::Cutoff; +use super::super::shared::{Density, SoapRadialBasis, SphericalExpansionBasis}; + use crate::math::SphericalHarmonicsCache; -use super::super::CalculatorBase; +use crate::calculators::{CalculatorBase,GradientsOptions}; use super::super::neighbor_list::FullNeighborList; -use super::{CutoffFunction, RadialScaling}; - -use crate::calculators::radial_basis::RadialBasis; -use super::SoapRadialIntegralCache; - -use super::radial_integral::SoapRadialIntegralParameters; +use super::SoapRadialIntegralCacheByAngular; /// Parameters for spherical expansion calculator. /// /// The spherical expansion is at the core of representations in the SOAP -/// (Smooth Overlap of Atomic Positions) family. See [this review -/// article](https://doi.org/10.1063/1.5090481) for more information on the SOAP -/// representation, and [this paper](https://doi.org/10.1063/5.0044689) for -/// information on how it is implemented in rascaline. +/// (Smooth Overlap of Atomic Positions) family. The core idea is to define +/// atom-centered environments using a spherical cutoff; create an atomic +/// density according to all neighbors in a given environment; and finally +/// expand this density on a given set of basis functions. The parameters for +/// each of these steps can be defined separately below. +/// +/// See [this review article](https://doi.org/10.1063/1.5090481) for more +/// information on the SOAP representation, and [this +/// paper](https://doi.org/10.1063/5.0044689) for information on how it is +/// implemented in featomic. #[derive(Debug, Clone)] #[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +#[serde(deny_unknown_fields)] pub struct SphericalExpansionParameters { - /// Spherical cutoff to use for atomic environments - pub cutoff: f64, - /// Number of radial basis function to use in the expansion - pub max_radial: usize, - /// Number of spherical harmonics to use in the expansion - pub max_angular: usize, - /// Width of the atom-centered gaussian used to create the atomic density - pub atomic_gaussian_width: f64, - /// Weight of the central atom contribution to the - /// features. If `1` the center atom contribution is weighted the same - /// as any other contribution. If `0` the central atom does not - /// contribute to the features at all. - pub center_atom_weight: f64, - /// Radial basis to use for the radial integral - pub radial_basis: RadialBasis, - /// Cutoff function used to smooth the behavior around the cutoff radius - pub cutoff_function: CutoffFunction, - /// radial scaling can be used to reduce the importance of neighbor atoms - /// further away from the center, usually improving the performance of the - /// model - #[serde(default)] - pub radial_scaling: RadialScaling, + /// Definition of the atomic environment within a cutoff, and how + /// neighboring atoms enter and leave the environment. + pub cutoff: Cutoff, + /// Definition of the density arising from atoms in the local environment. + pub density: Density, + /// Definition of the basis functions used to expand the atomic density + pub basis: SphericalExpansionBasis, } impl SphericalExpansionParameters { /// Validate all the parameters - pub fn validate(&self) -> Result<(), Error> { - self.cutoff_function.validate()?; - self.radial_scaling.validate()?; - - // try constructing a radial integral - SoapRadialIntegralCache::new(self.radial_basis.clone(), SoapRadialIntegralParameters { - max_radial: self.max_radial, - max_angular: self.max_angular, - atomic_gaussian_width: self.atomic_gaussian_width, - cutoff: self.cutoff, - })?; + pub fn validate(&mut self) -> Result<(), Error> { + self.cutoff.validate()?; + if let Some(scaling) = self.density.scaling { + scaling.validate()?; + } + + // try constructing a radial integral cache to catch any errors early + SoapRadialIntegralCacheByAngular::new(self.cutoff.radius, self.density.kind, &self.basis)?; return Ok(()); } @@ -78,12 +64,12 @@ pub struct SphericalExpansionByPair { pub(crate) parameters: SphericalExpansionParameters, /// implementation + cached allocation to compute the radial integral for a /// single pair - radial_integral: ThreadLocal>, + pub(crate) radial_integral: ThreadLocal>, /// implementation + cached allocation to compute the spherical harmonics /// for a single pair - spherical_harmonics: ThreadLocal>, + pub(crate) spherical_harmonics: ThreadLocal>, /// Cache for (-1)^l values - m_1_pow_l: Vec, + pub(crate) m_1_pow_l: Vec, } impl std::fmt::Debug for SphericalExpansionByPair { @@ -93,42 +79,33 @@ impl std::fmt::Debug for SphericalExpansionByPair { } -/// Which gradients are we computing -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) struct GradientsOptions { - pub positions: bool, - pub cell: bool, - pub strain: bool, -} - -impl GradientsOptions { - pub fn any(self) -> bool { - return self.positions || self.cell || self.strain; - } -} - - /// Contribution of a single pair to the spherical expansion pub(super) struct PairContribution { - /// Values of the contribution. The shape is (lm, n), where the lm index - /// runs over both l and m - pub values: ndarray::Array2, + /// Values of the contribution. The `BTreeMap` contains one array for each + /// angular channel, and the shape of the arrays is (2 * L + 1, N) + pub values: BTreeMap>, /// Gradients of the contribution w.r.t. the distance between the atoms in - /// the pair. The shape is (x/y/z, lm, n). - pub gradients: Option>, + /// the pair. shape of the arrays is (3, 2 * L + 1, N) + pub gradients: Option>>, } impl PairContribution { - pub fn new(max_radial: usize, max_angular: usize, do_gradients: bool) -> PairContribution { - let lm_shape = (max_angular + 1) * (max_angular + 1); - PairContribution { - values: ndarray::Array2::from_elem((lm_shape, max_radial), 0.0), - gradients: if do_gradients { - Some(ndarray::Array3::from_elem((3, lm_shape, max_radial), 0.0)) - } else { - None - } - } + pub fn new(angular_channels: &[usize], radial_sizes: &[usize], do_gradients: bool) -> PairContribution { + let values = angular_channels.iter().zip(radial_sizes).map(|(&o3_lambda, &radial_size)| { + let array = ndarray::Array2::from_elem((2 * o3_lambda + 1, radial_size), 0.0); + (o3_lambda, array) + }).collect(); + + let gradients = if do_gradients { + Some(angular_channels.iter().zip(radial_sizes).map(|(&o3_lambda, &radial_size)| { + let array = ndarray::Array3::from_elem((3, 2 * o3_lambda + 1, radial_size), 0.0); + (o3_lambda, array) + }).collect()) + } else { + None + }; + + return PairContribution { values, gradients } } /// Modify the values/gradients as required to construct the @@ -136,33 +113,34 @@ impl PairContribution { /// /// `m_1_pow_l` should contain the values of `(-1)^l` up to `max_angular` pub fn inverse_pair(&mut self, m_1_pow_l: &[f64]) { - let max_angular = m_1_pow_l.len() - 1; - let max_radial = self.values.shape()[1]; - debug_assert_eq!(self.values.shape()[0], (max_angular + 1) * (max_angular + 1)); - // inverting the pair is equivalent to adding a (-1)^l factor to the // pair contribution values, and -(-1)^l to the gradients - let mut lm_index = 0; - for o3_lambda in 0..=max_angular { + for (&o3_lambda, values) in &mut self.values { + let shape = values.shape(); + debug_assert_eq!(shape[0], 2 * o3_lambda + 1); + let shape_n = shape[1]; + let factor = m_1_pow_l[o3_lambda]; - for _m in 0..(2 * o3_lambda + 1) { - for n in 0..max_radial { - self.values[[lm_index, n]] *= factor; + for m in 0..2 * o3_lambda + 1 { + for n in 0..shape_n { + values[[m, n]] *= factor; } - lm_index += 1; } } if let Some(ref mut gradients) = self.gradients { - for xyz in 0..3 { - let mut lm_index = 0; - for o3_lambda in 0..=max_angular { - let factor = -m_1_pow_l[o3_lambda]; - for _m in 0..(2 * o3_lambda + 1) { - for n in 0..max_radial { - gradients[[xyz, lm_index, n]] *= factor; + for (&o3_lambda, gradients) in gradients.iter_mut() { + let shape = gradients.shape(); + debug_assert_eq!(shape[0], 3); + debug_assert_eq!(shape[1], 2 * o3_lambda + 1); + let shape_n = shape[2]; + + let factor = -m_1_pow_l[o3_lambda]; + for xyz in 0..3 { + for m in 0..(2 * o3_lambda + 1) { + for n in 0..shape_n { + gradients[[xyz, m, n]] *= factor; } - lm_index += 1; } } } @@ -172,18 +150,18 @@ impl PairContribution { impl SphericalExpansionByPair { - pub fn new(parameters: SphericalExpansionParameters) -> Result { + pub fn new(mut parameters: SphericalExpansionParameters) -> Result { parameters.validate()?; - let m_1_pow_l = (0..=parameters.max_angular) - .map(|l| f64::powi(-1.0, l as i32)) + let max_angular = parameters.basis.angular_channels().into_iter().max().expect("there should be at least one angular channel"); + let m_1_pow_l = (0..=max_angular).map(|l| f64::powi(-1.0, l as i32)) .collect::>(); Ok(SphericalExpansionByPair { parameters: parameters, radial_integral: ThreadLocal::new(), spherical_harmonics: ThreadLocal::new(), - m_1_pow_l, + m_1_pow_l: m_1_pow_l, }) } @@ -193,19 +171,25 @@ impl SphericalExpansionByPair { } /// Compute the product of radial scaling & cutoff smoothing functions - fn scaling_functions(&self, r: f64) -> f64 { - let cutoff = self.parameters.cutoff_function.compute(r, self.parameters.cutoff); - let scaling = self.parameters.radial_scaling.compute(r); - return cutoff * scaling; + pub(crate) fn scaling_functions(&self, r: f64) -> f64 { + let mut scaling = 1.0; + if let Some(scaler) = self.parameters.density.scaling { + scaling = scaler.compute(r); + } + return scaling * self.parameters.cutoff.smoothing(r); } /// Compute the gradient of the product of radial scaling & cutoff smoothing functions - fn scaling_functions_gradient(&self, r: f64) -> f64 { - let cutoff = self.parameters.cutoff_function.compute(r, self.parameters.cutoff); - let cutoff_grad = self.parameters.cutoff_function.derivative(r, self.parameters.cutoff); + pub(crate) fn scaling_functions_gradient(&self, r: f64) -> f64 { + let mut scaling = 1.0; + let mut scaling_grad = 0.0; + if let Some(scaler) = self.parameters.density.scaling { + scaling = scaler.compute(r); + scaling_grad = scaler.gradient(r); + } - let scaling = self.parameters.radial_scaling.compute(r); - let scaling_grad = self.parameters.radial_scaling.derivative(r); + let cutoff = self.parameters.cutoff.smoothing(r); + let cutoff_grad = self.parameters.cutoff.smoothing_gradient(r); return cutoff_grad * scaling + cutoff * scaling_grad; } @@ -220,41 +204,36 @@ impl SphericalExpansionByPair { /// /// By symmetry, the self-contribution is only non-zero for `L=0`, and does /// not contributes to the gradients. - pub(super) fn self_contribution(&self) -> PairContribution { + pub(super) fn self_contribution(&self) -> ndarray::Array1 { let mut radial_integral = self.radial_integral.get_or(|| { - let radial_integral = SoapRadialIntegralCache::new( - self.parameters.radial_basis.clone(), - SoapRadialIntegralParameters { - max_radial: self.parameters.max_radial, - max_angular: self.parameters.max_angular, - atomic_gaussian_width: self.parameters.atomic_gaussian_width, - cutoff: self.parameters.cutoff, - } - ).expect("invalid radial integral parameters"); - return RefCell::new(radial_integral); + RefCell::new(SoapRadialIntegralCacheByAngular::new( + self.parameters.cutoff.radius, + self.parameters.density.kind, + &self.parameters.basis + ).expect("invalid radial integral parameters") + ) }).borrow_mut(); let mut spherical_harmonics = self.spherical_harmonics.get_or(|| { - RefCell::new(SphericalHarmonicsCache::new(self.parameters.max_angular)) + let max_angular = self.parameters.basis.angular_channels().into_iter().max().unwrap_or(0); + RefCell::new(SphericalHarmonicsCache::new(max_angular)) }).borrow_mut(); // Compute the three factors that appear in the center contribution. // Note that this is simply the pair contribution for the special // case where the pair distance is zero. + let radial_integral = radial_integral.get_mut(0) + .expect("self_contribution can't be done when o3_lambda=0 is missing"); radial_integral.compute(0.0, false); + spherical_harmonics.compute(Vector3D::new(0.0, 0.0, 1.0), false); let f_scaling = self.scaling_functions(0.0); - let factor = self.parameters.center_atom_weight + let factor = self.parameters.density.center_atom_weight * f_scaling * spherical_harmonics.values[[0, 0]]; - radial_integral.values *= factor; - - return PairContribution { - values: radial_integral.values.clone(), - gradients: None - }; + return factor * radial_integral.values.clone(); } /// Accumulate the self contribution to the spherical expansion @@ -262,8 +241,14 @@ impl SphericalExpansionByPair { /// /// For the pair-by-pair spherical expansion, we use a special `pair_id` /// (-1) to store the data associated with self-pairs. - fn do_self_contributions(&self, systems: &[Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn do_self_contributions(&self, systems: &[System], descriptor: &mut TensorMap) -> Result<(), Error> { debug_assert_eq!(descriptor.keys().names(), ["o3_lambda", "o3_sigma", "first_atom_type", "second_atom_type"]); + + if !self.parameters.basis.angular_channels().contains(&0) { + // o3_lambda is not part of the output, skip self contributions + return Ok(()); + } + let self_contribution = self.self_contribution(); for (key, mut block) in descriptor { @@ -279,8 +264,9 @@ impl SphericalExpansionByPair { let data = block.data_mut(); let array = data.values.to_array_mut(); - // loop over all samples in this block, find self pairs - // (`pair_id` is -1), and fill the data using `self_contribution` + // loop over all samples in this block, find self pairs (`i == j` + // and `shift == [0, 0, 0]`), and fill the data using + // `self_contribution` for (sample_i, &[system, atom_1, atom_2, cell_a, cell_b, cell_c]) in data.samples.iter_fixed_size().enumerate() { // it is possible that the samples from values.samples are not // part of the systems (the user requested extra samples). In @@ -304,7 +290,7 @@ impl SphericalExpansionByPair { } for (property_i, &[n]) in data.properties.iter_fixed_size().enumerate() { - array[[sample_i, 0, property_i]] = self_contribution.values[[0, n.usize()]]; + array[[sample_i, 0, property_i]] = self_contribution[n.usize()]; } } } @@ -338,20 +324,17 @@ impl SphericalExpansionByPair { } let mut radial_integral = self.radial_integral.get_or(|| { - let radial_integral = SoapRadialIntegralCache::new( - self.parameters.radial_basis.clone(), - SoapRadialIntegralParameters { - max_radial: self.parameters.max_radial, - max_angular: self.parameters.max_angular, - atomic_gaussian_width: self.parameters.atomic_gaussian_width, - cutoff: self.parameters.cutoff, - } - ).expect("invalid parameters"); - return RefCell::new(radial_integral); + RefCell::new(SoapRadialIntegralCacheByAngular::new( + self.parameters.cutoff.radius, + self.parameters.density.kind, + &self.parameters.basis + ).expect("invalid radial integral parameters") + ) }).borrow_mut(); let mut spherical_harmonics = self.spherical_harmonics.get_or(|| { - RefCell::new(SphericalHarmonicsCache::new(self.parameters.max_angular)) + let max_angular = self.parameters.basis.angular_channels().into_iter().max().unwrap_or(0); + RefCell::new(SphericalHarmonicsCache::new(max_angular)) }).borrow_mut(); radial_integral.compute(distance, do_gradients.any()); @@ -360,28 +343,37 @@ impl SphericalExpansionByPair { let f_scaling = self.scaling_functions(distance); let f_scaling_grad = self.scaling_functions_gradient(distance); - let mut lm_index = 0; - let mut lm_index_grad = 0; - for o3_lambda in 0..=self.parameters.max_angular { + for o3_lambda in self.parameters.basis.angular_channels() { + let radial_basis_size = match self.parameters.basis { + SphericalExpansionBasis::TensorProduct(ref basis) => basis.radial.size(), + SphericalExpansionBasis::Explicit(ref basis) => { + basis.by_angular.get(&o3_lambda).expect("missing o3_lambda").size() + }, + }; + let spherical_harmonics_grad = [ - spherical_harmonics.gradients[0].slice(o3_lambda as isize), - spherical_harmonics.gradients[1].slice(o3_lambda as isize), - spherical_harmonics.gradients[2].slice(o3_lambda as isize), + spherical_harmonics.gradients[0].angular_slice(o3_lambda), + spherical_harmonics.gradients[1].angular_slice(o3_lambda), + spherical_harmonics.gradients[2].angular_slice(o3_lambda), ]; - let spherical_harmonics = spherical_harmonics.values.slice(o3_lambda as isize); + let spherical_harmonics = spherical_harmonics.values.angular_slice(o3_lambda); + + let radial_integral_grad = &radial_integral.get(o3_lambda).expect("missing o3_lambda").gradients; + let radial_integral = &radial_integral.get(o3_lambda).expect("missing o3_lambda").values; - let radial_integral_grad = radial_integral.gradients.slice(s![o3_lambda, ..]); - let radial_integral = radial_integral.values.slice(s![o3_lambda, ..]); + let values = contribution.values.get_mut(&o3_lambda).expect("missing o3_lambda"); // compute the full spherical expansion coefficients & gradients - for sph_value in spherical_harmonics { + for m in 0..(2 * o3_lambda + 1) { + let sph_value = spherical_harmonics[m]; for (n, ri_value) in radial_integral.iter().enumerate() { - contribution.values[[lm_index, n]] = f_scaling * sph_value * ri_value; + values[[m, n]] = f_scaling * sph_value * ri_value; } - lm_index += 1; } - if let Some(ref mut gradient) = contribution.gradients { + if let Some(ref mut gradients) = contribution.gradients { + let gradients = gradients.get_mut(&o3_lambda).expect("missing o3_lambda"); + let dr_d_spatial = direction; for m in 0..(2 * o3_lambda + 1) { @@ -390,27 +382,25 @@ impl SphericalExpansionByPair { let sph_grad_y = spherical_harmonics_grad[1][m]; let sph_grad_z = spherical_harmonics_grad[2][m]; - for n in 0..self.parameters.max_radial { + for n in 0..radial_basis_size { let ri_value = radial_integral[n]; let ri_grad = radial_integral_grad[n]; - gradient[[0, lm_index_grad, n]] = + gradients[[0, m, n]] = f_scaling_grad * dr_d_spatial[0] * ri_value * sph_value + f_scaling * ri_grad * dr_d_spatial[0] * sph_value + f_scaling * ri_value * sph_grad_x / distance; - gradient[[1, lm_index_grad, n]] = + gradients[[1, m, n]] = f_scaling_grad * dr_d_spatial[1] * ri_value * sph_value + f_scaling * ri_grad * dr_d_spatial[1] * sph_value + f_scaling * ri_value * sph_grad_y / distance; - gradient[[2, lm_index_grad, n]] = + gradients[[2, m, n]] = f_scaling_grad * dr_d_spatial[2] * ri_value * sph_value + f_scaling * ri_grad * dr_d_spatial[2] * sph_value + f_scaling * ri_value * sph_grad_z / distance; } - - lm_index_grad += 1; } } } @@ -421,27 +411,27 @@ impl SphericalExpansionByPair { o3_lambda: usize, mut block: TensorBlockRefMut, sample: &[LabelValue], - contribution: &PairContribution, + contributions: &PairContribution, do_gradients: GradientsOptions, pair_vector: Vector3D, ) { let data = block.data_mut(); let array = data.values.to_array_mut(); + let contribution_values = contributions.values.get(&o3_lambda).expect("missing o3_lambda"); let sample_i = data.samples.position(sample); if let Some(sample_i) = sample_i { - let lm_start = o3_lambda * o3_lambda; - for m in 0..(2 * o3_lambda + 1) { for (property_i, [n]) in data.properties.iter_fixed_size().enumerate() { unsafe { let out = array.uget_mut([sample_i, m, property_i]); - *out += *contribution.values.uget([lm_start + m, n.usize()]); + *out += *contribution_values.uget([m, n.usize()]); } } } - if let Some(ref contribution_gradients) = contribution.gradients { + if let Some(ref contribution_gradients) = contributions.gradients { + let contribution_gradients = contribution_gradients.get(&o3_lambda).expect("missing o3_lambda"); if do_gradients.positions { let mut gradient = block.gradient_mut("positions").expect("missing positions gradients"); let gradient = gradient.data_mut(); @@ -460,7 +450,7 @@ impl SphericalExpansionByPair { for (property_i, [n]) in gradient.properties.iter_fixed_size().enumerate() { unsafe { let out = array.uget_mut([first_grad_sample_i, xyz, m, property_i]); - *out -= contribution_gradients.uget([xyz, lm_start + m, n.usize()]); + *out -= contribution_gradients.uget([xyz, m, n.usize()]); } } } @@ -477,7 +467,7 @@ impl SphericalExpansionByPair { for (property_i, [n]) in gradient.properties.iter_fixed_size().enumerate() { unsafe { let out = array.uget_mut([second_grad_sample_i, xyz, m, property_i]); - *out += contribution_gradients.uget([xyz, lm_start + m, n.usize()]); + *out += contribution_gradients.uget([xyz, m, n.usize()]); } } } @@ -498,7 +488,7 @@ impl SphericalExpansionByPair { for (property_i, [n]) in gradient.properties.iter_fixed_size().enumerate() { unsafe { let out = array.uget_mut([sample_i, xyz_1, xyz_2, m, property_i]); - *out += pair_vector[xyz_1] * contribution_gradients.uget([xyz_2, lm_start + m, n.usize()]); + *out += pair_vector[xyz_1] * contribution_gradients.uget([xyz_2, m, n.usize()]); } } } @@ -526,7 +516,7 @@ impl SphericalExpansionByPair { for (property_i, [n]) in gradient.properties.iter_fixed_size().enumerate() { unsafe { let out = array.uget_mut([sample_i, abc, xyz, m, property_i]); - *out += shifts[abc] * contribution_gradients.uget([xyz, lm_start + m, n.usize()]); + *out += shifts[abc] * contribution_gradients.uget([xyz, m, n.usize()]); } } } @@ -549,14 +539,14 @@ impl CalculatorBase for SphericalExpansionByPair { } fn cutoffs(&self) -> &[f64] { - std::slice::from_ref(&self.parameters.cutoff) + std::slice::from_ref(&self.parameters.cutoff.radius) } - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { // the atomic type part of the keys is the same for all l, and the same // as what a FullNeighborList with `self_pairs=True` produces. let full_neighbors_list_keys = FullNeighborList { - cutoff: self.parameters.cutoff, + cutoff: self.parameters.cutoff.radius, self_pairs: true, }.keys(systems)?; @@ -568,8 +558,8 @@ impl CalculatorBase for SphericalExpansionByPair { ]); for &[first_type, second_type] in full_neighbors_list_keys.iter_fixed_size() { - for l in 0..=self.parameters.max_angular { - keys.add(&[l.into(), 1.into(), first_type, second_type]); + for o3_lambda in self.parameters.basis.angular_channels() { + keys.add(&[o3_lambda.into(), 1.into(), first_type, second_type]); } } @@ -580,7 +570,7 @@ impl CalculatorBase for SphericalExpansionByPair { return vec!["system", "first_atom", "second_atom", "cell_shift_a", "cell_shift_b", "cell_shift_c"]; } - fn samples(&self, keys: &Labels, systems: &mut [Box]) -> Result, Error> { + fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error> { // get all atomic types pairs in keys as a new set of Labels let mut types_keys = BTreeSet::new(); for &[_, _, first_type, second_type] in keys.iter_fixed_size() { @@ -595,7 +585,7 @@ impl CalculatorBase for SphericalExpansionByPair { // for l=0, we want to include self pairs in the samples let mut samples_by_types_l0: BTreeMap<_, Labels> = BTreeMap::new(); let full_neighbors_list_samples = FullNeighborList { - cutoff: self.parameters.cutoff, + cutoff: self.parameters.cutoff.radius, self_pairs: true, }.samples(&types_keys, systems)?; @@ -609,16 +599,14 @@ impl CalculatorBase for SphericalExpansionByPair { // neighbor_type) => Labels map and then re-use them from this map as // needed. let mut samples_by_types: BTreeMap<_, Labels> = BTreeMap::new(); - if self.parameters.max_angular > 0 { - let full_neighbors_list_samples = FullNeighborList { - cutoff: self.parameters.cutoff, - self_pairs: false, - }.samples(&types_keys, systems)?; - - debug_assert_eq!(types_keys.count(), full_neighbors_list_samples.len()); - for (&[first_type, second_type], samples) in types_keys.iter_fixed_size().zip(full_neighbors_list_samples) { - samples_by_types.insert((first_type, second_type), samples); - } + let full_neighbors_list_samples = FullNeighborList { + cutoff: self.parameters.cutoff.radius, + self_pairs: false, + }.samples(&types_keys, systems)?; + + debug_assert_eq!(types_keys.count(), full_neighbors_list_samples.len()); + for (&[first_type, second_type], samples) in types_keys.iter_fixed_size().zip(full_neighbors_list_samples) { + samples_by_types.insert((first_type, second_type), samples); } let mut result = Vec::new(); @@ -642,7 +630,7 @@ impl CalculatorBase for SphericalExpansionByPair { } } - fn positions_gradient_samples(&self, _: &Labels, samples: &[Labels], _: &mut [Box]) -> Result, Error> { + fn positions_gradient_samples(&self, _: &Labels, samples: &[Labels], _: &mut [System]) -> Result, Error> { let mut results = Vec::new(); for block_samples in samples { @@ -697,16 +685,36 @@ impl CalculatorBase for SphericalExpansionByPair { } fn properties(&self, keys: &Labels) -> Vec { - let mut properties = LabelsBuilder::new(self.property_names()); - for n in 0..self.parameters.max_radial { - properties.add(&[n]); - } + assert_eq!(keys.names(), ["o3_lambda", "o3_sigma", "first_atom_type", "second_atom_type"]); + + match self.parameters.basis { + SphericalExpansionBasis::TensorProduct(ref basis) => { + let mut properties = LabelsBuilder::new(self.property_names()); + for n in 0..basis.radial.size() { + properties.add(&[n]); + } + + return vec![properties.finish(); keys.count()]; + } + SphericalExpansionBasis::Explicit(ref basis) => { + let mut result = Vec::new(); + for [o3_lambda, _, _, _] in keys.iter_fixed_size() { + let mut properties = LabelsBuilder::new(self.property_names()); + + let radial = basis.by_angular.get(&o3_lambda.usize()).expect("missing o3_lambda"); + for n in 0..radial.size() { + properties.add(&[n]); + } - return vec![properties.finish(); keys.count()]; + result.push(properties.finish()); + } + return result; + } + } } #[time_graph::instrument(name = "SphericalExpansionByPair::compute")] - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { assert_eq!(descriptor.keys().names(), ["o3_lambda", "o3_sigma", "first_atom_type", "second_atom_type"]); assert!(descriptor.keys().count() > 0); @@ -720,12 +728,23 @@ impl CalculatorBase for SphericalExpansionByPair { let keys = descriptor.keys().clone(); - let max_angular = self.parameters.max_angular; - let max_radial = self.parameters.max_radial; - let mut contribution = PairContribution::new(max_radial, max_angular, do_gradients.any()); + let radial_sizes = match self.parameters.basis { + SphericalExpansionBasis::TensorProduct(ref basis) => { + vec![basis.radial.size(); basis.max_angular + 1] + }, + SphericalExpansionBasis::Explicit(ref basis) => { + basis.by_angular.values().map(|radial| radial.size()).collect() + }, + }; + + let mut contribution = PairContribution::new( + &self.parameters.basis.angular_channels(), + &radial_sizes, + do_gradients.any(), + ); for (system_i, system) in systems.iter_mut().enumerate() { - system.compute_neighbors(self.parameters.cutoff)?; + system.compute_neighbors(self.parameters.cutoff.radius)?; let types = system.types()?; for pair in system.pairs()? { @@ -738,7 +757,7 @@ impl CalculatorBase for SphericalExpansionByPair { let first_type = types[pair.first]; let second_type = types[pair.second]; - for o3_lambda in 0..=self.parameters.max_angular { + for o3_lambda in self.parameters.basis.angular_channels() { let block_i = keys.position(&[ o3_lambda.into(), 1.into(), @@ -770,7 +789,7 @@ impl CalculatorBase for SphericalExpansionByPair { // also check for the block with a reversed pair contribution.inverse_pair(&self.m_1_pow_l); - for o3_lambda in 0..=self.parameters.max_angular { + for o3_lambda in self.parameters.basis.angular_channels() { let block_i = keys.position(&[ o3_lambda.into(), 1.into(), @@ -809,29 +828,46 @@ impl CalculatorBase for SphericalExpansionByPair { #[cfg(test)] mod tests { + use std::collections::BTreeMap; + use metatensor::Labels; use ndarray::{s, Axis}; use approx::assert_ulps_eq; + use crate::systems::test_utils::{test_system, test_systems}; use crate::Calculator; use crate::calculators::{CalculatorBase, SphericalExpansion}; use super::{SphericalExpansionByPair, SphericalExpansionParameters}; - use super::super::{CutoffFunction, RadialScaling}; - use crate::calculators::radial_basis::RadialBasis; - + use crate::calculators::soap::{Cutoff, Smoothing}; + use crate::calculators::shared::{Density, DensityKind, DensityScaling, ExplicitBasis}; + use crate::calculators::shared::{SoapRadialBasis, SphericalExpansionBasis, TensorProductBasis}; + + fn basis() -> TensorProductBasis { + TensorProductBasis { + max_angular: 2, + radial: SoapRadialBasis::Gto { max_radial: 5, radius: None }, + spline_accuracy: Some(1e-8), + } + } fn parameters() -> SphericalExpansionParameters { SphericalExpansionParameters { - cutoff: 7.3, - max_radial: 6, - max_angular: 6, - atomic_gaussian_width: 0.3, - center_atom_weight: 1.0, - radial_basis: RadialBasis::splined_gto(1e-8), - radial_scaling: RadialScaling::Willatt2018 { scale: 1.5, rate: 0.8, exponent: 2.0}, - cutoff_function: CutoffFunction::ShiftedCosine { width: 0.5 }, + cutoff: Cutoff { + radius: 7.3, + smoothing: Smoothing::ShiftedCosine { width: 0.5 } + }, + density: Density { + kind: DensityKind::Gaussian { width: 0.3 }, + scaling: Some(DensityScaling::Willatt2018 { + scale: 1.5, + rate: 0.8, + exponent: 2.0 + }), + center_atom_weight: 1.0, + }, + basis: SphericalExpansionBasis::TensorProduct(basis()), } } @@ -884,7 +920,10 @@ mod tests { fn compute_partial() { let calculator = Calculator::from(Box::new(SphericalExpansionByPair::new( SphericalExpansionParameters { - max_angular: 2, + basis: SphericalExpansionBasis::TensorProduct(TensorProductBasis { + max_angular: 2, + ..basis() + }), ..parameters() } ).unwrap()) as Box); @@ -965,4 +1004,36 @@ mod tests { } } } + + #[test] + fn explicit_basis() { + let mut by_angular = BTreeMap::new(); + by_angular.insert(1, SoapRadialBasis::Gto { max_radial: 5, radius: None }); + by_angular.insert(12, SoapRadialBasis::Gto { max_radial: 3, radius: None }); + + let mut calculator = Calculator::from(Box::new(SphericalExpansionByPair::new( + SphericalExpansionParameters { + basis: SphericalExpansionBasis::Explicit(ExplicitBasis { + by_angular: by_angular.into(), + spline_accuracy: None, + + }), + ..parameters() + } + ).unwrap()) as Box); + + let mut systems = test_systems(&["water"]); + + let descriptor = calculator.compute(&mut systems, Default::default()).unwrap(); + + for (key, block) in &descriptor { + if key[0] == 1 { + assert_eq!(block.properties().count(), 6); + } else if key[0] == 12 { + assert_eq!(block.properties().count(), 4); + } else { + panic!("unexpected o3_lambda value"); + } + } + } } diff --git a/rascaline/src/calculators/sorted_distances.rs b/featomic/src/calculators/sorted_distances.rs similarity index 95% rename from rascaline/src/calculators/sorted_distances.rs rename to featomic/src/calculators/sorted_distances.rs index b92163fbf..9f283339a 100644 --- a/rascaline/src/calculators/sorted_distances.rs +++ b/featomic/src/calculators/sorted_distances.rs @@ -41,7 +41,7 @@ impl CalculatorBase for SortedDistances { std::slice::from_ref(&self.cutoff) } - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { if self.separate_neighbor_types { let builder = CenterSingleNeighborsTypesKeys { cutoff: self.cutoff, @@ -57,7 +57,7 @@ impl CalculatorBase for SortedDistances { AtomCenteredSamples::sample_names() } - fn samples(&self, keys: &Labels, systems: &mut [Box]) -> Result, Error> { + fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error> { let mut samples = Vec::new(); if self.separate_neighbor_types { assert_eq!(keys.names(), ["center_type", "neighbor_type"]); @@ -92,7 +92,7 @@ impl CalculatorBase for SortedDistances { return false; } - fn positions_gradient_samples(&self, _: &Labels, _: &[Labels], _: &mut [Box]) -> Result, Error> { + fn positions_gradient_samples(&self, _: &Labels, _: &[Labels], _: &mut [System]) -> Result, Error> { unimplemented!() } @@ -115,7 +115,7 @@ impl CalculatorBase for SortedDistances { } #[time_graph::instrument(name = "SortedDistances::compute")] - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { if self.separate_neighbor_types { assert_eq!(descriptor.keys().names(), ["center_type", "neighbor_type"]); } else { diff --git a/rascaline/src/calculators/tests_utils.rs b/featomic/src/calculators/tests_utils.rs similarity index 93% rename from rascaline/src/calculators/tests_utils.rs rename to featomic/src/calculators/tests_utils.rs index 5594acaba..466713eb7 100644 --- a/rascaline/src/calculators/tests_utils.rs +++ b/featomic/src/calculators/tests_utils.rs @@ -5,7 +5,7 @@ use metatensor::{Labels, TensorMap, LabelsBuilder}; use crate::calculator::LabelsSelection; use crate::{CalculationOptions, Calculator, Matrix3}; -use crate::systems::{System, SimpleSystem, UnitCell}; +use crate::systems::{System, SystemBase, SimpleSystem, UnitCell}; /// Check that computing a partial subset of features/samples works as intended /// for the given `calculator` and `systems`. @@ -15,7 +15,7 @@ use crate::systems::{System, SimpleSystem, UnitCell}; /// gradients. pub fn compute_partial( mut calculator: Calculator, - systems: &mut [Box], + systems: &mut [System], keys: &Labels, samples: &Labels, properties: &Labels, @@ -51,7 +51,7 @@ pub fn compute_partial( fn check_compute_partial_keys( calculator: &mut Calculator, - systems: &mut [Box], + systems: &mut [System], full: &TensorMap, keys: &Labels, ) { @@ -84,7 +84,7 @@ fn check_compute_partial_keys( fn check_compute_partial_properties( calculator: &mut Calculator, - systems: &mut [Box], + systems: &mut [System], full: &TensorMap, properties: &Labels, ) { @@ -130,7 +130,7 @@ fn check_compute_partial_properties( fn check_compute_partial_samples( calculator: &mut Calculator, - systems: &mut [Box], + systems: &mut [System], full: &TensorMap, samples: &Labels, ) { @@ -183,7 +183,7 @@ fn check_compute_partial_samples( fn check_compute_partial_both( calculator: &mut Calculator, - systems: &mut [Box], + systems: &mut [System], full: &TensorMap, samples: &Labels, properties: &Labels, @@ -265,17 +265,17 @@ pub fn finite_differences_positions(mut calculator: Calculator, system: &SimpleS gradients: &["positions"], ..Default::default() }; - let reference = calculator.compute(&mut [Box::new(system.clone())], calculation_options).unwrap(); + let reference = calculator.compute(&mut [System::new(system.clone())], calculation_options).unwrap(); for neighbor_i in 0..system.size().unwrap() { for xyz in 0..3 { let mut system_pos = system.clone(); system_pos.positions_mut()[neighbor_i][xyz] += options.displacement / 2.0; - let updated_pos = calculator.compute(&mut [Box::new(system_pos)], Default::default()).unwrap(); + let updated_pos = calculator.compute(&mut [System::new(system_pos)], Default::default()).unwrap(); let mut system_neg = system.clone(); system_neg.positions_mut()[neighbor_i][xyz] -= options.displacement / 2.0; - let updated_neg = calculator.compute(&mut [Box::new(system_neg)], Default::default()).unwrap(); + let updated_neg = calculator.compute(&mut [System::new(system_neg)], Default::default()).unwrap(); assert_eq!(updated_pos.keys(), reference.keys()); assert_eq!(updated_neg.keys(), reference.keys()); @@ -327,7 +327,7 @@ pub fn finite_differences_cell(mut calculator: Calculator, system: &SimpleSystem gradients: &["cell"], ..Default::default() }; - let reference = calculator.compute(&mut [Box::new(system.clone())], calculation_options).unwrap(); + let reference = calculator.compute(&mut [System::new(system.clone())], calculation_options).unwrap(); let original_cell = system.cell().unwrap().matrix(); for abc in 0..3 { @@ -336,12 +336,12 @@ pub fn finite_differences_cell(mut calculator: Calculator, system: &SimpleSystem deformed_cell[abc][xyz] += options.displacement / 2.0; let mut system_pos = system.clone(); system_pos.set_cell(UnitCell::from(deformed_cell)); - let updated_pos = calculator.compute(&mut [Box::new(system_pos)], Default::default()).unwrap(); + let updated_pos = calculator.compute(&mut [System::new(system_pos)], Default::default()).unwrap(); deformed_cell[abc][xyz] -= options.displacement; let mut system_neg = system.clone(); system_neg.set_cell(UnitCell::from(deformed_cell)); - let updated_neg = calculator.compute(&mut [Box::new(system_neg)], Default::default()).unwrap(); + let updated_neg = calculator.compute(&mut [System::new(system_neg)], Default::default()).unwrap(); for (block_i, (_, block)) in reference.iter().enumerate() { let gradients = &block.gradient("cell").unwrap(); @@ -389,7 +389,7 @@ pub fn finite_differences_strain(mut calculator: Calculator, system: &SimpleSyst gradients: &["strain"], ..Default::default() }; - let reference = calculator.compute(&mut [Box::new(system.clone())], calculation_options).unwrap(); + let reference = calculator.compute(&mut [System::new(system.clone())], calculation_options).unwrap(); let original_cell = system.cell().unwrap().matrix(); for xyz_1 in 0..3 { @@ -401,7 +401,7 @@ pub fn finite_differences_strain(mut calculator: Calculator, system: &SimpleSyst for position in system_pos.positions_mut() { *position = *position * strain; } - let updated_pos = calculator.compute(&mut [Box::new(system_pos)], Default::default()).unwrap(); + let updated_pos = calculator.compute(&mut [System::new(system_pos)], Default::default()).unwrap(); strain[xyz_1][xyz_2] -= options.displacement; let mut system_neg = system.clone(); @@ -409,7 +409,7 @@ pub fn finite_differences_strain(mut calculator: Calculator, system: &SimpleSyst for position in system_neg.positions_mut() { *position = *position * strain; } - let updated_neg = calculator.compute(&mut [Box::new(system_neg)], Default::default()).unwrap(); + let updated_neg = calculator.compute(&mut [System::new(system_neg)], Default::default()).unwrap(); for (block_i, (_, block)) in reference.iter().enumerate() { let gradients = &block.gradient("strain").unwrap(); diff --git a/rascaline/src/errors.rs b/featomic/src/errors.rs similarity index 92% rename from rascaline/src/errors.rs rename to featomic/src/errors.rs index dfe48106b..b7bc3a899 100644 --- a/rascaline/src/errors.rs +++ b/featomic/src/errors.rs @@ -9,8 +9,6 @@ pub enum Error { Json(serde_json::Error), /// Error due to C strings containing non-utf8 data Utf8(Utf8Error), - /// Error related to reading files with chemfiles - Chemfiles(String), /// Errors coming from metatensor Metatensor(metatensor::Error), /// Errors coming from external callbacks, typically inside the System @@ -23,7 +21,7 @@ pub enum Error { /// usually in the C API. BufferSize(String), /// Error used for failed internal consistency check and panics, i.e. bugs - /// in rascaline. + /// in featomic. Internal(String), } @@ -33,12 +31,11 @@ impl std::fmt::Display for Error { Error::InvalidParameter(e) => write!(f, "invalid parameter: {}", e), Error::Json(e) => write!(f, "json error: {}", e), Error::Utf8(e) => write!(f, "utf8 decoding error: {}", e), - Error::Chemfiles(e) => write!(f, "chemfiles error: {}", e), Error::Metatensor(e) => write!(f, "metatensor error: {}", e), Error::BufferSize(e) => write!(f, "buffer is not big enough: {}", e), Error::External{status, message} => write!(f, "error from external code (status {}): {}", status, message), Error::Internal(e) => { - write!(f, "internal error")?; + write!(f, "internal featomic error")?; if e.contains("assertion failed") { write!(f, " (this is likely a bug, please report it)")?; } @@ -53,7 +50,6 @@ impl std::error::Error for Error { match self { Error::InvalidParameter(_) | Error::Internal(_) | - Error::Chemfiles(_) | Error::BufferSize(_) | Error::External{..} => None, Error::Metatensor(e) => Some(e), diff --git a/rascaline/src/labels/keys.rs b/featomic/src/labels/keys.rs similarity index 68% rename from rascaline/src/labels/keys.rs rename to featomic/src/labels/keys.rs index aae620af5..e4bc4810c 100644 --- a/rascaline/src/labels/keys.rs +++ b/featomic/src/labels/keys.rs @@ -3,19 +3,19 @@ use std::collections::BTreeSet; use metatensor::{Labels, LabelsBuilder}; use crate::{System, Error}; - +use crate::systems::BATripletNeighborList; /// Common interface to create a set of metatensor's `TensorMap` keys from systems pub trait KeysBuilder { /// Compute the keys corresponding to these systems - fn keys(&self, systems: &mut [Box]) -> Result; + fn keys(&self, systems: &mut [System]) -> Result; } /// Compute a set of keys with a single variable, the central atom type. pub struct CenterTypesKeys; impl KeysBuilder for CenterTypesKeys { - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { let mut all_types = BTreeSet::new(); for system in systems { for &atomic_type in system.types()? { @@ -36,7 +36,7 @@ impl KeysBuilder for CenterTypesKeys { pub struct AllTypesPairsKeys {} impl KeysBuilder for AllTypesPairsKeys { - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { let mut all_types_pairs = BTreeSet::new(); for system in systems { @@ -66,7 +66,7 @@ pub struct CenterSingleNeighborsTypesKeys { } impl KeysBuilder for CenterSingleNeighborsTypesKeys { - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { assert!(self.cutoff > 0.0 && self.cutoff.is_finite()); let mut all_types_pairs = BTreeSet::new(); @@ -95,6 +95,52 @@ impl KeysBuilder for CenterSingleNeighborsTypesKeys { } } +/// Compute a set of keys with three variables: the types of two central atoms within a given cutoff to each other, +/// and the type of a third, neighbor atom, within a cutoff of the first two. +pub struct TwoCentersSingleNeighborsTypesKeys<'a> { + /// Spherical cutoff to use when searching for neighbors around an atom + pub(crate) cutoffs: [f64;2], + /// Should we consider an atom to be it's own neighbor or not? + pub self_contributions: bool, + pub raw_triplets: &'a BATripletNeighborList, +} + +impl<'a> TwoCentersSingleNeighborsTypesKeys<'a>{ + pub fn bond_cutoff(&self) -> f64 { + self.cutoffs[0] + } + pub fn third_cutoff(&self) -> f64 { + self.cutoffs[1] + } +} + + +impl<'a> KeysBuilder for TwoCentersSingleNeighborsTypesKeys<'a> { + fn keys(&self, systems: &mut [System]) -> Result { + assert!(self.bond_cutoff() > 0.0 && self.bond_cutoff().is_finite() && self.third_cutoff() > 0.0 && self.third_cutoff().is_finite()); + + let mut all_types_triplets = BTreeSet::new(); + for system in systems { + self.raw_triplets.ensure_computed_for_system(system)?; + + let types = system.types()?; + for triplet in self.raw_triplets.get_for_system(system)? { + if (!self.self_contributions) && triplet.is_self_contrib { + continue; + } + all_types_triplets.insert((types[triplet.atom_i], types[triplet.atom_j], types[triplet.atom_k])); + all_types_triplets.insert((types[triplet.atom_j], types[triplet.atom_i], types[triplet.atom_k])); + } + } + + let mut keys = LabelsBuilder::new(vec!["center_1_type", "center_2_type", "neighbor_type"]); + for (center1, center2, neighbor) in all_types_triplets { + keys.add(&[center1,center2, neighbor]); + } + + return Ok(keys.finish()); + } +} /// Compute a set of keys with three variables: the central atom type and two /// neighbor atom types. @@ -108,7 +154,7 @@ pub struct CenterTwoNeighborsTypesKeys { } impl KeysBuilder for CenterTwoNeighborsTypesKeys { - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { assert!(self.cutoff > 0.0 && self.cutoff.is_finite()); let mut keys = BTreeSet::new(); diff --git a/rascaline/src/labels/mod.rs b/featomic/src/labels/mod.rs similarity index 58% rename from rascaline/src/labels/mod.rs rename to featomic/src/labels/mod.rs index 7c4e6e1a9..025b1f865 100644 --- a/rascaline/src/labels/mod.rs +++ b/featomic/src/labels/mod.rs @@ -1,11 +1,11 @@ mod samples; pub use self::samples::{AtomicTypeFilter, SamplesBuilder}; -pub use self::samples::AtomCenteredSamples; +pub use self::samples::{AtomCenteredSamples,BondCenteredSamples}; pub use self::samples::LongRangeSamplesPerAtom; mod keys; pub use self::keys::KeysBuilder; pub use self::keys::CenterTypesKeys; -pub use self::keys::{CenterSingleNeighborsTypesKeys, AllTypesPairsKeys}; +pub use self::keys::{CenterSingleNeighborsTypesKeys, TwoCentersSingleNeighborsTypesKeys, AllTypesPairsKeys}; pub use self::keys::CenterTwoNeighborsTypesKeys; diff --git a/rascaline/src/labels/samples/atom_centered.rs b/featomic/src/labels/samples/atom_centered.rs similarity index 98% rename from rascaline/src/labels/samples/atom_centered.rs rename to featomic/src/labels/samples/atom_centered.rs index 96592c733..c7d520646 100644 --- a/rascaline/src/labels/samples/atom_centered.rs +++ b/featomic/src/labels/samples/atom_centered.rs @@ -28,7 +28,7 @@ impl SamplesBuilder for AtomCenteredSamples { vec!["system", "atom"] } - fn samples(&self, systems: &mut [Box]) -> Result { + fn samples(&self, systems: &mut [System]) -> Result { assert!(self.cutoff > 0.0 && self.cutoff.is_finite(), "cutoff must be positive for AtomCenteredSamples"); let mut builder = LabelsBuilder::new(Self::sample_names()); for (system_i, system) in systems.iter_mut().enumerate() { @@ -100,7 +100,7 @@ impl SamplesBuilder for AtomCenteredSamples { return Ok(builder.finish()); } - fn gradients_for(&self, systems: &mut [Box], samples: &Labels) -> Result { + fn gradients_for(&self, systems: &mut [System], samples: &Labels) -> Result { assert!(self.cutoff > 0.0 && self.cutoff.is_finite(), "cutoff must be positive for AtomCenteredSamples"); assert_eq!(samples.names(), ["system", "atom"]); let mut builder = LabelsBuilder::new(vec!["sample", "system", "atom"]); diff --git a/featomic/src/labels/samples/bond_centered.rs b/featomic/src/labels/samples/bond_centered.rs new file mode 100644 index 000000000..fe7703c4c --- /dev/null +++ b/featomic/src/labels/samples/bond_centered.rs @@ -0,0 +1,413 @@ +use std::collections::{BTreeSet,BTreeMap}; + +use metatensor::{Labels, LabelsBuilder}; + +use crate::{Error, System}; +use super::{SamplesBuilder, AtomicTypeFilter}; +use crate::systems::BATripletNeighborList; + + +/// `SampleBuilder` for bond-centered representations. This will create one +/// sample for each pair of atoms (within a spherical cutoff to each other), +/// optionally filtering on the bond's atom types. The samples names are +/// (structure", "first_center", "second_center", "bond_i"). +/// (with type(first_center)<=type(second_center)) +/// +/// Positions gradient samples include all atoms within a spherical cutoff to the bond center, +/// optionally filtering on the neighbor atom types. +pub struct BondCenteredSamples<'a> { + /// spherical cutoff radius used to construct the atom-centered environments + pub cutoffs: [f64;2], + /// Filter for the central atom types + pub center_1_type: AtomicTypeFilter, + pub center_2_type: AtomicTypeFilter, + /// Filter for the neighbor atom types + pub neighbor_type: AtomicTypeFilter, + /// Should the central atom be considered it's own neighbor? + pub self_contributions: bool, + pub raw_triplets: &'a BATripletNeighborList, +} + +impl<'a> BondCenteredSamples<'a> { + pub fn bond_cutoff(&self) -> f64 { + self.cutoffs[0] + } + pub fn third_cutoff(&self) -> f64 { + self.cutoffs[1] + } +} + +impl<'a> SamplesBuilder for BondCenteredSamples<'a> { + fn sample_names() -> Vec<&'static str> { + // bond_i is needed in case we have several bonds with the same atoms (periodic boundaries) + vec!["system", "first_atom", "second_atom", "cell_shift_a","cell_shift_b","cell_shift_c"] + } + + fn samples(&self, systems: &mut [System]) -> Result { + assert!( + self.bond_cutoff() > 0.0 && self.bond_cutoff().is_finite() && self.third_cutoff() > 0.0 && self.third_cutoff().is_finite(), + "cutoffs must be positive for BondCenteredSamples" + ); + let mut builder = LabelsBuilder::new(Self::sample_names()); + for (system_i, system) in systems.iter_mut().enumerate() { + self.raw_triplets.ensure_computed_for_system(system)?; + let types = system.types()?; + + let mut center_cache: BTreeMap<(usize,usize,[i32;3]), BTreeSet> = BTreeMap::new(); + + match (&self.center_1_type, &self.center_2_type) { + (AtomicTypeFilter::Any, AtomicTypeFilter::Any) => { + for triplet in self.raw_triplets.get_for_system(system)? { + if self.self_contributions || (!triplet.is_self_contrib){ + center_cache.entry((triplet.atom_i, triplet.atom_j, triplet.bond_cell_shift)) + .or_insert_with(BTreeSet::new) + .insert(types[triplet.atom_k]); + } + } + } + (AtomicTypeFilter::AllOf(_),_)|(_,AtomicTypeFilter::AllOf(_)) => + panic!("Cannot use AtomicTypeFilter::AllOf on BondCenteredSamples.center_types"), + (AtomicTypeFilter::Single(s1), AtomicTypeFilter::Single(s2)) => { + let types_set = BTreeSet::from_iter(types.iter()); + for s3 in types_set { + for triplet in self.raw_triplets.get_per_system_per_type(system, *s1, *s2, *s3)? { + if !self.self_contributions && triplet.is_self_contrib { + continue; + } + center_cache.entry((triplet.atom_i, triplet.atom_j, triplet.bond_cell_shift)) + .or_insert_with(BTreeSet::new) + .insert(types[triplet.atom_k]); + } + } + + }, + (selection_1, selection_2) => { + for (center_i, ¢er_1_type) in types.iter().enumerate() { + if !selection_1.matches(center_1_type) { + continue; + } + for (center_j, ¢er_2_type) in types.iter().enumerate() { + if !selection_2.matches(center_2_type) { + continue; + } + for triplet in self.raw_triplets.get_per_system_per_center(system, center_i, center_j)? { + if !self.self_contributions && triplet.is_self_contrib { + continue; + } + center_cache.entry((triplet.atom_i, triplet.atom_j, triplet.bond_cell_shift)) + .or_insert_with(BTreeSet::new) + .insert(types[triplet.atom_k]); + } + } + } + } + } + match &self.neighbor_type { + AtomicTypeFilter::Any => { + for (center_1,center_2,cell_shft) in center_cache.keys() { + builder.add(&[system_i as i32,*center_1 as i32,*center_2 as i32, cell_shft[0],cell_shft[1],cell_shft[2]]); + } + }, + AtomicTypeFilter::AllOf(requirements) => { + for ((center_1,center_2,cell_shft), neigh_set) in center_cache.iter() { + if requirements.is_subset(neigh_set) { + builder.add(&[system_i as i32,*center_1 as i32,*center_2 as i32, cell_shft[0],cell_shft[1],cell_shft[2]]); + } + } + }, + AtomicTypeFilter::Single(requirement) => { + for ((center_1,center_2,cell_shft), neigh_set) in center_cache.iter() { + if neigh_set.contains(requirement) { + builder.add(&[system_i as i32,*center_1 as i32,*center_2 as i32, cell_shft[0],cell_shft[1],cell_shft[2]]); + } + } + }, + AtomicTypeFilter::OneOf(requirements) => { + let requirements: BTreeSet = BTreeSet::from_iter(requirements.iter().map(|x|*x)); + for ((center_1,center_2,cell_shft), neigh_set) in center_cache.iter() { + if neigh_set.intersection(&requirements).count()>0 { + builder.add(&[system_i as i32,*center_1 as i32,*center_2 as i32, cell_shft[0],cell_shft[1],cell_shft[2]]); + } + } + }, + } + } + + return Ok(builder.finish()); + } + + fn gradients_for(&self, systems: &mut [System], samples: &Labels) -> Result { + assert!( + self.bond_cutoff() > 0.0 && self.bond_cutoff().is_finite() && self.third_cutoff() > 0.0 && self.third_cutoff().is_finite(), + "cutoffs must be positive for BondCenteredSamples" + ); + assert_eq!(samples.names(), ["system", "first_atom", "second_atom", "cell_shift_a","cell_shift_b","cell_shift_c"]); + let mut builder = LabelsBuilder::new(vec!["sample", "system", "atom"]); + + // we could try to find a better way to estimate this, but in the worst + // case this would only over-allocate a bit + let average_neighbors_per_atom = 10; + builder.reserve(average_neighbors_per_atom * samples.count()); + + for (sample_i, [structure_i, center_1, center_2, clsh_a,clsh_b,clsh_c]) in samples.iter_fixed_size().enumerate() { + let structure_i = structure_i.usize(); + let center_1 = center_1.usize(); + let center_2 = center_2.usize(); + let cell_shift = [clsh_a.i32(),clsh_b.i32(),clsh_c.i32()]; + + let system = &mut systems[structure_i]; + self.raw_triplets.ensure_computed_for_system(system)?; + let types = system.types()?; + + let mut grad_contributors = BTreeSet::new(); + grad_contributors.insert(center_1); + grad_contributors.insert(center_2); + + for triplet in self.raw_triplets.get_per_system_per_center(system, center_1, center_2)? { + if triplet.bond_cell_shift != cell_shift { + continue; + } + match &self.neighbor_type{ + AtomicTypeFilter::Any | AtomicTypeFilter::AllOf(_) => { + // in both of those cases, the sample already has been validated, and all known neighbors contribute + grad_contributors.insert(triplet.atom_k); + }, + neighbor_filter => { + if neighbor_filter.matches(types[triplet.atom_k]) { + grad_contributors.insert(triplet.atom_k); + } + }, + } + } + + for contrib in grad_contributors{ + builder.add(&[sample_i, structure_i, contrib]); + } + } + + return Ok(builder.finish()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::systems::test_utils::test_systems; + + #[test] + fn all_samples() { + let mut systems = test_systems(&["CH", "water"]); + let raw = BATripletNeighborList { + cutoffs: [2.0,2.0], + }; + let builder = BondCenteredSamples { + cutoffs: [2.0,2.0], + center_1_type: AtomicTypeFilter::Any, + center_2_type: AtomicTypeFilter::Any, + neighbor_type: AtomicTypeFilter::Any, + self_contributions: true, + raw_triplets: &raw, + }; + + let samples = builder.samples(&mut systems).unwrap(); + assert_eq!(samples, Labels::new( + ["system", "first_atom", "second_atom", "cell_shift_a","cell_shift_b","cell_shift_c"], + &[ + // CH + [0, 1, 0, 0,0,0], + // water, single cell + [1, 0, 1, 0,0,0], [1, 0, 2, 0,0,0], [1, 1, 2, 0,0,0], + //water, H-H bond through cell bounds + [1, 1, 2, 0,1,0] + ], + )); + + let gradient_samples = builder.gradients_for(&mut systems, &samples).unwrap(); + assert_eq!(gradient_samples, Labels::new( + ["sample", "system", "atom"], + &[ + // gradients of atoms in CH + [0, 0, 0], [0, 0, 1], + // gradients of atoms in water + [1, 1, 0], [1, 1, 1], [1, 1, 2], + [2, 1, 0], [2, 1, 1], [2, 1, 2], + [3, 1, 0], [3, 1, 1], [3, 1, 2], + [4, 1, 0], [4, 1, 1], [4, 1, 2], + ], + )); + } + + #[test] + fn filter_types_center() { + let mut systems = test_systems(&["CH", "water"]); + let raw = BATripletNeighborList { + cutoffs: [2.0,2.0], + }; + let builder = BondCenteredSamples { + cutoffs: [2.0,2.0], + center_1_type: AtomicTypeFilter::Single(6), + center_2_type: AtomicTypeFilter::Single(1), + neighbor_type: AtomicTypeFilter::Any, + self_contributions: true, + raw_triplets: &raw, + }; + + let samples = builder.samples(&mut systems).unwrap(); + assert_eq!(samples, Labels::new( + ["system", "first_atom", "second_atom", "cell_shift_a","cell_shift_b","cell_shift_c"], + &[[0, 1, 0, 0,0,0]], + )); + + let gradient_samples = builder.gradients_for(&mut systems, &samples).unwrap(); + assert_eq!(gradient_samples, Labels::new( + ["sample", "system", "atom"], + &[ + // gradients of atoms in CH + [0, 0, 0], [0, 0, 1], + ] + )); + + let builder = BondCenteredSamples { + cutoffs: [2.0,2.0], + center_1_type: AtomicTypeFilter::Single(1), + center_2_type: AtomicTypeFilter::Single(1), + neighbor_type: AtomicTypeFilter::Any, + self_contributions: true, + raw_triplets: &raw, + }; + + let samples = builder.samples(&mut systems).unwrap(); + assert_eq!(samples, Labels::new( + ["system", "first_atom", "second_atom", "cell_shift_a","cell_shift_b","cell_shift_c"], + &[[1, 1, 2, 0,0,0], [1, 1, 2, 0,1,0]], + )); + + let gradient_samples = builder.gradients_for(&mut systems, &samples).unwrap(); + assert_eq!(gradient_samples, Labels::new( + ["sample", "system", "atom"], + &[ + // gradients of atoms in H2O + [0, 1, 0], [0, 1, 1], [0, 1, 2], + [1, 1, 0], [1, 1, 1], [1, 1, 2], + ] + )); + } + + #[test] + fn filter_neighbor_type() { + let mut systems = test_systems(&["CH", "water"]); + let raw = BATripletNeighborList { + cutoffs: [2.0,2.0], + }; + let builder = BondCenteredSamples { + cutoffs: [2.0,2.0], + center_1_type: AtomicTypeFilter::Any, + center_2_type: AtomicTypeFilter::Any, + neighbor_type: AtomicTypeFilter::Single(1), + self_contributions: true, + raw_triplets: &raw, + }; + + let samples = builder.samples(&mut systems).unwrap(); + assert_eq!(samples, Labels::new( + ["system", "first_atom", "second_atom", "cell_shift_a","cell_shift_b","cell_shift_c"], + &[ + //CH + [0, 1, 0, 0,0,0], + //water, in-cell + [1, 0, 1, 0,0,0], [1, 0, 2, 0,0,0], [1, 1, 2, 0,0,0], + // water, H-H through cell boundary + [1, 1, 2, 0,1,0] + ], + )); + + let gradient_samples = builder.gradients_for(&mut systems, &samples).unwrap(); + assert_eq!(gradient_samples, Labels::new( + ["sample", "system", "atom"], + &[ + // gradients of atoms in CH w.r.t H atom only + [0, 0, 0], [0, 0, 1], + // gradients of atoms in water w.r.t H atoms only + [1, 1, 0], [1, 1, 1], [1, 1, 2], + [2, 1, 0], [2, 1, 1], [2, 1, 2], + [3, 1, 1], [3, 1, 2], + [4, 1, 1], [4, 1, 2], + ] + )); + + let builder = BondCenteredSamples { + cutoffs: [2.0,2.0], + center_1_type: AtomicTypeFilter::Any, + center_2_type: AtomicTypeFilter::Any, + neighbor_type: AtomicTypeFilter::OneOf(vec![1, 6]), + self_contributions: true, + raw_triplets: &raw, + }; + + let gradient_samples = builder.gradients_for(&mut systems, &samples).unwrap(); + assert_eq!(gradient_samples, Labels::new( + ["sample", "system", "atom"], + &[ + // gradients of atoms in CH w.r.t C and H atoms + [0, 0, 0], [0, 0, 1], + // gradients of atoms in water w.r.t H atoms only + [1, 1, 0], [1, 1, 1], [1, 1, 2], + [2, 1, 0], [2, 1, 1], [2, 1, 2], + [3, 1, 1], [3, 1, 2], + [4, 1, 1], [4, 1, 2], + ] + )); + } + + #[test] + fn partial_gradients() { + let samples = Labels::new(["system", "first_atom", "second_atom", "cell_shift_a","cell_shift_b","cell_shift_c"], &[ + [1, 1, 0, 0,0,0], + [0, 0, 1, 0,0,0], + [1, 1, 2, 0,0,0], + ]); + + let mut systems = test_systems(&["CH", "water"]); + + let raw = BATripletNeighborList { + cutoffs: [2.0,2.0], + }; + + let builder = BondCenteredSamples { + cutoffs: [2.0,2.0], + center_1_type: AtomicTypeFilter::Any, + center_2_type: AtomicTypeFilter::Any, + neighbor_type: AtomicTypeFilter::Single(-42), + self_contributions: true, + raw_triplets: &raw, + }; + + let gradients = builder.gradients_for(&mut systems, &samples).unwrap(); + assert_eq!(gradients, Labels::new(["sample", "system", "atom"], &[ + [0, 1, 0], [0, 1, 1], + [1, 0, 0], [1, 0, 1], + [2, 1, 0], [2, 1, 1], [2, 1, 2], + ])); + + let builder = BondCenteredSamples { + cutoffs: [2.0,2.0], + center_1_type: AtomicTypeFilter::Any, + center_2_type: AtomicTypeFilter::Any, + neighbor_type: AtomicTypeFilter::Single(1), + self_contributions: true, + raw_triplets: &raw, + }; + let gradients = builder.gradients_for(&mut systems, &samples).unwrap(); + assert_eq!(gradients, Labels::new( + ["sample", "system", "atom"], + &[ + // gradients of first sample, O-H1 in water + [0, 1, 0], [0, 1, 1], [0, 1, 2], + // gradients of second sample, C-H in CH + [1, 0, 0], [1, 0, 1], + // gradients of third sample, H1-H2 in water + [2, 1, 1], [2, 1, 2], + ] + )); + } +} diff --git a/rascaline/src/labels/samples/long_range.rs b/featomic/src/labels/samples/long_range.rs similarity index 94% rename from rascaline/src/labels/samples/long_range.rs rename to featomic/src/labels/samples/long_range.rs index c4ac4c4a4..2edc59de8 100644 --- a/rascaline/src/labels/samples/long_range.rs +++ b/featomic/src/labels/samples/long_range.rs @@ -22,7 +22,7 @@ impl SamplesBuilder for LongRangeSamplesPerAtom { vec!["system", "atom"] } - fn samples(&self, systems: &mut [Box]) -> Result { + fn samples(&self, systems: &mut [System]) -> Result { assert!(self.self_pairs, "self.self_pairs = false is not implemented"); let mut builder = LabelsBuilder::new(Self::sample_names()); @@ -52,7 +52,7 @@ impl SamplesBuilder for LongRangeSamplesPerAtom { return Ok(builder.finish()); } - fn gradients_for(&self, systems: &mut [Box], samples: &Labels) -> Result { + fn gradients_for(&self, systems: &mut [System], samples: &Labels) -> Result { assert_eq!(samples.names(), ["system", "atom"]); let mut builder = LabelsBuilder::new(vec!["sample", "system", "atom"]); diff --git a/rascaline/src/labels/samples/mod.rs b/featomic/src/labels/samples/mod.rs similarity index 85% rename from rascaline/src/labels/samples/mod.rs rename to featomic/src/labels/samples/mod.rs index 96bd7c9d2..d12060697 100644 --- a/rascaline/src/labels/samples/mod.rs +++ b/featomic/src/labels/samples/mod.rs @@ -29,7 +29,7 @@ impl AtomicTypeFilter { } } -/// Abstraction over the different kinds of samples used in rascaline. +/// Abstraction over the different kinds of samples used in featomic. /// /// Different implementations of this trait correspond to different types of /// samples (for example one sample for each system; or one sample for each @@ -43,17 +43,19 @@ pub trait SamplesBuilder { /// Create `Labels` containing all the samples corresponding to the given /// list of systems. - fn samples(&self, systems: &mut [Box]) -> Result; + fn samples(&self, systems: &mut [System]) -> Result; /// Create a set of `Labels` containing the gradient samples corresponding /// to the given `samples` in the given `systems`; and only these. #[allow(unused_variables)] - fn gradients_for(&self, systems: &mut [Box], samples: &Labels) -> Result; + fn gradients_for(&self, systems: &mut [System], samples: &Labels) -> Result; } mod atom_centered; pub use self::atom_centered::AtomCenteredSamples; +mod bond_centered; +pub use self::bond_centered::BondCenteredSamples; mod long_range; pub use self::long_range::LongRangeSamplesPerAtom; diff --git a/rascaline/src/lib.rs b/featomic/src/lib.rs similarity index 91% rename from rascaline/src/lib.rs rename to featomic/src/lib.rs index 41acd16dc..66a8fd474 100644 --- a/rascaline/src/lib.rs +++ b/featomic/src/lib.rs @@ -18,13 +18,17 @@ pub mod types; pub use types::*; +/// cbindgen:ignore pub mod math; +#[cfg(feature="c-api")] +pub mod c_api; + mod errors; pub use self::errors::Error; pub mod systems; -pub use self::systems::{System, SimpleSystem}; +pub use self::systems::{System, SystemBase, SimpleSystem}; pub mod labels; diff --git a/rascaline/src/math/eigen.rs b/featomic/src/math/eigen.rs similarity index 100% rename from rascaline/src/math/eigen.rs rename to featomic/src/math/eigen.rs diff --git a/rascaline/src/math/erf.rs b/featomic/src/math/erf.rs similarity index 100% rename from rascaline/src/math/erf.rs rename to featomic/src/math/erf.rs diff --git a/rascaline/src/math/exp.rs b/featomic/src/math/exp.rs similarity index 100% rename from rascaline/src/math/exp.rs rename to featomic/src/math/exp.rs diff --git a/rascaline/src/math/gamma.rs b/featomic/src/math/gamma.rs similarity index 100% rename from rascaline/src/math/gamma.rs rename to featomic/src/math/gamma.rs diff --git a/rascaline/src/math/hyp1f1.rs b/featomic/src/math/hyp1f1.rs similarity index 100% rename from rascaline/src/math/hyp1f1.rs rename to featomic/src/math/hyp1f1.rs diff --git a/rascaline/src/math/hyp2f1.rs b/featomic/src/math/hyp2f1.rs similarity index 100% rename from rascaline/src/math/hyp2f1.rs rename to featomic/src/math/hyp2f1.rs diff --git a/rascaline/src/math/k_vectors.rs b/featomic/src/math/k_vectors.rs similarity index 100% rename from rascaline/src/math/k_vectors.rs rename to featomic/src/math/k_vectors.rs diff --git a/rascaline/src/math/mod.rs b/featomic/src/math/mod.rs similarity index 88% rename from rascaline/src/math/mod.rs rename to featomic/src/math/mod.rs index cece4214f..a399deb86 100644 --- a/rascaline/src/math/mod.rs +++ b/featomic/src/math/mod.rs @@ -1,9 +1,6 @@ /// Euler's constant pub const EULER: f64 = 0.5772156649015329; -mod double_regularized_1f1; -pub(crate) use self::double_regularized_1f1::DoubleRegularized1F1; - mod eigen; pub(crate) use self::eigen::SymmetricEigen; diff --git a/rascaline/src/math/spherical_harmonics.rs b/featomic/src/math/spherical_harmonics.rs similarity index 98% rename from rascaline/src/math/spherical_harmonics.rs rename to featomic/src/math/spherical_harmonics.rs index f561b0397..e103a04e3 100644 --- a/rascaline/src/math/spherical_harmonics.rs +++ b/featomic/src/math/spherical_harmonics.rs @@ -90,7 +90,7 @@ impl std::fmt::Debug for LegendreArray { /// code like /// /// ``` -/// # use rascaline::math::SphericalHarmonicsArray; +/// # use featomic::math::SphericalHarmonicsArray; /// let mut array = SphericalHarmonicsArray::new(8); /// array[[6, 3]] = 3.0; /// array[[6, -3]] = -3.0; @@ -131,7 +131,8 @@ impl SphericalHarmonicsArray { /// size of the resulting view is `2 * l + 1`, and contains value for `m` /// from `-l` to `l` in order. #[inline] - pub fn slice(&self, l: isize) -> ArrayView1<'_, f64> { + pub fn angular_slice(&self, l: usize) -> ArrayView1<'_, f64> { + let l = l as isize; let start = self.linear_index([l, -l]); let stop = self.linear_index([l, l]); return ArrayView1::from(&self.data[start..=stop]); @@ -152,6 +153,19 @@ impl std::ops::IndexMut<[isize; 2]> for SphericalHarmonicsArray { } } +impl std::ops::Index for SphericalHarmonicsArray { + type Output = f64; + fn index(&self, index: usize) -> &f64 { + &self.data[index] + } +} + +impl std::ops::IndexMut for SphericalHarmonicsArray { + fn index_mut(&mut self, index: usize) -> &mut f64 { + &mut self.data[index] + } +} + impl std::fmt::Debug for SphericalHarmonicsArray { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "SphericalHarmonicsArray[\n l \\ m ")?; diff --git a/rascaline/src/math/splines.rs b/featomic/src/math/splines.rs similarity index 98% rename from rascaline/src/math/splines.rs rename to featomic/src/math/splines.rs index eac0ea474..99b729f5c 100644 --- a/rascaline/src/math/splines.rs +++ b/featomic/src/math/splines.rs @@ -206,10 +206,15 @@ impl HermitCubicSpline { } /// Get the number of control points in this spline - fn len(&self) -> usize { + pub fn len(&self) -> usize { self.points.len() } + /// Get the shape of the arrays returned by the splined function + pub fn shape(&self) -> &[usize] { + &self.parameters.shape + } + /// Get the position of the control points for this spline fn positions(&self) -> Vec { self.points.iter().map(|p| p.position).collect() diff --git a/featomic/src/systems/bond_atom_neighbors.rs b/featomic/src/systems/bond_atom_neighbors.rs new file mode 100644 index 000000000..145439836 --- /dev/null +++ b/featomic/src/systems/bond_atom_neighbors.rs @@ -0,0 +1,622 @@ +use std::cmp::PartialEq; +use std::collections::BTreeMap; + +use crate::systems::Pair; +use crate::{Error, System, SystemBase}; +use crate::types::Vector3D; + + +/// Sort a pair and return true if the pair was inverted +#[inline] +fn sort_pair((i, j): (T, T)) -> ((T, T), bool) { + if i <= j { + ((i, j), false) + } else { + ((j, i), true) + } +} + + +/// This object is a simple representation of a bond-atom triplet (to represent a single neighbor atom to a bond environment) +/// it only makes sense for a given system +#[derive(Debug,Clone,Copy,PartialEq)] +pub struct BATripletInfo{ + /// number of the first atom (the bond's first atom) within the system + pub atom_i: usize, + /// number of the second atom (the bond's second atom) within the system + pub atom_j: usize, + /// number of the third atom (the neighbor atom) within the system + pub atom_k: usize, + /// how many cell boundaries are crossed by the vector between the triplet's first + /// and second atoms + pub bond_cell_shift: [i32;3], + /// how many cell boundaries are crossed by the vector between the triplet's first + /// and third atoms + pub third_cell_shift: [i32;3], + /// wether or not the third atom is the same as one of the first two (and NOT a periodic image thereof) + pub is_self_contrib: bool, + /// optional: the vector between first and second atom + pub bond_vector: Vector3D, + /// optional: the bector between the bond center and the third atom + pub third_vector: Vector3D, +} + + +/// Manages a list of 'neighbors', where one neighbor is the center of a pair of atoms +/// (first and second atom), and the other neighbor is a simple atom (third atom). +/// Both the length of the bond and the distance between neighbors are subjected to a spherical cutoff. +/// This pre-calculator can compute and cache this list within a given system +/// (with two distance vectors per entry: one within the bond and one between neighbors). +/// Then, it can re-enumerate those neighbors, either for a full system, or with restrictions on the atoms or their types. +/// +/// This saves memory/computational power by only working with "half" neighbor list +/// This is done by only including one entry for each `i - j` bond, not both `i - j` and `j - i`. +/// The order of i and j is that the atom with the smallest Z (or type ID in general) comes first. +/// +/// The two first atoms must not be the same atom, but the third atom may be one of them. +/// (When periodic boundaries arise, the two first atoms may be images of each other.) +#[derive(Debug,Clone)] +#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +pub struct BATripletNeighborList { + // /// Should we compute a full neighbor list (each pair appears twice, once as + // /// `i-j` and once as `j-i`), or a half neighbor list (each pair only + // /// appears once, (such that `types_i <= types_j`)) + // pub use_half_enumeration: bool, + /// Spherical cutoffs to use to determine if two atoms are neighbors + pub cutoffs: [f64;2], // bond_, third_cutoff +} + +/// the internal function doing the triplet computing itself +fn list_raw_triplets(system: &mut dyn SystemBase, bond_cutoff: f64, third_cutoff: f64) -> Result,Error> { + system.compute_neighbors(bond_cutoff)?; + let bonds = system.pairs()?.to_owned(); + + // atoms_cutoff needs to be a bit bigger than the one in the current + // implementation to be sure we get the same set of neighbors. + system.compute_neighbors(third_cutoff + bond_cutoff/2.)?; + let types = system.types()?; + + let reorient_pair = move |b: Pair| { + if types[b.first] <= types[b.second] { + b + } else { + Pair{ + first: b.second, + second: b.first, + distance: b.distance, + vector: -b.vector, + cell_shift_indices: b.cell_shift_indices, // not corrected because irrelevant here + } + } + }; + + let mut ba_triplets = vec![]; + for bond in bonds.into_iter().map(reorient_pair) { + let halfbond = 0.5 * bond.vector; + + // first, record the self contribution + { + let ((pairatom_i,pairatom_j),inverted) = sort_pair((bond.first,bond.second)); + let (halfbond, to_i, to_j) = if inverted { + (-halfbond, bond.cell_shift_indices, [0;3]) + } else { + (halfbond, [0;3], bond.cell_shift_indices) + }; + + let mut tri = BATripletInfo{ + atom_i: bond.first, atom_j: bond.second, atom_k: pairatom_i, + bond_cell_shift: bond.cell_shift_indices, + third_cell_shift: to_i, + bond_vector: bond.vector, + third_vector: -halfbond, + is_self_contrib: true, + }; + ba_triplets.push(tri.clone()); + tri.atom_k = pairatom_j; + tri.third_vector = halfbond; + tri.third_cell_shift = to_j; + ba_triplets.push(tri); + } + + + // note: pairs_containing gives the pairs which have that center, as first OR second + // but the full list of pairs only forms "an upper triangle" of pairs. + for one_three in system.pairs_containing(bond.first)?.iter().map(|p|reorient_pair(p.clone())) { + + let is_self_contrib = { + //ASSUMPTION: is_self_contrib means that bond and one_three are the exact same object + (bond.vector-one_three.vector).norm2() <1E-5 && + bond.second == one_three.second + }; + if is_self_contrib{ + //debug_assert_eq!(&bond as *const Pair, one_three as *const Pair); // they come from different allocations lol + debug_assert_eq!( + (bond.first, bond.second, bond.cell_shift_indices), + (one_three.first, one_three.second, one_three.cell_shift_indices), + ); + continue; + } + + // note: since we are looking for neighbors that can be distinguished (a pair of atoms and a lone atoms) + // we need to ensure undo the anti-double-counting protections from system.pairs, when it comes to self-image pairs + // hense, two separate if blocks rather than an if/else pair. + if one_three.first == bond.first { + let (third,third_vector, to_k) = (one_three.second, one_three.vector - halfbond, one_three.cell_shift_indices); + if third_vector.norm2() < third_cutoff*third_cutoff { + let tri = BATripletInfo{ + atom_i: bond.first, atom_j: bond.second, atom_k: third, + bond_cell_shift: bond.cell_shift_indices, + third_cell_shift: to_k, + bond_vector: bond.vector, + third_vector, + is_self_contrib: false, + }; + ba_triplets.push(tri); + } + } + if one_three.second == bond.first { + let (third,third_vector, to_k) = (one_three.first, -one_three.vector - halfbond, one_three.cell_shift_indices.map(|f|-f)); + if third_vector.norm2() < third_cutoff*third_cutoff { + let tri = BATripletInfo{ + atom_i: bond.first, atom_j: bond.second, atom_k: third, + bond_cell_shift: bond.cell_shift_indices, + third_cell_shift: to_k, + bond_vector: bond.vector, + third_vector, + is_self_contrib: false, + }; + ba_triplets.push(tri); + } + }; + } + } + Ok(ba_triplets) +} + +impl BATripletNeighborList { + const CACHE_NAME_ATTR: &'static str = "bond_atom_triplets_cutoffs"; + const CACHE_NAME1: &'static str = "bond_atom_triplets_raw_list"; + const CACHE_NAME2: &'static str = "bond_atom_triplets_types_LUT"; + const CACHE_NAME3: &'static str = "bond_atom_triplets_center_LUT"; + //type CACHE_TYPE1 = TensorBlock; + //type CACHE_TYPE2 = BTreeMap<(i32,i32,i32),Vec>; + //type CACHE_TYPE3 = Vec>>; + + /// get the cutoff distance for the selection of bonds + pub fn bond_cutoff(&self)-> f64 { + self.cutoffs[0] + } + /// get the cutoff distance for neighbours to the center of a bond + pub fn third_cutoff(&self)-> f64 { + self.cutoffs[1] + } + + /// validate that the cutoffs make sense + pub fn validate_cutoffs(&self) { + let (bond_cutoff, third_cutoff) = (self.bond_cutoff(), self.third_cutoff()); + assert!(bond_cutoff > 0.0 && bond_cutoff.is_finite()); + assert!(third_cutoff >= bond_cutoff && third_cutoff.is_finite()); + } + + /// internal function that deletages computing the triplets, but deals with storing them for a given system. + fn do_compute_for_system(&self, system: &mut System) -> Result<(), Error> { + let triplets = list_raw_triplets(&mut **system, self.cutoffs[0], self.cutoffs[1])?; + + let types = system.types()?; // calling this again so the previous borrow expires + let mut triplets_by_types = BTreeMap::new(); + let mut triplets_by_center = { + let sz = system.size()?; + (0..sz).map(|i|vec![vec![];i+1]).collect::>()//vec![vec![vec![];sz];sz] + }; + for (triplet_i, triplet) in triplets.iter().enumerate() { + let ((s1,s2),_) = sort_pair((types[triplet.atom_i],types[triplet.atom_j])); + triplets_by_types.entry((s1,s2,types[triplet.atom_k])) + .or_insert_with(Vec::new) + .push(triplet_i); + if triplet.atom_i >= triplet.atom_j{ + triplets_by_center[triplet.atom_i][triplet.atom_j].push(triplet_i); + } else { + triplets_by_center[triplet.atom_j][triplet.atom_i].push(triplet_i); + } + // triplets_by_types.entry((types[triplet.bond.first],types[triplet.bond.second],types[triplet.third])) + // .or_insert_with(Vec::new) + // .push(triplet_i); + // if self.use_half_enumeration { + // if sort_pair((types[triplet.bond.first], types[triplet.bond.second])).1 { + // triplets_by_center[triplet.bond.second][triplet.bond.first].push(triplet_i); + // } else { + // triplets_by_center[triplet.bond.first][triplet.bond.second].push(triplet_i); + // } + // } else { + // triplets_by_center[triplet.bond.first][triplet.bond.second].push(triplet_i); + // triplets_by_center[triplet.bond.second][triplet.bond.first].push(triplet_i); + // } + + } + system.store_data(Self::CACHE_NAME2.into(),triplets_by_types); + system.store_data(Self::CACHE_NAME3.into(),triplets_by_center); + system.store_data(Self::CACHE_NAME_ATTR.into(),self.cutoffs); + system.store_data(Self::CACHE_NAME1.into(),triplets); + Ok(()) + } + + /// check that the precalculator has computed its values for a given system, + /// and if not, compute them. + pub fn ensure_computed_for_system(&self, system: &mut System) -> Result<(),Error> { + self.validate_cutoffs(); + 'cached_path: { + let cutoffs2: &[f64;2] = match system.data(Self::CACHE_NAME_ATTR.into()) { + Some(cutoff) => cutoff.downcast_ref() + .ok_or_else(||Error::Internal("Failed to downcast cache".into()))?, + None => break 'cached_path, + }; + if cutoffs2 == &self.cutoffs { + return Ok(()); + } else { + break 'cached_path + } + } + // got out of the 'cached' path: need to compute this ourselves + return self.do_compute_for_system(system); + } + + /// for a given system, get a reference to all the bond-atom triplets, vectors included + pub fn get_for_system<'a>(&self, system: &'a System) -> Result<&'a [BATripletInfo], Error>{ + let triplets: &Vec = system.data(&Self::CACHE_NAME1) + .ok_or_else(||Error::Internal("triplets not yet computed".into()))? + .downcast_ref().ok_or_else(||{Error::Internal("Failed to downcast cache".into())})?; + Ok(triplets) + } + + + fn get_types_lut<'a>(&self, system: &'a System, s1:i32, s2:i32, s3:i32) -> Result<&'a [usize],Error> { + let full_lut: &BTreeMap<(i32,i32,i32),Vec> = system.data(&Self::CACHE_NAME2) + .ok_or_else(||Error::Internal("triplets not yet computed".into()))? + .downcast_ref().ok_or_else(||{Error::Internal("Failed to downcast cache".into())})?; + + let ((s1,s2),_) = sort_pair((s1,s2)); + Ok(match full_lut.get(&(s1,s2,s3)) { + None => &[], + Some(lut) => &lut[..], + }) + } + fn get_centers_lut<'a>(&self, system: &'a System, c1:usize, c2:usize) -> Result<&'a [usize], Error> { + { + let sz = system.size()?; + if c1 >= sz || c2 >= sz { + return Err(Error::InvalidParameter("center ID too high for system".into())); + } + } + let full_lut: &Vec>> = system.data(&Self::CACHE_NAME3) + .ok_or_else(||Error::Internal("triplets not yet computed".into()))? + .downcast_ref().ok_or_else(||{Error::Internal("Failed to downcast cache".into())})?; + if c1 >= c2 { + Ok(&full_lut[c1][c2]) + } else { + Ok(&full_lut[c2][c1]) + } + } + + /// for a given system, get a reference to the bond-atom triplets of given set of atomic types. + /// note: inverting s1 and s2 does not change the result, and the returned triplets may have these types swapped + pub fn get_per_system_per_type<'a>( + &self, system: &'a System, + s1:i32,s2:i32,s3:i32 + ) -> Result + 'a, Error> { + let triplets = self.get_for_system(system)?; + let types_lut = self.get_types_lut(system, s1, s2, s3)?; + + let res = types_lut.iter().map(|triplet_i|{ + triplets.get(*triplet_i).unwrap() + }); + Ok(res) + } + + /// for a given system, get a reference to the bond-atom triplets of given set of atomic types. + /// note: the triplets may be for (c2,c1) rather than (c1,c2) + pub fn get_per_system_per_center<'a>( + &self, system: &'a System, + c1:usize,c2:usize + ) -> Result + 'a, Error>{ + let triplets = self.get_for_system(system)?; + let centers_lut = self.get_centers_lut(system, c1, c2)?; + + let res = centers_lut.iter().map(|triplet_i|{ + triplets.get(*triplet_i).unwrap() + }); + Ok(res) + } + + /// for a given system, get a reference to the bond-atom triplets of given set of atomic types. + /// plus the number of each triplet + /// note: inverting s1 and s2 does not change the result, and the returned triplets may have these types swapped + pub fn get_per_system_per_type_enumerated<'a>( + &self, system: &'a System, + s1:i32,s2:i32,s3:i32 + ) -> Result + 'a, Error> { + let triplets = self.get_for_system(system)?; + let types_lut = self.get_types_lut(system, s1, s2, s3)?; + + let res = types_lut.iter().map(|triplet_i|{ + (*triplet_i,triplets.get(*triplet_i).unwrap()) + }); + Ok(res) + } + + /// for a given system, get a reference to the bond-atom triplets of given set of atomic types. + /// plus the number of each triplet + /// note: the triplets may be for (c2,c1) rather than (c1,c2) + pub fn get_per_system_per_center_enumerated<'a>( + &self, system: &'a System, + c1:usize,c2:usize + ) -> Result + 'a, Error>{ + let triplets = self.get_for_system(system)?; + let centers_lut = self.get_centers_lut(system, c1, c2)?; + + let res = centers_lut.iter().map(|triplet_i|{ + (*triplet_i,triplets.get(*triplet_i).unwrap()) + }); + Ok(res) + } + +} + + + +#[cfg(test)] +mod tests { + use approx::assert_ulps_eq; + use crate::systems::test_utils::test_systems; + //use crate::Matrix3; + use super::*; + + + fn no_vector(mut t: BATripletInfo) -> BATripletInfo { + t.bond_vector =Vector3D::new(0.,0.,0.); + t.third_vector =Vector3D::new(0.,0.,0.); + t + } + fn gen_triplet(atom_i: usize, atom_j: usize, atom_k:usize, is_self_contrib:bool) -> BATripletInfo { + BATripletInfo{ + atom_i,atom_j,atom_k, + is_self_contrib, + bond_vector: Vector3D::new(0.,0.,0.), + third_vector: Vector3D::new(0.,0.,0.), + bond_cell_shift: [0;3], + third_cell_shift: [0;3], + } + } + + fn post_process_triplets<'a>(triplets: impl Iterator) -> Vec{ + // needed for now to avoid tripling the number of triplets to take into account + triplets.filter(|v| v.bond_cell_shift == [0,0,0] && v.third_cell_shift == [0,0,0]) + .map(|v|no_vector(v.clone())) + .collect::>() + } + + #[test] + fn simple_enum() { + let mut tsysv = test_systems(&["water"]); + let precalc = BATripletNeighborList{ + cutoffs: [3.,3.], + }; + precalc.ensure_computed_for_system(&mut tsysv[0]).unwrap(); + + // /// ensure the enumeration is correct + let triplets = post_process_triplets(precalc.get_for_system(&mut tsysv[0]).unwrap().into_iter()); + assert_eq!(triplets, vec![ + gen_triplet(0,1,0, true,), + gen_triplet(0,1,1, true,), + gen_triplet(0,1,2, false,), + gen_triplet(0,2,0, true,), + gen_triplet(0,2,2, true,), + gen_triplet(0,2,1, false,), + gen_triplet(1,2,1, true,), + gen_triplet(1,2,2, true,), + gen_triplet(1,2,0, false,), + ]); + + // /// ensure the per-center enumeration is correct + let triplets = post_process_triplets(precalc.get_per_system_per_center(&mut tsysv[0], 0,1).unwrap().into_iter()); + assert_eq!(triplets, vec![ + gen_triplet(0,1,0, true,), + gen_triplet(0,1,1, true,), + gen_triplet(0,1,2, false,), + ]); + let triplets = post_process_triplets(precalc.get_per_system_per_center(&mut tsysv[0], 1,0).unwrap().into_iter()); + assert_eq!(triplets, vec![ + gen_triplet(0,1,0, true,), + gen_triplet(0,1,1, true,), + gen_triplet(0,1,2, false,), + ]); + let triplets = post_process_triplets(precalc.get_per_system_per_center(&mut tsysv[0], 0,2).unwrap().into_iter()); + assert_eq!(triplets, vec![ + gen_triplet(0,2,0, true,), + gen_triplet(0,2,2, true,), + gen_triplet(0,2,1, false,), + ]); + let triplets = post_process_triplets(precalc.get_per_system_per_center(&mut tsysv[0], 2,0).unwrap().into_iter()); + assert_eq!(triplets, vec![ + gen_triplet(0,2,0, true,), + gen_triplet(0,2,2, true,), + gen_triplet(0,2,1, false,), + ]); + let triplets = post_process_triplets(precalc.get_per_system_per_center(&mut tsysv[0], 1,2).unwrap().into_iter()); + assert_eq!(triplets, vec![ + gen_triplet(1,2,1, true,), + gen_triplet(1,2,2, true,), + gen_triplet(1,2,0, false,), + ]); + let triplets = post_process_triplets(precalc.get_per_system_per_center(&mut tsysv[0], 2,1).unwrap().into_iter()); + assert_eq!(triplets, vec![ + gen_triplet(1,2,1, true,), + gen_triplet(1,2,2, true,), + gen_triplet(1,2,0, false,), + ]); + + // /// ensure the per-type enumeration is correct + let triplets = post_process_triplets(precalc.get_per_system_per_type(&mut tsysv[0], 1,1, -42).unwrap().into_iter()); + assert_eq!(triplets, vec![ + gen_triplet(1,2,0, false,), + ]); + let triplets = post_process_triplets(precalc.get_per_system_per_type(&mut tsysv[0], 1,1, 1).unwrap().into_iter()); + assert_eq!(triplets, vec![ + gen_triplet(1,2,1, true,), + gen_triplet(1,2,2, true,), + ]); + let triplets = post_process_triplets(precalc.get_per_system_per_type(&mut tsysv[0], 1,-42, 1).unwrap().into_iter()); + assert_eq!(triplets, vec![ + gen_triplet(0,1,1, true,), + gen_triplet(0,1,2, false,), + gen_triplet(0,2,2, true,), + gen_triplet(0,2,1, false,), + ]); + let triplets = post_process_triplets(precalc.get_per_system_per_type(&mut tsysv[0], -42,1, 1).unwrap().into_iter()); + assert_eq!(triplets, vec![ + gen_triplet(0,1,1, true,), + gen_triplet(0,1,2, false,), + gen_triplet(0,2,2, true,), + gen_triplet(0,2,1, false,), + ]); + + // ///// deal with the vectors + + let triplets = precalc.get_for_system(&mut tsysv[0]).unwrap(); + let (bondvecs, thirdvecs): (Vec<_>,Vec<_>) = triplets.into_iter() + // needed for now to avoid tripling the number of triplets to take into account + .filter(|v| v.bond_cell_shift == [0,0,0] && v.third_cell_shift == [0,0,0]) + .map(|t|(t.bond_vector,t.third_vector)) + .unzip(); + + bondvecs.into_iter().map(|v|(v[0],v[1],v[2])) + .zip(vec![ + (0.0, 0.75545, -0.58895), + (0.0, 0.75545, -0.58895), + (0.0, 0.75545, -0.58895), + (0.0, -0.75545, -0.58895), + (0.0, -0.75545, -0.58895), + (0.0, -0.75545, -0.58895), + (0.0, -1.5109, 0.0), + (0.0, -1.5109, 0.0), + (0.0, -1.5109, 0.0), + ].into_iter()) + .map(|(v1,v2)|{ + assert_ulps_eq!(v1.0,v2.0); + assert_ulps_eq!(v1.1,v2.1); + assert_ulps_eq!(v1.2,v2.2); + }).last(); + + thirdvecs.into_iter() + .map(|v|(v[0],v[1],v[2])) + .zip(vec![ + (0.0, -0.377725, 0.294475), + (0.0, 0.377725, -0.294475), + (0.0, -1.133175, -0.294475), + (0.0, 0.377725, 0.294475), + (0.0, -0.377725, -0.294475), + (0.0, 1.133175, -0.294475), + (0.0, 0.75545, 0.0), + (0.0, -0.75545, 0.0), + (0.0, 0.0, 0.58895), + ].into_iter()) + .map(|(v1,v2)|{ + assert_ulps_eq!(v1.0,v2.0); + assert_ulps_eq!(v1.1,v2.1); + assert_ulps_eq!(v1.2,v2.2); + }).last(); + } + + // #[test] + // fn full_enum() { + // let mut tsysv = test_systems(&["water"]); + // let precalc = BATripletNeighborList{ + // cutoffs: [6.,6.], + // use_half_enumeration: false, + // }; + + // precalc.ensure_computed_for_system(&mut tsysv[0]).unwrap(); + // let triplets = precalc.get_for_system(&mut tsysv[0], false).unwrap(); + // assert_eq!(triplets, vec![ + // BATripletInfo{atom_i:0,atom_j:1,atom_k:0,bond_i:0,triplet_i:0,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:1,atom_k:0,bond_i:0,triplet_i:0,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:1,atom_k:1,bond_i:0,triplet_i:1,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:1,atom_k:1,bond_i:0,triplet_i:1,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:1,atom_k:2,bond_i:0,triplet_i:2,is_self_contrib:false,bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:1,atom_k:2,bond_i:0,triplet_i:2,is_self_contrib:false,bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:2,atom_k:0,bond_i:1,triplet_i:3,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:2,atom_k:0,bond_i:1,triplet_i:3,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:2,atom_k:1,bond_i:1,triplet_i:4,is_self_contrib:false,bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:2,atom_k:1,bond_i:1,triplet_i:4,is_self_contrib:false,bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:2,atom_k:2,bond_i:1,triplet_i:5,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:2,atom_k:2,bond_i:1,triplet_i:5,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:1,atom_j:2,atom_k:0,bond_i:2,triplet_i:6,is_self_contrib:false,bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:1,atom_j:2,atom_k:0,bond_i:2,triplet_i:6,is_self_contrib:false,bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:1,atom_j:2,atom_k:1,bond_i:2,triplet_i:7,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:1,atom_j:2,atom_k:1,bond_i:2,triplet_i:7,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:1,atom_j:2,atom_k:2,bond_i:2,triplet_i:8,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:1,atom_j:2,atom_k:2,bond_i:2,triplet_i:8,is_self_contrib:true, bond_vector:None,third_vector:None}, + // ]); + + // let triplets = precalc.get_per_system_per_center(&mut tsysv[0], 0,1,false).unwrap(); + // assert_eq!(triplets, vec![ + // BATripletInfo{atom_i:0,atom_j:1,atom_k:0,bond_i:0,triplet_i:0,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:1,atom_k:1,bond_i:0,triplet_i:1,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:1,atom_k:2,bond_i:0,triplet_i:2,is_self_contrib:false,bond_vector:None,third_vector:None}, + // ]); + // let triplets = precalc.get_per_system_per_center(&mut tsysv[0], 1,0,false).unwrap(); + // assert_eq!(triplets, vec![ + // BATripletInfo{atom_i:0,atom_j:1,atom_k:0,bond_i:0,triplet_i:0,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:1,atom_k:1,bond_i:0,triplet_i:1,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:1,atom_k:2,bond_i:0,triplet_i:2,is_self_contrib:false,bond_vector:None,third_vector:None}, + // ]); + // let triplets = precalc.get_per_system_per_center(&mut tsysv[0], 0,2,false).unwrap(); + // assert_eq!(triplets, vec![ + // BATripletInfo{atom_i:0,atom_j:2,atom_k:0,bond_i:1,triplet_i:3,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:2,atom_k:1,bond_i:1,triplet_i:4,is_self_contrib:false,bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:2,atom_k:2,bond_i:1,triplet_i:5,is_self_contrib:true, bond_vector:None,third_vector:None}, + // ]); + // let triplets = precalc.get_per_system_per_center(&mut tsysv[0], 2,0,false).unwrap(); + // assert_eq!(triplets, vec![ + // BATripletInfo{atom_i:0,atom_j:2,atom_k:0,bond_i:1,triplet_i:3,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:2,atom_k:1,bond_i:1,triplet_i:4,is_self_contrib:false,bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:2,atom_k:2,bond_i:1,triplet_i:5,is_self_contrib:true, bond_vector:None,third_vector:None}, + // ]); + // let triplets = precalc.get_per_system_per_center(&mut tsysv[0], 1,2,false).unwrap(); + // assert_eq!(triplets, vec![ + // BATripletInfo{atom_i:1,atom_j:2,atom_k:0,bond_i:2,triplet_i:6,is_self_contrib:false,bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:1,atom_j:2,atom_k:1,bond_i:2,triplet_i:7,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:1,atom_j:2,atom_k:2,bond_i:2,triplet_i:8,is_self_contrib:true, bond_vector:None,third_vector:None}, + // ]); + // let triplets = precalc.get_per_system_per_center(&mut tsysv[0], 2,1,false).unwrap(); + // assert_eq!(triplets, vec![ + // BATripletInfo{atom_i:1,atom_j:2,atom_k:0,bond_i:2,triplet_i:6,is_self_contrib:false,bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:1,atom_j:2,atom_k:1,bond_i:2,triplet_i:7,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:1,atom_j:2,atom_k:2,bond_i:2,triplet_i:8,is_self_contrib:true, bond_vector:None,third_vector:None}, + // ]); + + // // /// ensure the per-type enumeration is correct + // let triplets = precalc.get_per_system_per_type(&mut tsysv[0], 1,1, -42,false).unwrap(); + // assert_eq!(triplets, vec![ + // BATripletInfo{atom_i:1,atom_j:2,atom_k:0,bond_i:2,triplet_i:6,is_self_contrib:false,bond_vector:None,third_vector:None}, + // ]); + // let triplets = precalc.get_per_system_per_type(&mut tsysv[0], 1,1, 1,false).unwrap(); + // assert_eq!(triplets, vec![ + // BATripletInfo{atom_i:1,atom_j:2,atom_k:1,bond_i:2,triplet_i:7,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:1,atom_j:2,atom_k:2,bond_i:2,triplet_i:8,is_self_contrib:true, bond_vector:None,third_vector:None}, + // ]); + // let triplets = precalc.get_per_system_per_type(&mut tsysv[0], 1,-42, 1,false).unwrap(); + // assert_eq!(triplets, vec![ + // BATripletInfo{atom_i:0,atom_j:1,atom_k:1,bond_i:0,triplet_i:1,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:1,atom_k:2,bond_i:0,triplet_i:2,is_self_contrib:false,bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:2,atom_k:1,bond_i:1,triplet_i:4,is_self_contrib:false,bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:2,atom_k:2,bond_i:1,triplet_i:5,is_self_contrib:true, bond_vector:None,third_vector:None}, + // ]); + // let triplets = precalc.get_per_system_per_type(&mut tsysv[0], -42,1, 1,false).unwrap(); + // assert_eq!(triplets, vec![ + // BATripletInfo{atom_i:0,atom_j:1,atom_k:1,bond_i:0,triplet_i:1,is_self_contrib:true, bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:1,atom_k:2,bond_i:0,triplet_i:2,is_self_contrib:false,bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:2,atom_k:1,bond_i:1,triplet_i:4,is_self_contrib:false,bond_vector:None,third_vector:None}, + // BATripletInfo{atom_i:0,atom_j:2,atom_k:2,bond_i:1,triplet_i:5,is_self_contrib:true, bond_vector:None,third_vector:None}, + // ]); + // } +} diff --git a/rascaline/src/systems/cell.rs b/featomic/src/systems/cell.rs similarity index 99% rename from rascaline/src/systems/cell.rs rename to featomic/src/systems/cell.rs index b3634f551..4f1b43164 100644 --- a/rascaline/src/systems/cell.rs +++ b/featomic/src/systems/cell.rs @@ -364,7 +364,7 @@ mod tests { #[test] fn negative_determinant_cell() { - // https://github.com/Luthaf/rascaline/issues/215 + // https://github.com/metatensor/featomic/issues/215 let cell = UnitCell::from(Matrix3::new([ [3.83835964, 0.01566142, 0.0], [-0.04761064, 5.55603676, 0.0], diff --git a/featomic/src/systems/chemfiles.rs b/featomic/src/systems/chemfiles.rs new file mode 100644 index 000000000..d9943cd94 --- /dev/null +++ b/featomic/src/systems/chemfiles.rs @@ -0,0 +1,46 @@ +use std::collections::HashMap; + +use crate::Matrix3; +use crate::systems::UnitCell; + +use super::{System, SimpleSystem}; + +impl<'a> From<&'a chemfiles::Frame> for System { + fn from(frame: &chemfiles::Frame) -> Self { + let mut assigned_types = HashMap::new(); + let mut get_atomic_type = |atom: chemfiles::AtomRef| { + let atomic_number = atom.atomic_number(); + if atomic_number == 0 { + // use number assigned from the the atomic type, starting at 120 + // since that's larger than the number of elements in the + // periodic table + let new_type = 120 + assigned_types.len() as i32; + *assigned_types.entry(atom.atomic_type()).or_insert(new_type) + } else { + atomic_number as i32 + } + }; + let positions = frame.positions(); + + let cell = if frame.cell().shape() == chemfiles::CellShape::Infinite { + UnitCell::infinite() + } else { + // transpose since chemfiles is using columns for the cell vectors and + // we want rows as cell vectors + UnitCell::from(Matrix3::from(frame.cell().matrix()).transposed()) + }; + let mut system = SimpleSystem::new(cell); + for i in 0..frame.size() { + let atom = frame.atom(i); + system.add_atom(get_atomic_type(atom), positions[i].into()); + } + + return System::new(system); + } +} + +impl From for System { + fn from(frame: chemfiles::Frame) -> Self { + return (&frame).into(); + } +} diff --git a/rascaline/src/systems/mod.rs b/featomic/src/systems/mod.rs similarity index 62% rename from rascaline/src/systems/mod.rs rename to featomic/src/systems/mod.rs index 6e3252a11..0ce01bbcf 100644 --- a/rascaline/src/systems/mod.rs +++ b/featomic/src/systems/mod.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use crate::{Error, Vector3D}; mod cell; @@ -6,17 +8,20 @@ pub use self::cell::{UnitCell, CellShape}; mod neighbors; pub use self::neighbors::NeighborsList; +mod bond_atom_neighbors; +pub use bond_atom_neighbors::{BATripletInfo,BATripletNeighborList}; + mod simple_system; pub use self::simple_system::SimpleSystem; +#[cfg(feature = "chemfiles")] mod chemfiles; -pub use self::chemfiles::read_from_file; #[cfg(test)] pub(crate) mod test_utils; /// Pair of atoms coming from a neighbor list. -// WARNING: any change to this definition MUST be reflected in rascal_pair_t as +// WARNING: any change to this definition MUST be reflected in featomic_pair_t as // well #[repr(C)] #[derive(Debug, Clone, Copy, PartialEq)] @@ -36,9 +41,9 @@ pub struct Pair { pub cell_shift_indices: [i32; 3], } -/// A `System` deals with the storage of atoms and related information, as well +/// A `SystemBase` deals with the storage of atoms and related information, as well /// as the computation of neighbor lists. -pub trait System: Send + Sync { +pub trait SystemBase: Send + Sync { /// Get the unit cell for this system fn cell(&self) -> Result; @@ -57,7 +62,7 @@ pub trait System: Send + Sync { fn positions(&self) -> Result<&[Vector3D], Error>; /// Compute the neighbor list according to the given cutoff, and store it - /// for later access with `pairs` or `pairs_around`. + /// for later access with `pairs`, or `pairs_containing`. fn compute_neighbors(&mut self, cutoff: f64) -> Result<(), Error>; /// Get the list of pairs in this system. This list of pair should only @@ -75,3 +80,53 @@ pub trait System: Send + Sync { /// `pairs_containing(j)`. fn pairs_containing(&self, atom: usize) -> Result<&[Pair], Error>; } + +/// A `System` deals with the storage of atoms and related information, as well +/// as the computation of neighbor lists. +/// +/// `System` also allows calculator to store arbitrary data inside the system +/// (to be used a a cache between different function calls in the +/// `CalculatorBase` trait). +pub struct System { + implementation: Box, + data: BTreeMap>, +} + +impl std::ops::Deref for System { + type Target = dyn SystemBase + 'static; + + fn deref(&self) -> &Self::Target { + &*self.implementation + } +} + +impl std::ops::DerefMut for System { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut *self.implementation + } +} + +impl System { + /// TODO + pub fn new(system: impl SystemBase + 'static) -> System { + System { + implementation: Box::new(system), + data: BTreeMap::new() + } + } + + /// TODO + pub fn store_data(&mut self, name: String, data: impl std::any::Any + Send + Sync + 'static) { + self.data.insert(name, Box::new(data)); + } + + /// TODO + pub fn data(&self, name: &str) -> Option<&(dyn std::any::Any + Send + Sync)> { + self.data.get(name).map(|v| &**v) + } + + /// TODO + pub fn data_mut(&mut self, name: &str) -> Option<&mut (dyn std::any::Any + Send + Sync)> { + self.data.get_mut(name).map(|v| &mut **v) + } +} diff --git a/rascaline/src/systems/neighbors.rs b/featomic/src/systems/neighbors.rs similarity index 95% rename from rascaline/src/systems/neighbors.rs rename to featomic/src/systems/neighbors.rs index 9fa0f155d..200b1a54d 100644 --- a/rascaline/src/systems/neighbors.rs +++ b/featomic/src/systems/neighbors.rs @@ -79,14 +79,14 @@ pub struct AtomData { shift: CellShift, } -/// The cell list is used to sort atoms inside bins/cells. +/// The cell list is used to sort atoms inside bins/subcells. (of the unit cell or of infinite space) /// /// The list of potential pairs is then constructed by looking through all /// neighboring cells (the number of cells to search depends on the cutoff and /// the size of the cells) for each atom to create pair candidates. #[derive(Debug, Clone)] pub struct CellList { - /// How many cells do we need to look at when searching neighbors to include + /// How many subcells do we need to look at when searching neighbors to include /// all neighbors below cutoff n_search: [i32; 3], /// the cells themselves @@ -107,6 +107,7 @@ impl CellList { unit_cell.distances_between_faces() }; + // number of subcells per unit cell let mut n_cells = [ f64::clamp(f64::trunc(distances_between_faces[0] / cutoff), 1.0, f64::INFINITY), f64::clamp(f64::trunc(distances_between_faces[1] / cutoff), 1.0, f64::INFINITY), @@ -129,7 +130,7 @@ impl CellList { n_cells[0] = f64::trunc(ratio_x_y * n_cells[1]); } - // number of cells to search in each direction to make sure all possible + // number of subcells to search in each direction to make sure all possible // pairs below the cutoff are accounted for. let mut n_search = [ f64::ceil(cutoff * n_cells[0] / distances_between_faces[0]) as i32, @@ -156,14 +157,14 @@ impl CellList { } CellList { - n_search: n_search, + n_search, cells: Array3::from_elem(n_cells, Default::default()), - unit_cell: unit_cell, + unit_cell, } } - /// Add a single atom to the cell list at the given `position`. The atom is - /// uniquely identified by its `index`. + /// Add a single atom to the cell list at the given `position`. + /// ASSUMPTION: The atom is *uniquely* identified by its `index`. pub fn add_atom(&mut self, index: usize, position: Vector3D) { let fractional = if self.unit_cell.is_infinite() { position @@ -195,7 +196,7 @@ impl CellList { }; self.cells[cell_index].push(AtomData { - index: index, + index, shift: CellShift(shift), }); } @@ -294,7 +295,7 @@ impl CellList { pairs.push(CellPair { first: atom_i.index, second: atom_j.index, - shift: shift, + shift, }); } } // loop over atoms in current neighbor cells @@ -378,7 +379,7 @@ impl NeighborsList { first: pair.first, second: pair.second, distance: distance2.sqrt(), - vector: vector, + vector, cell_shift_indices: pair.shift.0 }; @@ -388,16 +389,19 @@ impl NeighborsList { } } - // sort the pairs to make sure the final output of rascaline is ordered + // sort the pairs to make sure the final output of featomic is ordered // naturally pairs.sort_unstable_by_key(|pair| (pair.first, pair.second)); - for pairs in &mut pairs_by_center { - pairs.sort_unstable_by_key(|pair| (pair.first, pair.second)); + + let mut pairs_by_center = vec![Vec::new(); positions.len()]; + for pair in pairs.iter() { + pairs_by_center[pair.first].push(pair.clone()); + pairs_by_center[pair.second].push(pair.clone()); } return NeighborsList { - cutoff: cutoff, - pairs: pairs, + cutoff, + pairs, pairs_by_atom: pairs_by_center, }; } diff --git a/rascaline/src/systems/simple_system.rs b/featomic/src/systems/simple_system.rs similarity index 93% rename from rascaline/src/systems/simple_system.rs rename to featomic/src/systems/simple_system.rs index 4af856ae2..c14042048 100644 --- a/rascaline/src/systems/simple_system.rs +++ b/featomic/src/systems/simple_system.rs @@ -1,6 +1,6 @@ use crate::Error; -use super::{UnitCell, System, Vector3D, Pair}; +use super::{UnitCell, SystemBase, Vector3D, Pair}; use super::neighbors::NeighborsList; @@ -45,7 +45,7 @@ impl SimpleSystem { } } -impl System for SimpleSystem { +impl SystemBase for SimpleSystem { fn size(&self) -> Result { Ok(self.types.len()) } @@ -90,10 +90,10 @@ impl System for SimpleSystem { } } -impl std::convert::TryFrom<&dyn System> for SimpleSystem { +impl std::convert::TryFrom<&dyn SystemBase> for SimpleSystem { type Error = Error; - fn try_from(system: &dyn System) -> Result { + fn try_from(system: &dyn SystemBase) -> Result { let mut new = SimpleSystem::new(system.cell()?); for (&atomic_type, &position) in system.types()?.iter().zip(system.positions()?) { new.add_atom(atomic_type, position); diff --git a/rascaline/src/systems/test_utils.rs b/featomic/src/systems/test_utils.rs similarity index 94% rename from rascaline/src/systems/test_utils.rs rename to featomic/src/systems/test_utils.rs index 39888a344..0e439d8ae 100644 --- a/rascaline/src/systems/test_utils.rs +++ b/featomic/src/systems/test_utils.rs @@ -1,9 +1,9 @@ use crate::{System, Vector3D}; use super::{UnitCell, SimpleSystem}; -pub fn test_systems(names: &[&str]) -> Vec> { +pub fn test_systems(names: &[&str]) -> Vec { return names.iter() - .map(|&name| Box::new(test_system(name)) as Box) + .map(|&name| System::new(test_system(name))) .collect(); } diff --git a/rascaline/src/tutorials/mod.rs b/featomic/src/tutorials/mod.rs similarity index 100% rename from rascaline/src/tutorials/mod.rs rename to featomic/src/tutorials/mod.rs diff --git a/rascaline/src/tutorials/moments/mod.rs b/featomic/src/tutorials/moments/mod.rs similarity index 100% rename from rascaline/src/tutorials/moments/mod.rs rename to featomic/src/tutorials/moments/mod.rs diff --git a/rascaline/src/tutorials/moments/moments.rs b/featomic/src/tutorials/moments/moments.rs similarity index 97% rename from rascaline/src/tutorials/moments/moments.rs rename to featomic/src/tutorials/moments/moments.rs index 0a3684446..bdee2f63f 100644 --- a/rascaline/src/tutorials/moments/moments.rs +++ b/featomic/src/tutorials/moments/moments.rs @@ -25,7 +25,7 @@ impl CalculatorBase for GeometricMoments { std::slice::from_ref(&self.cutoff) } - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { let builder = CenterSingleNeighborsTypesKeys { cutoff: self.cutoff, self_pairs: false, @@ -37,7 +37,7 @@ impl CalculatorBase for GeometricMoments { AtomCenteredSamples::sample_names() } - fn samples(&self, keys: &Labels, systems: &mut [Box]) -> Result, Error> { + fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error> { assert_eq!(keys.names(), ["center_type", "neighbor_type"]); let mut samples = Vec::new(); @@ -62,7 +62,7 @@ impl CalculatorBase for GeometricMoments { } } - fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [Box]) -> Result, Error> { + fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [System]) -> Result, Error> { assert_eq!(keys.names(), ["center_type", "neighbor_type"]); debug_assert_eq!(keys.count(), samples.len()); @@ -100,7 +100,7 @@ impl CalculatorBase for GeometricMoments { } // [compute] - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { assert_eq!(descriptor.keys().names(), ["center_type", "neighbor_type"]); assert!(descriptor.keys().count() > 0); diff --git a/rascaline/src/tutorials/moments/s1_scaffold.rs b/featomic/src/tutorials/moments/s1_scaffold.rs similarity index 76% rename from rascaline/src/tutorials/moments/s1_scaffold.rs rename to featomic/src/tutorials/moments/s1_scaffold.rs index 0b12962b8..f60c150c4 100644 --- a/rascaline/src/tutorials/moments/s1_scaffold.rs +++ b/featomic/src/tutorials/moments/s1_scaffold.rs @@ -29,7 +29,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { todo!() } @@ -37,7 +37,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn samples(&self, keys: &Labels, systems: &mut [Box]) -> Result, Error> { + fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error> { todo!() } @@ -45,7 +45,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [Box]) -> Result, Error> { + fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [System]) -> Result, Error> { todo!() } @@ -61,7 +61,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { todo!() } } diff --git a/rascaline/src/tutorials/moments/s2_metadata.rs b/featomic/src/tutorials/moments/s2_metadata.rs similarity index 91% rename from rascaline/src/tutorials/moments/s2_metadata.rs rename to featomic/src/tutorials/moments/s2_metadata.rs index 983f94093..51aaaf2ca 100644 --- a/rascaline/src/tutorials/moments/s2_metadata.rs +++ b/featomic/src/tutorials/moments/s2_metadata.rs @@ -34,7 +34,7 @@ impl CalculatorBase for GeometricMoments { // [CalculatorBase::cutoffs] // [CalculatorBase::keys] - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { let builder = CenterSingleNeighborsTypesKeys { cutoff: self.cutoff, // self pairs would have a distance of 0 and would not contribute @@ -50,7 +50,7 @@ impl CalculatorBase for GeometricMoments { AtomCenteredSamples::sample_names() } - fn samples(&self, keys: &Labels, systems: &mut [Box]) -> Result, Error> { + fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error> { assert_eq!(keys.names(), ["center_type", "neighbor_type"]); let mut samples = Vec::new(); @@ -82,7 +82,7 @@ impl CalculatorBase for GeometricMoments { // [CalculatorBase::supports_gradient] // [CalculatorBase::positions_gradient_samples] - fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [Box]) -> Result, Error> { + fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [System]) -> Result, Error> { assert_eq!(keys.names(), ["center_type", "neighbor_type"]); debug_assert_eq!(keys.count(), samples.len()); @@ -127,7 +127,7 @@ impl CalculatorBase for GeometricMoments { } // [CalculatorBase::properties] - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { todo!() } diff --git a/rascaline/src/tutorials/moments/s3_compute_1.rs b/featomic/src/tutorials/moments/s3_compute_1.rs similarity index 78% rename from rascaline/src/tutorials/moments/s3_compute_1.rs rename to featomic/src/tutorials/moments/s3_compute_1.rs index 434d1f058..18542ea46 100644 --- a/rascaline/src/tutorials/moments/s3_compute_1.rs +++ b/featomic/src/tutorials/moments/s3_compute_1.rs @@ -25,7 +25,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { todo!() } @@ -33,7 +33,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn samples(&self, keys: &Labels, systems: &mut [Box]) -> Result, Error> { + fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error> { todo!() } @@ -41,7 +41,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [Box]) -> Result, Error> { + fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [System]) -> Result, Error> { todo!() } @@ -58,7 +58,7 @@ impl CalculatorBase for GeometricMoments { } // [compute] - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { assert_eq!(descriptor.keys().names(), ["center_type", "neighbor_type"]); // we'll add more code here diff --git a/rascaline/src/tutorials/moments/s3_compute_2.rs b/featomic/src/tutorials/moments/s3_compute_2.rs similarity index 80% rename from rascaline/src/tutorials/moments/s3_compute_2.rs rename to featomic/src/tutorials/moments/s3_compute_2.rs index 0ef29fabd..5099447bc 100644 --- a/rascaline/src/tutorials/moments/s3_compute_2.rs +++ b/featomic/src/tutorials/moments/s3_compute_2.rs @@ -25,7 +25,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { todo!() } @@ -33,7 +33,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn samples(&self, keys: &Labels, systems: &mut [Box]) -> Result, Error> { + fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error> { todo!() } @@ -41,7 +41,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [Box]) -> Result, Error> { + fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [System]) -> Result, Error> { todo!() } @@ -58,7 +58,7 @@ impl CalculatorBase for GeometricMoments { } // [compute] - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { assert_eq!(descriptor.keys().names(), ["center_type", "neighbor_type"]); for (system_i, system) in systems.iter_mut().enumerate() { diff --git a/rascaline/src/tutorials/moments/s3_compute_3.rs b/featomic/src/tutorials/moments/s3_compute_3.rs similarity index 90% rename from rascaline/src/tutorials/moments/s3_compute_3.rs rename to featomic/src/tutorials/moments/s3_compute_3.rs index 13994cfc9..69f917e9e 100644 --- a/rascaline/src/tutorials/moments/s3_compute_3.rs +++ b/featomic/src/tutorials/moments/s3_compute_3.rs @@ -25,7 +25,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { todo!() } @@ -33,7 +33,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn samples(&self, keys: &Labels, systems: &mut [Box]) -> Result, Error> { + fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error> { todo!() } @@ -41,7 +41,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [Box]) -> Result, Error> { + fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [System]) -> Result, Error> { todo!() } @@ -58,7 +58,7 @@ impl CalculatorBase for GeometricMoments { } // [compute] - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { assert_eq!(descriptor.keys().names(), ["center_type", "neighbor_type"]); for (system_i, system) in systems.iter_mut().enumerate() { diff --git a/rascaline/src/tutorials/moments/s3_compute_4.rs b/featomic/src/tutorials/moments/s3_compute_4.rs similarity index 89% rename from rascaline/src/tutorials/moments/s3_compute_4.rs rename to featomic/src/tutorials/moments/s3_compute_4.rs index 3dd337c32..7c478e05d 100644 --- a/rascaline/src/tutorials/moments/s3_compute_4.rs +++ b/featomic/src/tutorials/moments/s3_compute_4.rs @@ -32,7 +32,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { todo!() } @@ -40,7 +40,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn samples(&self, keys: &Labels, systems: &mut [Box]) -> Result, Error> { + fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error> { todo!() } @@ -48,7 +48,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [Box]) -> Result, Error> { + fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [System]) -> Result, Error> { todo!() } @@ -65,7 +65,7 @@ impl CalculatorBase for GeometricMoments { } // [compute] - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { // ... for (system_i, system) in systems.iter_mut().enumerate() { // ... diff --git a/rascaline/src/tutorials/moments/s3_compute_5.rs b/featomic/src/tutorials/moments/s3_compute_5.rs similarity index 94% rename from rascaline/src/tutorials/moments/s3_compute_5.rs rename to featomic/src/tutorials/moments/s3_compute_5.rs index 5bb15fffc..5ac9e27b2 100644 --- a/rascaline/src/tutorials/moments/s3_compute_5.rs +++ b/featomic/src/tutorials/moments/s3_compute_5.rs @@ -33,7 +33,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn keys(&self, systems: &mut [Box]) -> Result { + fn keys(&self, systems: &mut [System]) -> Result { todo!() } @@ -41,7 +41,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn samples(&self, keys: &Labels, systems: &mut [Box]) -> Result, Error> { + fn samples(&self, keys: &Labels, systems: &mut [System]) -> Result, Error> { todo!() } @@ -49,7 +49,7 @@ impl CalculatorBase for GeometricMoments { todo!() } - fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [Box]) -> Result, Error> { + fn positions_gradient_samples(&self, keys: &Labels, samples: &[Labels], systems: &mut [System]) -> Result, Error> { todo!() } @@ -66,7 +66,7 @@ impl CalculatorBase for GeometricMoments { } // [compute] - fn compute(&mut self, systems: &mut [Box], descriptor: &mut TensorMap) -> Result<(), Error> { + fn compute(&mut self, systems: &mut [System], descriptor: &mut TensorMap) -> Result<(), Error> { // ... // add these lines diff --git a/rascaline/src/types/matrix.rs b/featomic/src/types/matrix.rs similarity index 98% rename from rascaline/src/types/matrix.rs rename to featomic/src/types/matrix.rs index 5599b7cb5..8ffc1ae38 100644 --- a/rascaline/src/types/matrix.rs +++ b/featomic/src/types/matrix.rs @@ -13,7 +13,7 @@ use crate::Vector3D; /// `Matrix3` implements all the usual arithmetic operations: /// /// ``` -/// use rascaline::types::{Matrix3, Vector3D}; +/// use featomic::types::{Matrix3, Vector3D}; /// /// let one = Matrix3::one(); /// let a = Matrix3::new([ @@ -74,7 +74,7 @@ impl Matrix3 { /// Create a new `Matrix3` specifying all its components /// # Examples /// ``` - /// # use rascaline::types::Matrix3; + /// # use featomic::types::Matrix3; /// let matrix = Matrix3::new([ /// [0.0, 0.0, 3.0], /// [0.0, 1.0, 5.6], @@ -91,7 +91,7 @@ impl Matrix3 { /// # Examples /// /// ``` - /// # use rascaline::types::Matrix3; + /// # use featomic::types::Matrix3; /// let matrix = Matrix3::zero(); /// /// for i in 0..3 { @@ -110,7 +110,7 @@ impl Matrix3 { /// # Examples /// /// ``` - /// # use rascaline::types::Matrix3; + /// # use featomic::types::Matrix3; /// let matrix = Matrix3::one(); /// /// for i in 0..3 { @@ -132,7 +132,7 @@ impl Matrix3 { /// # Examples /// /// ``` - /// # use rascaline::types::{Vector3D, Matrix3}; + /// # use featomic::types::{Vector3D, Matrix3}; /// let e1 = Vector3D::new(1.0f64, 0.0, 0.0); /// let e3 = Vector3D::new(0.0f64, 0.0, 1.0); /// let angle = 90f64.to_radians(); @@ -168,7 +168,7 @@ impl Matrix3 { /// Compute the trace of the matrix /// # Examples /// ``` - /// # use rascaline::types::Matrix3; + /// # use featomic::types::Matrix3; /// let matrix = Matrix3::new([ /// [0.0, 0.0, 3.0], /// [0.0, 1.0, 5.6], @@ -185,7 +185,7 @@ impl Matrix3 { /// # Examples /// /// ``` - /// # use rascaline::types::Matrix3; + /// # use featomic::types::Matrix3; /// // A diagonal matrix is trivially invertible /// let matrix = Matrix3::new([ /// [4.0, 0.0, 0.0], @@ -231,7 +231,7 @@ impl Matrix3 { /// # Examples /// /// ``` - /// # use rascaline::types::Matrix3; + /// # use featomic::types::Matrix3; /// let matrix = Matrix3::new([ /// [4.0, 0.0, 0.0], /// [0.0, 1.5, 0.0], @@ -251,7 +251,7 @@ impl Matrix3 { /// # Examples /// /// ``` - /// # use rascaline::types::Matrix3; + /// # use featomic::types::Matrix3; /// let matrix = Matrix3::new([ /// [1.0, 2.0, 4.0], /// [0.0, 1.0, 3.0], @@ -279,7 +279,7 @@ impl Matrix3 { /// # Examples /// /// ``` - /// # use rascaline::types::Matrix3; + /// # use featomic::types::Matrix3; /// let matrix = Matrix3::new([ /// [1.0, 2.0, 4.0], /// [0.0, 1.0, 3.0], diff --git a/rascaline/src/types/mod.rs b/featomic/src/types/mod.rs similarity index 100% rename from rascaline/src/types/mod.rs rename to featomic/src/types/mod.rs diff --git a/rascaline/src/types/vectors.rs b/featomic/src/types/vectors.rs similarity index 97% rename from rascaline/src/types/vectors.rs rename to featomic/src/types/vectors.rs index a470d431b..c42b3993b 100644 --- a/rascaline/src/types/vectors.rs +++ b/featomic/src/types/vectors.rs @@ -12,7 +12,7 @@ use crate::Matrix3; /// A `Vector3D` implement all the arithmetic operations: /// /// ``` -/// # use rascaline::types::Vector3D; +/// # use featomic::types::Vector3D; /// let u = Vector3D::new(1.0, 2.0, 3.0); /// let v = Vector3D::new(4.0, -2.0, 1.0); /// @@ -62,7 +62,7 @@ impl Vector3D { /// # Examples /// /// ``` - /// # use rascaline::types::Vector3D; + /// # use featomic::types::Vector3D; /// let vector = Vector3D::new(1.0, 0.0, -42.0); /// /// assert_eq!(vector[0], 1.0); @@ -78,7 +78,7 @@ impl Vector3D { /// # Examples /// /// ``` - /// # use rascaline::types::Vector3D; + /// # use featomic::types::Vector3D; /// let vector = Vector3D::zero(); /// assert_eq!(vector[0], 0.0); /// assert_eq!(vector[1], 0.0); @@ -92,7 +92,7 @@ impl Vector3D { /// /// # Examples /// ``` - /// # use rascaline::types::Vector3D; + /// # use featomic::types::Vector3D; /// let vec = Vector3D::new(1.0, 0.0, -4.0); /// assert_eq!(vec.norm2(), 17.0); /// ``` @@ -104,7 +104,7 @@ impl Vector3D { /// Return the euclidean norm of a `Vector3D` /// # Examples /// ``` - /// # use rascaline::types::Vector3D; + /// # use featomic::types::Vector3D; /// # use std::f64; /// let vec = Vector3D::new(1.0, 0.0, -4.0); /// assert_eq!(vec.norm(), f64::sqrt(17.0)); @@ -117,7 +117,7 @@ impl Vector3D { /// Normalize a `Vector3D`. /// # Examples /// ``` - /// # use rascaline::types::Vector3D; + /// # use featomic::types::Vector3D; /// let vec = Vector3D::new(1.0, 0.0, -4.0); /// let n = vec.normalized(); /// assert_eq!(n.norm(), 1.0); @@ -134,8 +134,8 @@ impl Vector3D { /// # Examples /// /// ``` - /// # use rascaline::types::Vector3D; - /// # use rascaline::types::Matrix3; + /// # use featomic::types::Vector3D; + /// # use featomic::types::Matrix3; /// let a = Vector3D::new(1.0, 0.0, -4.0); /// let b = Vector3D::new(1.0, 2.0, 3.0); /// let matrix = Matrix3::new([ @@ -159,7 +159,7 @@ impl Vector3D { /// # Examples /// /// ``` - /// # use rascaline::types::Vector3D; + /// # use featomic::types::Vector3D; /// let vector = Vector3D::new(1.0, 0.0, -4.0); /// /// assert_eq!(vector.min(), -4.0); @@ -174,7 +174,7 @@ impl Vector3D { /// # Examples /// /// ``` - /// # use rascaline::types::Vector3D; + /// # use featomic::types::Vector3D; /// let vector = Vector3D::new(1.0, 0.0, -4.0); /// /// assert_eq!(vector.max(), 1.0); diff --git a/rascaline-c-api/tests/CMakeLists.txt b/featomic/tests/CMakeLists.txt similarity index 59% rename from rascaline-c-api/tests/CMakeLists.txt rename to featomic/tests/CMakeLists.txt index bbe08bec4..eeea3c98c 100644 --- a/rascaline-c-api/tests/CMakeLists.txt +++ b/featomic/tests/CMakeLists.txt @@ -2,15 +2,19 @@ cmake_minimum_required(VERSION 3.16) message(STATUS "Running CMake version ${CMAKE_VERSION}") -project(rascaline-capi-tests C CXX) +if (POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) # Timestamp for FetchContent +endif() + +project(featomic-capi-tests C CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_NO_SYSTEM_FROM_IMPORTED TRUE) -option(RASCAL_ENABLE_COVERAGE "Collect code coverage for C and C++ API" OFF) +option(FEATOMIC_ENABLE_COVERAGE "Collect code coverage for C and C++ API" OFF) -if(RASCAL_ENABLE_COVERAGE) +if(FEATOMIC_ENABLE_COVERAGE) message(STATUS "Collecting code coverage") if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") @@ -49,81 +53,67 @@ else() string(TOLOWER ${CMAKE_BUILD_TYPE} CMAKE_BUILD_TYPE) endif() -set(RASCALINE_ENABLE_CHEMFILES ON) -add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR}/rascaline) +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR}/featomic) + + +# We use chemfiles in examples to show how to create System +set(BUILD_SHARED_LIBS OFF) +include(FetchContent) +FetchContent_Declare( + chemfiles + URL https://github.com/chemfiles/chemfiles/releases/download/0.10.4/chemfiles-0.10.4.tar.gz + URL_HASH SHA256=b8232ddaae2953538274982838aa6c2df87d300f7e2f80e92c171581e06325ba +) + +message(STATUS "Fetching chemfiles from github") +if (CMAKE_VERSION VERSION_GREATER 3.18) + FetchContent_MakeAvailable(chemfiles) +else() + if (NOT metatensor_POPULATED) + FetchContent_Populate(chemfiles) + endif() + + add_subdirectory(${chemfiles_SOURCE_DIR} ${chemfiles_BINARY_DIR}) +endif() -# Add rascaline and metatensor to the rpath of tests, so we can load the shared +# Add featomic and metatensor to the rpath of tests, so we can load the shared # library directly from `target/{debug,release}/` -get_target_property(RASCALINE_LOCATION rascaline::shared IMPORTED_LOCATION) -get_filename_component(RASCALINE_DIRECTORY "${RASCALINE_LOCATION}" DIRECTORY) +get_target_property(FEATOMIC_LOCATION featomic::shared IMPORTED_LOCATION) +get_filename_component(FEATOMIC_DIRECTORY "${FEATOMIC_LOCATION}" DIRECTORY) get_target_property(METATENSOR_LOCATION metatensor::shared IMPORTED_LOCATION) get_filename_component(METATENSOR_DIRECTORY "${METATENSOR_LOCATION}" DIRECTORY) -set(CMAKE_BUILD_RPATH "${RASCALINE_DIRECTORY};${METATENSOR_DIRECTORY}") +set(CMAKE_BUILD_RPATH "${FEATOMIC_DIRECTORY};${METATENSOR_DIRECTORY}") -add_subdirectory(catch) +add_subdirectory(utils/catch) -add_library(tests_helpers STATIC helpers.cpp) -target_link_libraries(tests_helpers rascaline::shared) +add_library(tests_helpers STATIC utils/helpers.cpp) +target_include_directories(tests_helpers PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/utils/) +target_link_libraries(tests_helpers featomic::shared) find_program(VALGRIND valgrind) if (VALGRIND) - if (NOT "$ENV{RASCALINE_DISABLE_VALGRIND}" EQUAL "1") + if (NOT "$ENV{FEATOMIC_DISABLE_VALGRIND}" EQUAL "1") message(STATUS "Running tests using valgrind") set(TEST_COMMAND "${VALGRIND}" "--tool=memcheck" "--dsymutil=yes" "--error-exitcode=125" "--leak-check=full" "--show-leak-kinds=definite,indirect,possible" "--track-origins=yes" - "--suppressions=${CMAKE_CURRENT_SOURCE_DIR}/valgrind.supp" "--gen-suppressions=all" + "--suppressions=${CMAKE_CURRENT_SOURCE_DIR}/utils/valgrind.supp" "--gen-suppressions=all" ) endif() else() set(TEST_COMMAND "") endif() -file(GLOB ALL_TESTS *.cpp) -list(REMOVE_ITEM ALL_TESTS "${CMAKE_CURRENT_SOURCE_DIR}/helpers.cpp") - # on windows, shared libraries are found in the PATH. This plays a similar role # to `CMAKE_BUILD_RPATH` above string(REPLACE ";" "\\;" PATH_STRING "$ENV{PATH}") -set(WINDOWS_TEST_PATH "${PATH_STRING}\;${METATENSOR_DIRECTORY}\;${RASCALINE_DIRECTORY}") - -enable_testing() -foreach(_file_ ${ALL_TESTS}) - get_filename_component(_name_ ${_file_} NAME_WE) - add_executable(${_name_} ${_file_}) - target_link_libraries(${_name_} rascaline::shared catch tests_helpers) - add_test( - NAME ${_name_} - COMMAND ${TEST_COMMAND} $ - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - ) - - if(WIN32) - set_tests_properties(${_name_} PROPERTIES ENVIRONMENT "PATH=${WINDOWS_TEST_PATH}") - endif() -endforeach() - -# make sure example compile and run -set(XYZ_EXAMPLE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/../../rascaline/examples/data/water.xyz) +set(WINDOWS_TEST_PATH "${PATH_STRING}\;${METATENSOR_DIRECTORY}\;${FEATOMIC_DIRECTORY}") -file(GLOB EXAMPLES ../examples/*.c) -foreach(_file_ ${EXAMPLES}) - get_filename_component(_name_ ${_file_} NAME_WE) - set(_name_ example-c-${_name_}) - add_executable(${_name_} ${_file_}) - target_link_libraries(${_name_} rascaline::shared) - - add_test( - NAME ${_name_} - COMMAND ${TEST_COMMAND} $ ${XYZ_EXAMPLE_FILE} - ) - - if(WIN32) - set_tests_properties(${_name_} PROPERTIES ENVIRONMENT "PATH=${WINDOWS_TEST_PATH}") - endif() -endforeach() +set(XYZ_EXAMPLE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/../../featomic/examples/data/water.xyz) +enable_testing() +add_subdirectory(c) add_subdirectory(cxx) diff --git a/featomic/tests/c/CMakeLists.txt b/featomic/tests/c/CMakeLists.txt new file mode 100644 index 000000000..45f62be91 --- /dev/null +++ b/featomic/tests/c/CMakeLists.txt @@ -0,0 +1,34 @@ +file(GLOB ALL_TESTS *.cpp) + +foreach(_file_ ${ALL_TESTS}) + get_filename_component(_name_ ${_file_} NAME_WE) + set(_name_ c-${_name_}) + add_executable(${_name_} ${_file_}) + target_link_libraries(${_name_} featomic::shared tests_helpers catch) + add_test( + NAME ${_name_} + COMMAND ${TEST_COMMAND} $ + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + + if(WIN32) + set_tests_properties(${_name_} PROPERTIES ENVIRONMENT "PATH=${WINDOWS_TEST_PATH}") + endif() +endforeach() + +file(GLOB EXAMPLES ../../examples/*.c) +foreach(_file_ ${EXAMPLES}) + get_filename_component(_name_ ${_file_} NAME_WE) + set(_name_ example-c-${_name_}) + add_executable(${_name_} ${_file_}) + target_link_libraries(${_name_} featomic::shared chemfiles) + + add_test( + NAME ${_name_} + COMMAND ${TEST_COMMAND} $ ${XYZ_EXAMPLE_FILE} + ) + + if(WIN32) + set_tests_properties(${_name_} PROPERTIES ENVIRONMENT "PATH=${WINDOWS_TEST_PATH}") + endif() +endforeach() diff --git a/rascaline-c-api/tests/calculator.cpp b/featomic/tests/c/calculator.cpp similarity index 87% rename from rascaline-c-api/tests/calculator.cpp rename to featomic/tests/c/calculator.cpp index b87d80389..cb7e2f6f2 100644 --- a/rascaline-c-api/tests/calculator.cpp +++ b/featomic/tests/c/calculator.cpp @@ -2,7 +2,7 @@ #include #include -#include "rascaline.h" +#include "featomic.h" #include "catch.hpp" #include "helpers.hpp" @@ -23,14 +23,14 @@ TEST_CASE("calculator name") { "delta": 25, "name": "bar" })"; - auto* calculator = rascal_calculator("dummy_calculator", HYPERS_JSON); + auto* calculator = featomic_calculator("dummy_calculator", HYPERS_JSON); REQUIRE(calculator != nullptr); char buffer[256] = {}; - CHECK_SUCCESS(rascal_calculator_name(calculator, buffer, sizeof(buffer))); + CHECK_SUCCESS(featomic_calculator_name(calculator, buffer, sizeof(buffer))); CHECK(buffer == std::string("dummy test calculator with cutoff: 3.5 - delta: 25 - name: bar")); - rascal_calculator_free(calculator); + featomic_calculator_free(calculator); } SECTION("long strings") { @@ -40,21 +40,21 @@ TEST_CASE("calculator name") { "delta": 25, "name": ")" + name + "\"}"; - auto* calculator = rascal_calculator("dummy_calculator", HYPERS_JSON.c_str()); + auto* calculator = featomic_calculator("dummy_calculator", HYPERS_JSON.c_str()); REQUIRE(calculator != nullptr); char* buffer = new char[4096]; - auto status = rascal_calculator_name(calculator, buffer, 256); - CHECK(status == RASCAL_BUFFER_SIZE_ERROR); + auto status = featomic_calculator_name(calculator, buffer, 256); + CHECK(status == FEATOMIC_BUFFER_SIZE_ERROR); - CHECK_SUCCESS(rascal_calculator_name(calculator, buffer, 4096)); + CHECK_SUCCESS(featomic_calculator_name(calculator, buffer, 4096)); std::string expected = "dummy test calculator with cutoff: 3.5 - delta: 25 - "; expected += "name: " + name; CHECK(buffer == expected); delete[] buffer; - rascal_calculator_free(calculator); + featomic_calculator_free(calculator); } } @@ -65,14 +65,14 @@ TEST_CASE("calculator parameters") { "delta": 25, "name": "bar" })"; - auto* calculator = rascal_calculator("dummy_calculator", HYPERS_JSON.c_str()); + auto* calculator = featomic_calculator("dummy_calculator", HYPERS_JSON.c_str()); REQUIRE(calculator != nullptr); char buffer[256] = {}; - CHECK_SUCCESS(rascal_calculator_parameters(calculator, buffer, sizeof(buffer))); + CHECK_SUCCESS(featomic_calculator_parameters(calculator, buffer, sizeof(buffer))); CHECK(buffer == HYPERS_JSON); - rascal_calculator_free(calculator); + featomic_calculator_free(calculator); } SECTION("long strings") { @@ -82,19 +82,19 @@ TEST_CASE("calculator parameters") { "delta": 25, "name": ")" + name + "\"}"; - auto* calculator = rascal_calculator("dummy_calculator", HYPERS_JSON.c_str()); + auto* calculator = featomic_calculator("dummy_calculator", HYPERS_JSON.c_str()); REQUIRE(calculator != nullptr); char* buffer = new char[4096]; - auto status = rascal_calculator_parameters(calculator, buffer, 256); - CHECK(status == RASCAL_BUFFER_SIZE_ERROR); + auto status = featomic_calculator_parameters(calculator, buffer, 256); + CHECK(status == FEATOMIC_BUFFER_SIZE_ERROR); - CHECK_SUCCESS(rascal_calculator_parameters(calculator, buffer, 4096)); + CHECK_SUCCESS(featomic_calculator_parameters(calculator, buffer, 4096)); CHECK(buffer == HYPERS_JSON); delete[] buffer; - rascal_calculator_free(calculator); + featomic_calculator_free(calculator); } SECTION("cutoffs") { @@ -103,16 +103,16 @@ TEST_CASE("calculator parameters") { "delta": 25, "name": "bar" })"; - auto* calculator = rascal_calculator("dummy_calculator", HYPERS_JSON.c_str()); + auto* calculator = featomic_calculator("dummy_calculator", HYPERS_JSON.c_str()); REQUIRE(calculator != nullptr); const double* cutoffs = nullptr; uintptr_t cutoffs_count = 0; - CHECK_SUCCESS(rascal_calculator_cutoffs(calculator, &cutoffs, &cutoffs_count)); + CHECK_SUCCESS(featomic_calculator_cutoffs(calculator, &cutoffs, &cutoffs_count)); CHECK(cutoffs_count == 1); CHECK(cutoffs[0] == 3.5); - rascal_calculator_free(calculator); + featomic_calculator_free(calculator); } } @@ -122,10 +122,10 @@ TEST_CASE("calculator creation errors") { "delta": 25, "name": "bar" })"; - auto *calculator = rascal_calculator("dummy_calculator", HYPERS_JSON); + auto *calculator = featomic_calculator("dummy_calculator", HYPERS_JSON); CHECK(calculator == nullptr); - CHECK(std::string(rascal_last_error()) == "json error: invalid type: string \"532\", expected f64 at line 2 column 23"); + CHECK(std::string(featomic_last_error()) == "json error: invalid type: string \"532\", expected f64 at line 2 column 23"); } TEST_CASE("Compute descriptor") { @@ -138,17 +138,17 @@ TEST_CASE("Compute descriptor") { SECTION("Full compute") { auto system = simple_system(); - rascal_calculation_options_t options; - std::memset(&options, 0, sizeof(rascal_calculation_options_t)); + featomic_calculation_options_t options; + std::memset(&options, 0, sizeof(featomic_calculation_options_t)); const char* gradients_list[] = {"positions"}; options.gradients = gradients_list; options.gradients_count = 1; - auto* calculator = rascal_calculator("dummy_calculator", HYPERS_JSON); + auto* calculator = featomic_calculator("dummy_calculator", HYPERS_JSON); REQUIRE(calculator != nullptr); mts_tensormap_t* descriptor = nullptr; - auto status = rascal_calculator_compute( + auto status = featomic_calculator_compute( calculator, &descriptor, &system, 1, options ); CHECK_SUCCESS(status); @@ -212,7 +212,7 @@ TEST_CASE("Compute descriptor") { check_block(descriptor, 1, samples, properties, values, gradient_samples, gradients); mts_tensormap_free(descriptor); - rascal_calculator_free(calculator); + featomic_calculator_free(calculator); } SECTION("Partial compute -- samples") { @@ -233,18 +233,18 @@ TEST_CASE("Compute descriptor") { auto system = simple_system(); - rascal_calculation_options_t options; - std::memset(&options, 0, sizeof(rascal_calculation_options_t)); + featomic_calculation_options_t options; + std::memset(&options, 0, sizeof(featomic_calculation_options_t)); const char* gradients_list[] = {"positions"}; options.gradients = gradients_list; options.gradients_count = 1; options.selected_samples.subset = &selected_samples; - auto* calculator = rascal_calculator("dummy_calculator", HYPERS_JSON); + auto* calculator = featomic_calculator("dummy_calculator", HYPERS_JSON); REQUIRE(calculator != nullptr); mts_tensormap_t* descriptor = nullptr; - auto status = rascal_calculator_compute( + auto status = featomic_calculator_compute( calculator, &descriptor, &system, 1, options ); @@ -296,7 +296,7 @@ TEST_CASE("Compute descriptor") { check_block(descriptor, 1, samples, properties, values, gradient_samples, gradients); mts_tensormap_free(descriptor); - rascal_calculator_free(calculator); + featomic_calculator_free(calculator); } SECTION("Partial compute -- features") { @@ -317,18 +317,18 @@ TEST_CASE("Compute descriptor") { auto system = simple_system(); - rascal_calculation_options_t options; - std::memset(&options, 0, sizeof(rascal_calculation_options_t)); + featomic_calculation_options_t options; + std::memset(&options, 0, sizeof(featomic_calculation_options_t)); const char* gradients_list[] = {"positions"}; options.gradients = gradients_list; options.gradients_count = 1; options.selected_properties.subset = &selected_properties; - auto* calculator = rascal_calculator("dummy_calculator", HYPERS_JSON); + auto* calculator = featomic_calculator("dummy_calculator", HYPERS_JSON); REQUIRE(calculator != nullptr); mts_tensormap_t* descriptor = nullptr; - auto status = rascal_calculator_compute( + auto status = featomic_calculator_compute( calculator, &descriptor, &system, 1, options ); CHECK_SUCCESS(status); @@ -390,7 +390,7 @@ TEST_CASE("Compute descriptor") { check_block(descriptor, 1, samples, properties, values, gradient_samples, gradients); mts_tensormap_free(descriptor); - rascal_calculator_free(calculator); + featomic_calculator_free(calculator); } SECTION("Partial compute -- preselected") { @@ -459,17 +459,17 @@ TEST_CASE("Compute descriptor") { REQUIRE(predefined != nullptr); auto system = simple_system(); - rascal_calculation_options_t options = {}; + featomic_calculation_options_t options = {}; const char* gradients_list[] = {"positions"}; options.gradients = gradients_list; options.gradients_count = 1; options.selected_samples.predefined = predefined; options.selected_properties.predefined = predefined; - auto* calculator = rascal_calculator("dummy_calculator", HYPERS_JSON); + auto* calculator = featomic_calculator("dummy_calculator", HYPERS_JSON); REQUIRE(calculator != nullptr); mts_tensormap_t* descriptor = nullptr; - auto status = rascal_calculator_compute( + auto status = featomic_calculator_compute( calculator, &descriptor, &system, 1, options ); CHECK_SUCCESS(status); @@ -526,7 +526,7 @@ TEST_CASE("Compute descriptor") { mts_tensormap_free(predefined); mts_tensormap_free(descriptor); - rascal_calculator_free(calculator); + featomic_calculator_free(calculator); } SECTION("Partial compute -- key selection") { @@ -545,17 +545,17 @@ TEST_CASE("Compute descriptor") { auto system = simple_system(); - rascal_calculation_options_t options = {}; + featomic_calculation_options_t options = {}; const char* gradients_list[] = {"positions"}; options.gradients = gradients_list; options.gradients_count = 1; options.selected_keys = &selected_keys; - auto* calculator = rascal_calculator("dummy_calculator", HYPERS_JSON); + auto* calculator = featomic_calculator("dummy_calculator", HYPERS_JSON); REQUIRE(calculator != nullptr); mts_tensormap_t* descriptor = nullptr; - auto status = rascal_calculator_compute( + auto status = featomic_calculator_compute( calculator, &descriptor, &system, 1, options ); CHECK_SUCCESS(status); @@ -603,7 +603,7 @@ TEST_CASE("Compute descriptor") { check_block(descriptor, 1, samples, properties, values, gradient_samples, gradients); mts_tensormap_free(descriptor); - rascal_calculator_free(calculator); + featomic_calculator_free(calculator); } } diff --git a/rascaline-c-api/tests/logging.cpp b/featomic/tests/c/logging.cpp similarity index 67% rename from rascaline-c-api/tests/logging.cpp rename to featomic/tests/c/logging.cpp index 61e00860f..4a8df841c 100644 --- a/rascaline-c-api/tests/logging.cpp +++ b/featomic/tests/c/logging.cpp @@ -5,22 +5,22 @@ #include "helpers.hpp" #include "metatensor.h" -#include "rascaline.h" +#include "featomic.h" static std::vector> RECORDED_LOG_EVENTS; static void run_calculation(const char* hypers) { - auto* calculator = rascal_calculator("dummy_calculator", hypers); + auto* calculator = featomic_calculator("dummy_calculator", hypers); REQUIRE(calculator != nullptr); auto system = simple_system(); - rascal_calculation_options_t options = {}; + featomic_calculation_options_t options = {}; mts_tensormap_t* descriptor = nullptr; - CHECK_SUCCESS(rascal_calculator_compute( + CHECK_SUCCESS(featomic_calculator_compute( calculator, &descriptor, &system, 1, options )); - rascal_calculator_free(calculator); + featomic_calculator_free(calculator); mts_tensormap_free(descriptor); } @@ -28,7 +28,7 @@ TEST_CASE("Logging") { auto record_log_events = [](int level, const char* message) { RECORDED_LOG_EVENTS.emplace_back(level, std::string(message)); }; - CHECK_SUCCESS(rascal_set_logging_callback(record_log_events)); + CHECK_SUCCESS(featomic_set_logging_callback(record_log_events)); const char* hypers_log_info = R"({ "cutoff": 3.0, @@ -42,8 +42,8 @@ TEST_CASE("Logging") { bool event_found = false; for (const auto& event: RECORDED_LOG_EVENTS) { - if (std::get<1>(event) == "rascaline::calculators::dummy_calculator -- log-test-info: test info message") { - CHECK(std::get<0>(event) == RASCAL_LOG_LEVEL_INFO); + if (std::get<1>(event) == "featomic::calculators::dummy_calculator -- log-test-info: test info message") { + CHECK(std::get<0>(event) == FEATOMIC_LOG_LEVEL_INFO); event_found = true; } } @@ -62,8 +62,8 @@ TEST_CASE("Logging") { event_found = false; for (const auto& event: RECORDED_LOG_EVENTS) { - if (std::get<1>(event) == "rascaline::calculators::dummy_calculator -- log-test-warn: test warning message") { - CHECK(std::get<0>(event) == RASCAL_LOG_LEVEL_WARN); + if (std::get<1>(event) == "featomic::calculators::dummy_calculator -- log-test-warn: test warning message") { + CHECK(std::get<0>(event) == FEATOMIC_LOG_LEVEL_WARN); event_found = true; } } diff --git a/featomic/tests/c/systems.cpp b/featomic/tests/c/systems.cpp new file mode 100644 index 000000000..dfa943321 --- /dev/null +++ b/featomic/tests/c/systems.cpp @@ -0,0 +1,42 @@ +#include "featomic.h" +#include "catch.hpp" + + +TEST_CASE("systems errors") { + const char* HYPERS_JSON = R"({ + "cutoff": 3.0, + "delta": 4, + "name": "" + })"; + + auto* calculator = featomic_calculator("dummy_calculator", HYPERS_JSON); + REQUIRE(calculator != nullptr); + + featomic_system_t system = {}; + featomic_calculation_options_t options = {}; + + mts_tensormap_t* descriptor = nullptr; + auto status = featomic_calculator_compute( + calculator, &descriptor, &system, 1, options + ); + CHECK(descriptor == nullptr); + CHECK(status == FEATOMIC_SYSTEM_ERROR); + + std::string expected = "error from external code (status 128): featomic_system_t.types function is NULL"; + CHECK(featomic_last_error() == expected); + + system.types = [](const void* _, const int32_t** types) { + return -5242832; + }; + + status = featomic_calculator_compute( + calculator, &descriptor, &system, 1, options + ); + CHECK(descriptor == nullptr); + CHECK(status == -5242832); + expected = "error from external code (status -5242832): call to featomic_system_t.types failed"; + CHECK(featomic_last_error() == expected); + + featomic_calculator_free(calculator); + mts_tensormap_free(descriptor); +} diff --git a/rascaline-c-api/tests/check-cxx-install.rs b/featomic/tests/check-cxx-install.rs similarity index 92% rename from rascaline-c-api/tests/check-cxx-install.rs rename to featomic/tests/check-cxx-install.rs index 15c8d12a1..4436441e0 100644 --- a/rascaline-c-api/tests/check-cxx-install.rs +++ b/featomic/tests/check-cxx-install.rs @@ -21,13 +21,13 @@ fn check_cxx_install() { let cargo_manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); - // build and install rascaline with cmake + // build and install featomic with cmake let mut cmake_config = utils::cmake_config(&cargo_manifest_dir, &build_dir); let mut install_dir = build_dir.clone(); install_dir.push("usr"); cmake_config.arg(format!("-DCMAKE_INSTALL_PREFIX={}", install_dir.display())); - cmake_config.arg("-DRASCALINE_FETCH_METATENSOR=ON"); + cmake_config.arg("-DFEATOMIC_FETCH_METATENSOR=ON"); let status = cmake_config.status().expect("cmake configuration failed"); assert!(status.success()); @@ -39,7 +39,7 @@ fn check_cxx_install() { let status = cmake_build.status().expect("cmake build failed"); assert!(status.success()); - // try to use the installed rascaline from cmake + // try to use the installed featomic from cmake let mut build_dir = PathBuf::from(CARGO_TARGET_TMPDIR); build_dir.push("cxx-sample-project"); if build_dir.exists() { diff --git a/featomic/tests/cmake-project/CMakeLists.txt b/featomic/tests/cmake-project/CMakeLists.txt new file mode 100644 index 000000000..67d237ba0 --- /dev/null +++ b/featomic/tests/cmake-project/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.16) + +project(featomic-test-cmake-project C CXX) + +# We need to update the REQUIRED_FEATOMIC_VERSION in the same way we update the +# featomic version for dev builds +include(../../cmake/dev-versions.cmake) +set(REQUIRED_FEATOMIC_VERSION "0.6.0") +create_development_version("${REQUIRED_FEATOMIC_VERSION}" FEATOMIC_FULL_VERSION "featomic-v") +string(REGEX REPLACE "([0-9]*)\\.([0-9]*).*" "\\1.\\2" REQUIRED_FEATOMIC_VERSION ${FEATOMIC_FULL_VERSION}) +find_package(featomic ${REQUIRED_FEATOMIC_VERSION} REQUIRED) + +add_executable(c-main src/main.c) +target_link_libraries(c-main featomic) + +add_executable(cxx-main src/main.cpp) +target_link_libraries(cxx-main featomic) diff --git a/featomic/tests/cmake-project/README.md b/featomic/tests/cmake-project/README.md new file mode 100644 index 000000000..e65dae36b --- /dev/null +++ b/featomic/tests/cmake-project/README.md @@ -0,0 +1,3 @@ +# Sample CMake project using featomic + +This is a basic cmake project linking to featomic from C and C++ code. diff --git a/rascaline-c-api/tests/cmake-project/src/main.c b/featomic/tests/cmake-project/src/main.c similarity index 56% rename from rascaline-c-api/tests/cmake-project/src/main.c rename to featomic/tests/cmake-project/src/main.c index 84c6093b5..fb804ab4c 100644 --- a/rascaline-c-api/tests/cmake-project/src/main.c +++ b/featomic/tests/cmake-project/src/main.c @@ -1,19 +1,19 @@ #include -#include +#include int main(void) { - rascal_calculator_t* calculator = rascal_calculator( + featomic_calculator_t* calculator = featomic_calculator( "dummy_calculator", "{\"cutoff\": 3.4, \"delta\": -3, \"name\": \"testing\", \"gradients\": true}" ); if (calculator == NULL) { - printf("error: %s\n", rascal_last_error()); + printf("error: %s\n", featomic_last_error()); return 1; } - rascal_calculator_free(calculator); + featomic_calculator_free(calculator); return 0; } diff --git a/rascaline-c-api/tests/cmake-project/src/main.cpp b/featomic/tests/cmake-project/src/main.cpp similarity index 61% rename from rascaline-c-api/tests/cmake-project/src/main.cpp rename to featomic/tests/cmake-project/src/main.cpp index 986fcb69f..f77995582 100644 --- a/rascaline-c-api/tests/cmake-project/src/main.cpp +++ b/featomic/tests/cmake-project/src/main.cpp @@ -1,14 +1,14 @@ -#include +#include int main() { try { - auto calculator = rascaline::Calculator( + auto calculator = featomic::Calculator( "dummy_calculator", R"({"cutoff": 3.4, "delta": -3, "name": "testing", "gradients": true})" ); return 0; - } catch (const rascaline::RascalineError& e) { + } catch (const featomic::FeatomicError& e) { return 1; } } diff --git a/rascaline-c-api/tests/cxx/CMakeLists.txt b/featomic/tests/cxx/CMakeLists.txt similarity index 88% rename from rascaline-c-api/tests/cxx/CMakeLists.txt rename to featomic/tests/cxx/CMakeLists.txt index 84ee2b52f..eb914c856 100644 --- a/rascaline-c-api/tests/cxx/CMakeLists.txt +++ b/featomic/tests/cxx/CMakeLists.txt @@ -4,7 +4,7 @@ foreach(_file_ ${ALL_TESTS}) get_filename_component(_name_ ${_file_} NAME_WE) set(_name_ cxx-${_name_}) add_executable(${_name_} ${_file_}) - target_link_libraries(${_name_} rascaline::shared catch) + target_link_libraries(${_name_} featomic::shared catch) add_test( NAME ${_name_} COMMAND ${TEST_COMMAND} $ @@ -21,7 +21,7 @@ foreach(_file_ ${EXAMPLES}) get_filename_component(_name_ ${_file_} NAME_WE) set(_name_ example-cxx-${_name_}) add_executable(${_name_} ${_file_}) - target_link_libraries(${_name_} rascaline::shared) + target_link_libraries(${_name_} featomic::shared chemfiles) add_test( NAME ${_name_} diff --git a/rascaline-c-api/tests/cxx/calculator.cpp b/featomic/tests/cxx/calculator.cpp similarity index 92% rename from rascaline-c-api/tests/cxx/calculator.cpp rename to featomic/tests/cxx/calculator.cpp index f1e0e9999..6dbdfb7fb 100644 --- a/rascaline-c-api/tests/cxx/calculator.cpp +++ b/featomic/tests/cxx/calculator.cpp @@ -1,7 +1,7 @@ #include #include -#include "rascaline.hpp" +#include "featomic.hpp" #include "catch.hpp" #include "test_system.hpp" @@ -13,7 +13,7 @@ TEST_CASE("Calculator name") { "delta": 25, "name": "bar" })"; - auto calculator = rascaline::Calculator("dummy_calculator", HYPERS_JSON); + auto calculator = featomic::Calculator("dummy_calculator", HYPERS_JSON); CHECK(calculator.name() == "dummy test calculator with cutoff: 3.5 - delta: 25 - name: bar"); } @@ -25,7 +25,7 @@ TEST_CASE("Calculator name") { "delta": 25, "name": ")" + name + "\"}"; - auto calculator = rascaline::Calculator("dummy_calculator", HYPERS_JSON); + auto calculator = featomic::Calculator("dummy_calculator", HYPERS_JSON); std::string expected = "dummy test calculator with cutoff: 3.5 - delta: 25 - "; expected += "name: " + name; @@ -41,7 +41,7 @@ TEST_CASE("Calculator parameters") { "name": "bar", "gradients": false })"; - auto calculator = rascaline::Calculator("dummy_calculator", HYPERS_JSON); + auto calculator = featomic::Calculator("dummy_calculator", HYPERS_JSON); CHECK(calculator.parameters() == HYPERS_JSON); } @@ -53,7 +53,7 @@ TEST_CASE("Calculator parameters") { "gradients": false, "name": ")" + name + "\"}"; - auto calculator = rascaline::Calculator("dummy_calculator", HYPERS_JSON); + auto calculator = featomic::Calculator("dummy_calculator", HYPERS_JSON); CHECK(calculator.parameters() == HYPERS_JSON); } @@ -64,7 +64,7 @@ TEST_CASE("Calculator parameters") { "name": "bar", "gradients": false })"; - auto calculator = rascaline::Calculator("dummy_calculator", HYPERS_JSON); + auto calculator = featomic::Calculator("dummy_calculator", HYPERS_JSON); CHECK(calculator.cutoffs() == std::vector{3.5}); } } @@ -78,7 +78,7 @@ TEST_CASE("calculator creation errors") { })"; CHECK_THROWS_WITH( - rascaline::Calculator("dummy_calculator", HYPERS_JSON), + featomic::Calculator("dummy_calculator", HYPERS_JSON), "json error: invalid type: string \"532\", expected f64 at line 2 column 23" ); } @@ -89,10 +89,10 @@ TEST_CASE("Compute descriptor") { })"; auto systems = std::vector{TestSystem()}; - auto calculator = rascaline::Calculator("dummy_calculator", HYPERS_JSON); + auto calculator = featomic::Calculator("dummy_calculator", HYPERS_JSON); SECTION("Full compute") { - auto options = rascaline::CalculationOptions(); + auto options = featomic::CalculationOptions(); options.gradients.push_back("positions"); auto descriptor = calculator.compute(systems, options); @@ -169,9 +169,9 @@ TEST_CASE("Compute descriptor") { } SECTION("Partial compute -- samples") { - auto options = rascaline::CalculationOptions(); + auto options = featomic::CalculationOptions(); options.gradients.push_back("positions"); - options.selected_samples = rascaline::LabelsSelection::subset( + options.selected_samples = featomic::LabelsSelection::subset( metatensor::Labels({"system", "atom"}, {{0, 1}, {0, 3}}) ); auto descriptor = calculator.compute(systems, options); @@ -242,9 +242,9 @@ TEST_CASE("Compute descriptor") { } SECTION("Partial compute -- features") { - auto options = rascaline::CalculationOptions(); + auto options = featomic::CalculationOptions(); options.gradients.push_back("positions"); - options.selected_properties = rascaline::LabelsSelection::subset( + options.selected_properties = featomic::LabelsSelection::subset( metatensor::Labels({"index_delta", "x_y_z"}, {{0, 1}}) ); auto descriptor = calculator.compute(systems, options); @@ -322,7 +322,7 @@ TEST_CASE("Compute descriptor") { } SECTION("Partial compute -- preselected") { - auto options = rascaline::CalculationOptions(); + auto options = featomic::CalculationOptions(); options.gradients.push_back("positions"); auto blocks = std::vector(); @@ -348,8 +348,8 @@ TEST_CASE("Compute descriptor") { metatensor::Labels({"center_type"}, {{1}, {6}}), std::move(blocks) ); - options.selected_samples = rascaline::LabelsSelection::predefined(predefined); - options.selected_properties = rascaline::LabelsSelection::predefined(predefined); + options.selected_samples = featomic::LabelsSelection::predefined(predefined); + options.selected_properties = featomic::LabelsSelection::predefined(predefined); auto descriptor = calculator.compute(systems, options); @@ -422,7 +422,7 @@ TEST_CASE("Compute descriptor") { // existing one (1) from the default set of keys. We also put the keys // in a different order than what would be the default (6, 12). - auto options = rascaline::CalculationOptions(); + auto options = featomic::CalculationOptions(); options.selected_keys = metatensor::Labels( {"center_type"}, {{12}, {6}} diff --git a/featomic/tests/cxx/systems.cpp b/featomic/tests/cxx/systems.cpp new file mode 100644 index 000000000..4e482518b --- /dev/null +++ b/featomic/tests/cxx/systems.cpp @@ -0,0 +1,48 @@ +#include "featomic.hpp" +#include "catch.hpp" + + +class BadSystem: public featomic::System { +public: + uintptr_t size() const override { + throw std::runtime_error("unimplemented function 'size'"); + } + + const int32_t* types() const override { + throw std::runtime_error("unimplemented function 'types'"); + } + + const double* positions() const override { + throw std::runtime_error("unimplemented function 'positions'"); + } + + CellMatrix cell() const override { + throw std::runtime_error("unimplemented function 'cell'"); + } + + void compute_neighbors(double cutoff) override { + throw std::runtime_error("unimplemented function 'compute_neighbors'"); + } + + const std::vector& pairs() const override { + throw std::runtime_error("unimplemented function 'pairs'"); + } + + const std::vector& pairs_containing(uintptr_t atom) const override { + throw std::runtime_error("unimplemented function 'pairs_containing'"); + } +}; + +TEST_CASE("systems errors") { + const char* HYPERS_JSON = R"({ + "cutoff": 3.0, + "delta": 4, + "name": "", + "gradients": true + })"; + + auto system = BadSystem(); + auto calculator = featomic::Calculator("dummy_calculator", HYPERS_JSON); + + CHECK_THROWS_WITH(calculator.compute(system), "unimplemented function 'types'"); +} diff --git a/rascaline-c-api/tests/cxx/test_system.hpp b/featomic/tests/cxx/test_system.hpp similarity index 75% rename from rascaline-c-api/tests/cxx/test_system.hpp rename to featomic/tests/cxx/test_system.hpp index c97c1a2f5..63ee24f02 100644 --- a/rascaline-c-api/tests/cxx/test_system.hpp +++ b/featomic/tests/cxx/test_system.hpp @@ -1,11 +1,11 @@ -#ifndef RASCAL_CXX_TEST_SYSTEMS_H -#define RASCAL_CXX_TEST_SYSTEMS_H +#ifndef FEATOMIC_CXX_TEST_SYSTEMS_H +#define FEATOMIC_CXX_TEST_SYSTEMS_H -#include "rascaline.hpp" +#include "featomic.hpp" #define SQRT_3 1.73205080756887729352 -class TestSystem: public rascaline::System { +class TestSystem: public featomic::System { uintptr_t size() const override { return 4; } @@ -38,8 +38,8 @@ class TestSystem: public rascaline::System { assert(cutoff > SQRT_3 && cutoff < 3.46410161513775458704); } - const std::vector& pairs() const override { - static std::vector PAIRS = { + const std::vector& pairs() const override { + static std::vector PAIRS = { {0, 1, SQRT_3, {1.0, 1.0, 1.0}, {1, 1, 1}}, {1, 2, SQRT_3, {1.0, 1.0, 1.0}, {0, 0, 0}}, {2, 3, SQRT_3, {1.0, 1.0, 1.0}, {0, 0, 0}}, @@ -47,22 +47,22 @@ class TestSystem: public rascaline::System { return PAIRS; } - const std::vector& pairs_containing(uintptr_t atom) const override { - static std::vector PAIRS_0 = { + const std::vector& pairs_containing(uintptr_t atom) const override { + static std::vector PAIRS_0 = { {0, 1, SQRT_3, {1.0, 1.0, 1.0}, {1, 1, 1}}, }; - static std::vector PAIRS_1 = { + static std::vector PAIRS_1 = { {0, 1, SQRT_3, {1.0, 1.0, 1.0}, {1, 1, 1}}, {1, 2, SQRT_3, {1.0, 1.0, 1.0}, {0, 0, 0}}, }; - static std::vector PAIRS_2 = { + static std::vector PAIRS_2 = { {1, 2, SQRT_3, {1.0, 1.0, 1.0}, {0, 0, 0}}, {2, 3, SQRT_3, {1.0, 1.0, 1.0}, {0, 0, 0}}, }; - static std::vector PAIRS_3 = { + static std::vector PAIRS_3 = { {2, 3, SQRT_3, {1.0, 1.0, 1.0}, {0, 0, 0}}, }; diff --git a/rascaline/tests/data/.gitignore b/featomic/tests/data/.gitignore similarity index 100% rename from rascaline/tests/data/.gitignore rename to featomic/tests/data/.gitignore diff --git a/rascaline/tests/data/README.md b/featomic/tests/data/README.md similarity index 81% rename from rascaline/tests/data/README.md rename to featomic/tests/data/README.md index 4d9656179..e3ecd9b40 100644 --- a/rascaline/tests/data/README.md +++ b/featomic/tests/data/README.md @@ -1,6 +1,6 @@ -# Data for regression-testing rascaline +# Data for regression-testing featomic -This directory contains data used in rascaline regression tests, as well as the +This directory contains data used in featomic regression tests, as well as the scripts used to generate this data. Each test consist of two files: one JSON file containing all input data @@ -12,7 +12,7 @@ version of the python binding when generating new data or re-creating existing data. Something like this should work: ```bash -cd rascaline/tests/data/ +cd featomic/tests/data/ python3 -m venv .regtests-pyvenv source .regtests-pyvenv/bin/activate pip install --upgrade pip diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-1/gradients-input.json b/featomic/tests/data/generated/lode-spherical-expansion/exponent-1/gradients-input.json similarity index 78% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-1/gradients-input.json rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-1/gradients-input.json index 5dd35f0ad..ef302ba07 100644 --- a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-1/gradients-input.json +++ b/featomic/tests/data/generated/lode-spherical-expansion/exponent-1/gradients-input.json @@ -1,15 +1,19 @@ { "hyperparameters": { - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "cutoff": 2.5, - "max_angular": 3, - "max_radial": 3, - "potential_exponent": 1, - "radial_basis": { - "Gto": { - "splined_radial_integral": false - } + "basis": { + "max_angular": 3, + "radial": { + "max_radial": 2, + "radius": 2.5, + "type": "Gto" + }, + "spline_accuracy": null, + "type": "TensorProduct" + }, + "density": { + "exponent": 1, + "type": "SmearedPowerLaw", + "smearing": 0.3 } }, "systems": [ diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-1/positions-gradient.npy.gz b/featomic/tests/data/generated/lode-spherical-expansion/exponent-1/positions-gradient.npy.gz similarity index 100% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-1/positions-gradient.npy.gz rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-1/positions-gradient.npy.gz diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-3/values-input.json b/featomic/tests/data/generated/lode-spherical-expansion/exponent-1/values-input.json similarity index 78% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-3/values-input.json rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-1/values-input.json index 3db3fad82..62a5e69b7 100644 --- a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-3/values-input.json +++ b/featomic/tests/data/generated/lode-spherical-expansion/exponent-1/values-input.json @@ -1,15 +1,19 @@ { "hyperparameters": { - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "cutoff": 2.5, - "max_angular": 4, - "max_radial": 4, - "potential_exponent": 3, - "radial_basis": { - "Gto": { - "splined_radial_integral": false - } + "basis": { + "max_angular": 4, + "radial": { + "max_radial": 3, + "radius": 2.5, + "type": "Gto" + }, + "spline_accuracy": null, + "type": "TensorProduct" + }, + "density": { + "exponent": 1, + "type": "SmearedPowerLaw", + "smearing": 0.3 } }, "systems": [ diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-1/values.npy.gz b/featomic/tests/data/generated/lode-spherical-expansion/exponent-1/values.npy.gz similarity index 100% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-1/values.npy.gz rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-1/values.npy.gz diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-3/gradients-input.json b/featomic/tests/data/generated/lode-spherical-expansion/exponent-2/gradients-input.json similarity index 78% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-3/gradients-input.json rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-2/gradients-input.json index 2fc3bdd59..8ce2eac77 100644 --- a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-3/gradients-input.json +++ b/featomic/tests/data/generated/lode-spherical-expansion/exponent-2/gradients-input.json @@ -1,15 +1,19 @@ { "hyperparameters": { - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "cutoff": 2.5, - "max_angular": 3, - "max_radial": 3, - "potential_exponent": 3, - "radial_basis": { - "Gto": { - "splined_radial_integral": false - } + "basis": { + "max_angular": 3, + "radial": { + "max_radial": 2, + "radius": 2.5, + "type": "Gto" + }, + "spline_accuracy": null, + "type": "TensorProduct" + }, + "density": { + "exponent": 2, + "type": "SmearedPowerLaw", + "smearing": 0.3 } }, "systems": [ diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-2/positions-gradient.npy.gz b/featomic/tests/data/generated/lode-spherical-expansion/exponent-2/positions-gradient.npy.gz similarity index 100% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-2/positions-gradient.npy.gz rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-2/positions-gradient.npy.gz diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-2/values-input.json b/featomic/tests/data/generated/lode-spherical-expansion/exponent-2/values-input.json similarity index 78% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-2/values-input.json rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-2/values-input.json index 63d6e9795..aa8b341d7 100644 --- a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-2/values-input.json +++ b/featomic/tests/data/generated/lode-spherical-expansion/exponent-2/values-input.json @@ -1,15 +1,19 @@ { "hyperparameters": { - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "cutoff": 2.5, - "max_angular": 4, - "max_radial": 4, - "potential_exponent": 2, - "radial_basis": { - "Gto": { - "splined_radial_integral": false - } + "basis": { + "max_angular": 4, + "radial": { + "max_radial": 3, + "radius": 2.5, + "type": "Gto" + }, + "spline_accuracy": null, + "type": "TensorProduct" + }, + "density": { + "exponent": 2, + "type": "SmearedPowerLaw", + "smearing": 0.3 } }, "systems": [ diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-2/values.npy.gz b/featomic/tests/data/generated/lode-spherical-expansion/exponent-2/values.npy.gz similarity index 100% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-2/values.npy.gz rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-2/values.npy.gz diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-4/gradients-input.json b/featomic/tests/data/generated/lode-spherical-expansion/exponent-3/gradients-input.json similarity index 78% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-4/gradients-input.json rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-3/gradients-input.json index b06eb04df..d415fd8d7 100644 --- a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-4/gradients-input.json +++ b/featomic/tests/data/generated/lode-spherical-expansion/exponent-3/gradients-input.json @@ -1,15 +1,19 @@ { "hyperparameters": { - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "cutoff": 2.5, - "max_angular": 3, - "max_radial": 3, - "potential_exponent": 4, - "radial_basis": { - "Gto": { - "splined_radial_integral": false - } + "basis": { + "max_angular": 3, + "radial": { + "max_radial": 2, + "radius": 2.5, + "type": "Gto" + }, + "spline_accuracy": null, + "type": "TensorProduct" + }, + "density": { + "exponent": 3, + "type": "SmearedPowerLaw", + "smearing": 0.3 } }, "systems": [ diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-3/positions-gradient.npy.gz b/featomic/tests/data/generated/lode-spherical-expansion/exponent-3/positions-gradient.npy.gz similarity index 100% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-3/positions-gradient.npy.gz rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-3/positions-gradient.npy.gz diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-1/values-input.json b/featomic/tests/data/generated/lode-spherical-expansion/exponent-3/values-input.json similarity index 78% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-1/values-input.json rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-3/values-input.json index 62e7e718f..ea517218d 100644 --- a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-1/values-input.json +++ b/featomic/tests/data/generated/lode-spherical-expansion/exponent-3/values-input.json @@ -1,15 +1,19 @@ { "hyperparameters": { - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "cutoff": 2.5, - "max_angular": 4, - "max_radial": 4, - "potential_exponent": 1, - "radial_basis": { - "Gto": { - "splined_radial_integral": false - } + "basis": { + "max_angular": 4, + "radial": { + "max_radial": 3, + "radius": 2.5, + "type": "Gto" + }, + "spline_accuracy": null, + "type": "TensorProduct" + }, + "density": { + "exponent": 3, + "type": "SmearedPowerLaw", + "smearing": 0.3 } }, "systems": [ diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-3/values.npy.gz b/featomic/tests/data/generated/lode-spherical-expansion/exponent-3/values.npy.gz similarity index 100% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-3/values.npy.gz rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-3/values.npy.gz diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-2/gradients-input.json b/featomic/tests/data/generated/lode-spherical-expansion/exponent-4/gradients-input.json similarity index 78% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-2/gradients-input.json rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-4/gradients-input.json index eb3c9ac2e..ac87632ac 100644 --- a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-2/gradients-input.json +++ b/featomic/tests/data/generated/lode-spherical-expansion/exponent-4/gradients-input.json @@ -1,15 +1,19 @@ { "hyperparameters": { - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "cutoff": 2.5, - "max_angular": 3, - "max_radial": 3, - "potential_exponent": 2, - "radial_basis": { - "Gto": { - "splined_radial_integral": false - } + "basis": { + "max_angular": 3, + "radial": { + "max_radial": 2, + "radius": 2.5, + "type": "Gto" + }, + "spline_accuracy": null, + "type": "TensorProduct" + }, + "density": { + "exponent": 4, + "type": "SmearedPowerLaw", + "smearing": 0.3 } }, "systems": [ diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-4/positions-gradient.npy.gz b/featomic/tests/data/generated/lode-spherical-expansion/exponent-4/positions-gradient.npy.gz similarity index 100% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-4/positions-gradient.npy.gz rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-4/positions-gradient.npy.gz diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-4/values-input.json b/featomic/tests/data/generated/lode-spherical-expansion/exponent-4/values-input.json similarity index 78% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-4/values-input.json rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-4/values-input.json index 1a1d4883d..8dc745203 100644 --- a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-4/values-input.json +++ b/featomic/tests/data/generated/lode-spherical-expansion/exponent-4/values-input.json @@ -1,15 +1,19 @@ { "hyperparameters": { - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "cutoff": 2.5, - "max_angular": 4, - "max_radial": 4, - "potential_exponent": 4, - "radial_basis": { - "Gto": { - "splined_radial_integral": false - } + "basis": { + "max_angular": 4, + "radial": { + "max_radial": 3, + "radius": 2.5, + "type": "Gto" + }, + "spline_accuracy": null, + "type": "TensorProduct" + }, + "density": { + "exponent": 4, + "type": "SmearedPowerLaw", + "smearing": 0.3 } }, "systems": [ diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-4/values.npy.gz b/featomic/tests/data/generated/lode-spherical-expansion/exponent-4/values.npy.gz similarity index 100% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-4/values.npy.gz rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-4/values.npy.gz diff --git a/featomic/tests/data/generated/lode-spherical-expansion/exponent-5/gradients-input.json b/featomic/tests/data/generated/lode-spherical-expansion/exponent-5/gradients-input.json new file mode 100644 index 000000000..6d517e2bd --- /dev/null +++ b/featomic/tests/data/generated/lode-spherical-expansion/exponent-5/gradients-input.json @@ -0,0 +1,92 @@ +{ + "hyperparameters": { + "basis": { + "max_angular": 3, + "radial": { + "max_radial": 2, + "radius": 2.5, + "type": "Gto" + }, + "spline_accuracy": null, + "type": "TensorProduct" + }, + "density": { + "exponent": 5, + "type": "SmearedPowerLaw", + "smearing": 0.3 + } + }, + "systems": [ + { + "cell": [ + 6.0, + 0.0, + 0.0, + 0.0, + 6.0, + 0.0, + 0.0, + 0.0, + 6.0 + ], + "positions": [ + [ + 3.88997, + 5.11396, + 1.9859 + ], + [ + 1.60538, + 5.74085, + 3.48071 + ], + [ + 5.15178, + 5.59335, + 5.55114 + ], + [ + 2.22548, + 2.03678, + 4.16896 + ], + [ + 2.33853, + 2.79487, + 2.3533 + ], + [ + 3.54073, + 3.59016, + 2.34664 + ], + [ + 1.34344, + 2.94555, + 2.4665 + ], + [ + 1.74165, + 3.03466, + 0.921584 + ], + [ + 0.474942, + 3.34246, + 2.73754 + ] + ], + "types": [ + 6, + 1, + 8, + 6, + 6, + 6, + 6, + 1, + 1 + ] + } + ] +} diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-5/positions-gradient.npy.gz b/featomic/tests/data/generated/lode-spherical-expansion/exponent-5/positions-gradient.npy.gz similarity index 100% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-5/positions-gradient.npy.gz rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-5/positions-gradient.npy.gz diff --git a/featomic/tests/data/generated/lode-spherical-expansion/exponent-5/values-input.json b/featomic/tests/data/generated/lode-spherical-expansion/exponent-5/values-input.json new file mode 100644 index 000000000..cf102ad27 --- /dev/null +++ b/featomic/tests/data/generated/lode-spherical-expansion/exponent-5/values-input.json @@ -0,0 +1,92 @@ +{ + "hyperparameters": { + "basis": { + "max_angular": 4, + "radial": { + "max_radial": 3, + "radius": 2.5, + "type": "Gto" + }, + "spline_accuracy": null, + "type": "TensorProduct" + }, + "density": { + "exponent": 5, + "type": "SmearedPowerLaw", + "smearing": 0.3 + } + }, + "systems": [ + { + "cell": [ + 6.0, + 0.0, + 0.0, + 0.0, + 6.0, + 0.0, + 0.0, + 0.0, + 6.0 + ], + "positions": [ + [ + 3.88997, + 5.11396, + 1.9859 + ], + [ + 1.60538, + 5.74085, + 3.48071 + ], + [ + 5.15178, + 5.59335, + 5.55114 + ], + [ + 2.22548, + 2.03678, + 4.16896 + ], + [ + 2.33853, + 2.79487, + 2.3533 + ], + [ + 3.54073, + 3.59016, + 2.34664 + ], + [ + 1.34344, + 2.94555, + 2.4665 + ], + [ + 1.74165, + 3.03466, + 0.921584 + ], + [ + 0.474942, + 3.34246, + 2.73754 + ] + ], + "types": [ + 6, + 1, + 8, + 6, + 6, + 6, + 6, + 1, + 1 + ] + } + ] +} diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-5/values.npy.gz b/featomic/tests/data/generated/lode-spherical-expansion/exponent-5/values.npy.gz similarity index 100% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-5/values.npy.gz rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-5/values.npy.gz diff --git a/featomic/tests/data/generated/lode-spherical-expansion/exponent-6/gradients-input.json b/featomic/tests/data/generated/lode-spherical-expansion/exponent-6/gradients-input.json new file mode 100644 index 000000000..c2f8fa5fb --- /dev/null +++ b/featomic/tests/data/generated/lode-spherical-expansion/exponent-6/gradients-input.json @@ -0,0 +1,92 @@ +{ + "hyperparameters": { + "basis": { + "max_angular": 3, + "radial": { + "max_radial": 2, + "radius": 2.5, + "type": "Gto" + }, + "spline_accuracy": null, + "type": "TensorProduct" + }, + "density": { + "exponent": 6, + "type": "SmearedPowerLaw", + "smearing": 0.3 + } + }, + "systems": [ + { + "cell": [ + 6.0, + 0.0, + 0.0, + 0.0, + 6.0, + 0.0, + 0.0, + 0.0, + 6.0 + ], + "positions": [ + [ + 3.88997, + 5.11396, + 1.9859 + ], + [ + 1.60538, + 5.74085, + 3.48071 + ], + [ + 5.15178, + 5.59335, + 5.55114 + ], + [ + 2.22548, + 2.03678, + 4.16896 + ], + [ + 2.33853, + 2.79487, + 2.3533 + ], + [ + 3.54073, + 3.59016, + 2.34664 + ], + [ + 1.34344, + 2.94555, + 2.4665 + ], + [ + 1.74165, + 3.03466, + 0.921584 + ], + [ + 0.474942, + 3.34246, + 2.73754 + ] + ], + "types": [ + 6, + 1, + 8, + 6, + 6, + 6, + 6, + 1, + 1 + ] + } + ] +} diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-6/positions-gradient.npy.gz b/featomic/tests/data/generated/lode-spherical-expansion/exponent-6/positions-gradient.npy.gz similarity index 100% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-6/positions-gradient.npy.gz rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-6/positions-gradient.npy.gz diff --git a/featomic/tests/data/generated/lode-spherical-expansion/exponent-6/values-input.json b/featomic/tests/data/generated/lode-spherical-expansion/exponent-6/values-input.json new file mode 100644 index 000000000..4fa3e47c8 --- /dev/null +++ b/featomic/tests/data/generated/lode-spherical-expansion/exponent-6/values-input.json @@ -0,0 +1,92 @@ +{ + "hyperparameters": { + "basis": { + "max_angular": 4, + "radial": { + "max_radial": 3, + "radius": 2.5, + "type": "Gto" + }, + "spline_accuracy": null, + "type": "TensorProduct" + }, + "density": { + "exponent": 6, + "type": "SmearedPowerLaw", + "smearing": 0.3 + } + }, + "systems": [ + { + "cell": [ + 6.0, + 0.0, + 0.0, + 0.0, + 6.0, + 0.0, + 0.0, + 0.0, + 6.0 + ], + "positions": [ + [ + 3.88997, + 5.11396, + 1.9859 + ], + [ + 1.60538, + 5.74085, + 3.48071 + ], + [ + 5.15178, + 5.59335, + 5.55114 + ], + [ + 2.22548, + 2.03678, + 4.16896 + ], + [ + 2.33853, + 2.79487, + 2.3533 + ], + [ + 3.54073, + 3.59016, + 2.34664 + ], + [ + 1.34344, + 2.94555, + 2.4665 + ], + [ + 1.74165, + 3.03466, + 0.921584 + ], + [ + 0.474942, + 3.34246, + 2.73754 + ] + ], + "types": [ + 6, + 1, + 8, + 6, + 6, + 6, + 6, + 1, + 1 + ] + } + ] +} diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-6/values.npy.gz b/featomic/tests/data/generated/lode-spherical-expansion/exponent-6/values.npy.gz similarity index 100% rename from rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-6/values.npy.gz rename to featomic/tests/data/generated/lode-spherical-expansion/exponent-6/values.npy.gz diff --git a/rascaline/tests/data/generated/soap-power-spectrum-cell-gradient.npy.gz b/featomic/tests/data/generated/soap-power-spectrum-cell-gradient.npy.gz similarity index 100% rename from rascaline/tests/data/generated/soap-power-spectrum-cell-gradient.npy.gz rename to featomic/tests/data/generated/soap-power-spectrum-cell-gradient.npy.gz diff --git a/rascaline/tests/data/generated/soap-power-spectrum-gradients-input.json b/featomic/tests/data/generated/soap-power-spectrum-gradients-input.json similarity index 76% rename from rascaline/tests/data/generated/soap-power-spectrum-gradients-input.json rename to featomic/tests/data/generated/soap-power-spectrum-gradients-input.json index e02c564ed..4cc288456 100644 --- a/rascaline/tests/data/generated/soap-power-spectrum-gradients-input.json +++ b/featomic/tests/data/generated/soap-power-spectrum-gradients-input.json @@ -1,19 +1,24 @@ { "hyperparameters": { - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "cutoff": 5.5, - "cutoff_function": { - "ShiftedCosine": { + "basis": { + "max_angular": 4, + "radial": { + "max_radial": 2, + "type": "Gto" + }, + "spline_accuracy": null, + "type": "TensorProduct" + }, + "cutoff": { + "radius": 5.5, + "smoothing": { + "type": "ShiftedCosine", "width": 0.5 } }, - "max_angular": 4, - "max_radial": 3, - "radial_basis": { - "Gto": { - "splined_radial_integral": false - } + "density": { + "type": "Gaussian", + "width": 0.3 } }, "systems": [ diff --git a/rascaline/tests/data/generated/soap-power-spectrum-positions-gradient.npy.gz b/featomic/tests/data/generated/soap-power-spectrum-positions-gradient.npy.gz similarity index 100% rename from rascaline/tests/data/generated/soap-power-spectrum-positions-gradient.npy.gz rename to featomic/tests/data/generated/soap-power-spectrum-positions-gradient.npy.gz diff --git a/rascaline/tests/data/generated/soap-power-spectrum-strain-gradient.npy.gz b/featomic/tests/data/generated/soap-power-spectrum-strain-gradient.npy.gz similarity index 100% rename from rascaline/tests/data/generated/soap-power-spectrum-strain-gradient.npy.gz rename to featomic/tests/data/generated/soap-power-spectrum-strain-gradient.npy.gz diff --git a/rascaline/tests/data/generated/soap-power-spectrum-values-input.json b/featomic/tests/data/generated/soap-power-spectrum-values-input.json similarity index 76% rename from rascaline/tests/data/generated/soap-power-spectrum-values-input.json rename to featomic/tests/data/generated/soap-power-spectrum-values-input.json index f80f57baa..32e1d7a75 100644 --- a/rascaline/tests/data/generated/soap-power-spectrum-values-input.json +++ b/featomic/tests/data/generated/soap-power-spectrum-values-input.json @@ -1,19 +1,24 @@ { "hyperparameters": { - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "cutoff": 5.5, - "cutoff_function": { - "ShiftedCosine": { + "basis": { + "max_angular": 8, + "radial": { + "max_radial": 7, + "type": "Gto" + }, + "spline_accuracy": null, + "type": "TensorProduct" + }, + "cutoff": { + "radius": 5.5, + "smoothing": { + "type": "ShiftedCosine", "width": 0.5 } }, - "max_angular": 8, - "max_radial": 8, - "radial_basis": { - "Gto": { - "splined_radial_integral": false - } + "density": { + "type": "Gaussian", + "width": 0.3 } }, "systems": [ diff --git a/rascaline/tests/data/generated/soap-power-spectrum-values.npy.gz b/featomic/tests/data/generated/soap-power-spectrum-values.npy.gz similarity index 100% rename from rascaline/tests/data/generated/soap-power-spectrum-values.npy.gz rename to featomic/tests/data/generated/soap-power-spectrum-values.npy.gz diff --git a/rascaline/tests/data/generated/spherical-expansion-cell-gradient.npy.gz b/featomic/tests/data/generated/spherical-expansion-cell-gradient.npy.gz similarity index 100% rename from rascaline/tests/data/generated/spherical-expansion-cell-gradient.npy.gz rename to featomic/tests/data/generated/spherical-expansion-cell-gradient.npy.gz diff --git a/rascaline/tests/data/generated/spherical-expansion-gradients-input.json b/featomic/tests/data/generated/spherical-expansion-gradients-input.json similarity index 76% rename from rascaline/tests/data/generated/spherical-expansion-gradients-input.json rename to featomic/tests/data/generated/spherical-expansion-gradients-input.json index 48110d04b..017e5e9e6 100644 --- a/rascaline/tests/data/generated/spherical-expansion-gradients-input.json +++ b/featomic/tests/data/generated/spherical-expansion-gradients-input.json @@ -1,19 +1,24 @@ { "hyperparameters": { - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "cutoff": 5.5, - "cutoff_function": { - "ShiftedCosine": { + "basis": { + "max_angular": 4, + "radial": { + "max_radial": 3, + "type": "Gto" + }, + "spline_accuracy": null, + "type": "TensorProduct" + }, + "cutoff": { + "radius": 5.5, + "smoothing": { + "type": "ShiftedCosine", "width": 0.5 } }, - "max_angular": 4, - "max_radial": 4, - "radial_basis": { - "Gto": { - "splined_radial_integral": false - } + "density": { + "type": "Gaussian", + "width": 0.3 } }, "systems": [ diff --git a/rascaline/tests/data/generated/spherical-expansion-pbc-values-input.json b/featomic/tests/data/generated/spherical-expansion-pbc-values-input.json similarity index 86% rename from rascaline/tests/data/generated/spherical-expansion-pbc-values-input.json rename to featomic/tests/data/generated/spherical-expansion-pbc-values-input.json index 8d67738c6..9bfdea66b 100644 --- a/rascaline/tests/data/generated/spherical-expansion-pbc-values-input.json +++ b/featomic/tests/data/generated/spherical-expansion-pbc-values-input.json @@ -1,19 +1,24 @@ { "hyperparameters": { - "atomic_gaussian_width": 0.5, - "center_atom_weight": 1.0, - "cutoff": 4.5, - "cutoff_function": { - "ShiftedCosine": { + "basis": { + "max_angular": 6, + "radial": { + "max_radial": 5, + "type": "Gto" + }, + "spline_accuracy": null, + "type": "TensorProduct" + }, + "cutoff": { + "radius": 4.5, + "smoothing": { + "type": "ShiftedCosine", "width": 0.2 } }, - "max_angular": 6, - "max_radial": 6, - "radial_basis": { - "Gto": { - "splined_radial_integral": false - } + "density": { + "type": "Gaussian", + "width": 0.5 } }, "systems": [ diff --git a/rascaline/tests/data/generated/spherical-expansion-pbc-values.npy.gz b/featomic/tests/data/generated/spherical-expansion-pbc-values.npy.gz similarity index 100% rename from rascaline/tests/data/generated/spherical-expansion-pbc-values.npy.gz rename to featomic/tests/data/generated/spherical-expansion-pbc-values.npy.gz diff --git a/rascaline/tests/data/generated/spherical-expansion-positions-gradient.npy.gz b/featomic/tests/data/generated/spherical-expansion-positions-gradient.npy.gz similarity index 100% rename from rascaline/tests/data/generated/spherical-expansion-positions-gradient.npy.gz rename to featomic/tests/data/generated/spherical-expansion-positions-gradient.npy.gz diff --git a/rascaline/tests/data/generated/spherical-expansion-strain-gradient.npy.gz b/featomic/tests/data/generated/spherical-expansion-strain-gradient.npy.gz similarity index 100% rename from rascaline/tests/data/generated/spherical-expansion-strain-gradient.npy.gz rename to featomic/tests/data/generated/spherical-expansion-strain-gradient.npy.gz diff --git a/rascaline/tests/data/generated/spherical-expansion-values-input.json b/featomic/tests/data/generated/spherical-expansion-values-input.json similarity index 76% rename from rascaline/tests/data/generated/spherical-expansion-values-input.json rename to featomic/tests/data/generated/spherical-expansion-values-input.json index f80f57baa..32e1d7a75 100644 --- a/rascaline/tests/data/generated/spherical-expansion-values-input.json +++ b/featomic/tests/data/generated/spherical-expansion-values-input.json @@ -1,19 +1,24 @@ { "hyperparameters": { - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "cutoff": 5.5, - "cutoff_function": { - "ShiftedCosine": { + "basis": { + "max_angular": 8, + "radial": { + "max_radial": 7, + "type": "Gto" + }, + "spline_accuracy": null, + "type": "TensorProduct" + }, + "cutoff": { + "radius": 5.5, + "smoothing": { + "type": "ShiftedCosine", "width": 0.5 } }, - "max_angular": 8, - "max_radial": 8, - "radial_basis": { - "Gto": { - "splined_radial_integral": false - } + "density": { + "type": "Gaussian", + "width": 0.3 } }, "systems": [ diff --git a/rascaline/tests/data/generated/spherical-expansion-values.npy.gz b/featomic/tests/data/generated/spherical-expansion-values.npy.gz similarity index 100% rename from rascaline/tests/data/generated/spherical-expansion-values.npy.gz rename to featomic/tests/data/generated/spherical-expansion-values.npy.gz diff --git a/rascaline/tests/data/generated/spherical-harmonics-input.json b/featomic/tests/data/generated/spherical-harmonics-input.json similarity index 99% rename from rascaline/tests/data/generated/spherical-harmonics-input.json rename to featomic/tests/data/generated/spherical-harmonics-input.json index f7a38db05..01b015c65 100644 --- a/rascaline/tests/data/generated/spherical-harmonics-input.json +++ b/featomic/tests/data/generated/spherical-harmonics-input.json @@ -37,4 +37,4 @@ ] ], "max_angular": 25 -} \ No newline at end of file +} diff --git a/rascaline/tests/data/generated/spherical-harmonics-values.npy.gz b/featomic/tests/data/generated/spherical-harmonics-values.npy.gz similarity index 100% rename from rascaline/tests/data/generated/spherical-harmonics-values.npy.gz rename to featomic/tests/data/generated/spherical-harmonics-values.npy.gz diff --git a/rascaline/tests/data/lode-spherical-expansion.py b/featomic/tests/data/lode-spherical-expansion.py similarity index 63% rename from rascaline/tests/data/lode-spherical-expansion.py rename to featomic/tests/data/lode-spherical-expansion.py index ada901c54..115c38480 100644 --- a/rascaline/tests/data/lode-spherical-expansion.py +++ b/featomic/tests/data/lode-spherical-expansion.py @@ -2,10 +2,9 @@ import ase import numpy as np -from ase import io # noqa - -from rascaline import LodeSphericalExpansion +from ase import io # noqa: F401 +from featomic import LodeSphericalExpansion from save_data import save_calculator_input, save_numpy_array @@ -35,7 +34,7 @@ def sum_gradient(descriptor): gradient = descriptor.block().gradient("positions") result = np.zeros((len(frame), 3, len(gradient.properties))) - for sample, row in zip(gradient.samples, gradient.data): + for sample, row in zip(gradient.samples, gradient.values): result[sample["atom"], :, :] += row[:, :] return result @@ -47,12 +46,12 @@ def sum_gradient(descriptor): pass -for potential_exponent in [1, 2, 3, 4, 5, 6]: +for exponent in [1, 2, 3, 4, 5, 6]: path = os.path.join( ROOT, "generated", "lode-spherical-expansion", - f"potential_exponent-{potential_exponent}", + f"exponent-{exponent}", ) try: os.mkdir(path) @@ -60,33 +59,33 @@ def sum_gradient(descriptor): pass hyperparameters = { - "cutoff": 2.5, - "max_radial": 4, - "max_angular": 4, - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "radial_basis": { - "Gto": { - "splined_radial_integral": False, - }, + "density": { + "type": "SmearedPowerLaw", + "smearing": 0.3, + "exponent": exponent, + }, + "basis": { + "type": "TensorProduct", + "max_angular": 4, + "radial": {"max_radial": 3, "type": "Gto", "radius": 2.5}, + "spline_accuracy": None, }, - "potential_exponent": potential_exponent, } calculator = LodeSphericalExpansion(**hyperparameters) descriptor = calculator.compute(frame, use_native_system=True) - descriptor.keys_to_samples("center_type") - descriptor.keys_to_properties("neighbor_type") - descriptor.components_to_properties("o3_mu") - descriptor.keys_to_properties("o3_lambda") + descriptor = descriptor.keys_to_samples("center_type") + descriptor = descriptor.keys_to_properties("neighbor_type") + descriptor = descriptor.components_to_properties("o3_mu") + descriptor = descriptor.keys_to_properties("o3_lambda") save_calculator_input(os.path.join(path, "values"), frame, hyperparameters) save_numpy_array(os.path.join(path, "values"), descriptor.block().values) # Use smaller hypers for gradients to keep the file size low - hyperparameters["max_radial"] = 3 - hyperparameters["max_angular"] = 3 + hyperparameters["basis"]["radial"]["max_radial"] = 2 + hyperparameters["basis"]["max_angular"] = 3 calculator = LodeSphericalExpansion(**hyperparameters) descriptor = calculator.compute( @@ -95,10 +94,10 @@ def sum_gradient(descriptor): gradients=["positions"], ) - descriptor.keys_to_samples("center_type") - descriptor.keys_to_properties("neighbor_type") - descriptor.components_to_properties("o3_mu") - descriptor.keys_to_properties("o3_lambda") + descriptor = descriptor.keys_to_samples("center_type") + descriptor = descriptor.keys_to_properties("neighbor_type") + descriptor = descriptor.components_to_properties("o3_mu") + descriptor = descriptor.keys_to_properties("o3_lambda") save_calculator_input(os.path.join(path, "gradients"), frame, hyperparameters) save_numpy_array(os.path.join(path, "positions-gradient"), sum_gradient(descriptor)) diff --git a/rascaline/tests/data/mod.rs b/featomic/tests/data/mod.rs similarity index 89% rename from rascaline/tests/data/mod.rs rename to featomic/tests/data/mod.rs index fd6045c55..2b07284ba 100644 --- a/rascaline/tests/data/mod.rs +++ b/featomic/tests/data/mod.rs @@ -6,12 +6,12 @@ use ndarray_npy::ReadNpyExt; use ndarray::ArrayD; use flate2::read::GzDecoder; -use rascaline::{SimpleSystem, System, Matrix3, Vector3D}; -use rascaline::systems::UnitCell; +use featomic::{SimpleSystem, System, Matrix3, Vector3D}; +use featomic::systems::UnitCell; type HyperParameters = String; -pub fn load_calculator_input(path: impl AsRef) -> (Vec>, HyperParameters) { +pub fn load_calculator_input(path: impl AsRef) -> (Vec, HyperParameters) { let json = std::fs::read_to_string(format!("tests/data/generated/{}", path.as_ref().display())) .expect("failed to read input file"); @@ -38,7 +38,7 @@ pub fn load_calculator_input(path: impl AsRef) -> (Vec>, H simple_system.add_atom(atomic_type, position); } - systems.push(Box::new(simple_system) as Box); + systems.push(System::new(simple_system)); } (systems, parameters) diff --git a/rascaline/tests/data/requirements.txt b/featomic/tests/data/requirements.txt similarity index 100% rename from rascaline/tests/data/requirements.txt rename to featomic/tests/data/requirements.txt diff --git a/rascaline/tests/data/save_data.py b/featomic/tests/data/save_data.py similarity index 100% rename from rascaline/tests/data/save_data.py rename to featomic/tests/data/save_data.py diff --git a/rascaline/tests/data/soap-power-spectrum.py b/featomic/tests/data/soap-power-spectrum.py similarity index 82% rename from rascaline/tests/data/soap-power-spectrum.py rename to featomic/tests/data/soap-power-spectrum.py index 897b183f2..82a346f3c 100644 --- a/rascaline/tests/data/soap-power-spectrum.py +++ b/featomic/tests/data/soap-power-spectrum.py @@ -1,7 +1,7 @@ import ase import numpy as np -from rascaline import SoapPowerSpectrum +from featomic import SoapPowerSpectrum from save_data import save_calculator_input, save_numpy_array @@ -22,20 +22,19 @@ ) hyperparameters = { - "cutoff": 5.5, - "max_radial": 8, - "max_angular": 8, - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "radial_basis": { - "Gto": { - "splined_radial_integral": False, - }, + "cutoff": { + "radius": 5.5, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, }, - "cutoff_function": { - "ShiftedCosine": { - "width": 0.5, - } + "density": { + "type": "Gaussian", + "width": 0.3, + }, + "basis": { + "type": "TensorProduct", + "max_angular": 8, + "radial": {"max_radial": 7, "type": "Gto"}, + "spline_accuracy": None, }, } @@ -48,8 +47,8 @@ save_numpy_array("soap-power-spectrum-values", descriptor.block().values) # Use less values for gradients to keep the file size low -hyperparameters["max_radial"] = 3 -hyperparameters["max_angular"] = 4 +hyperparameters["basis"]["radial"]["max_radial"] = 2 +hyperparameters["basis"]["max_angular"] = 4 frame.cell = [6.0, 6.0, 6.0] frame.pbc = [True, True, True] diff --git a/rascaline/tests/data/soap-spherical-expansion.py b/featomic/tests/data/soap-spherical-expansion.py similarity index 80% rename from rascaline/tests/data/soap-spherical-expansion.py rename to featomic/tests/data/soap-spherical-expansion.py index 5d48fbd4d..f40f80581 100644 --- a/rascaline/tests/data/soap-spherical-expansion.py +++ b/featomic/tests/data/soap-spherical-expansion.py @@ -2,9 +2,9 @@ import ase import numpy as np -from ase import io # noqa +from ase import io # noqa: F401 -from rascaline import SphericalExpansion +from featomic import SphericalExpansion from save_data import save_calculator_input, save_numpy_array @@ -26,20 +26,19 @@ ) hyperparameters = { - "cutoff": 5.5, - "max_radial": 8, - "max_angular": 8, - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "radial_basis": { - "Gto": { - "splined_radial_integral": False, - }, + "cutoff": { + "radius": 5.5, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, }, - "cutoff_function": { - "ShiftedCosine": { - "width": 0.5, - } + "density": { + "type": "Gaussian", + "width": 0.3, + }, + "basis": { + "type": "TensorProduct", + "max_angular": 8, + "radial": {"max_radial": 7, "type": "Gto"}, + "spline_accuracy": None, }, } @@ -67,8 +66,8 @@ def sum_gradient(descriptor): # Use smaller hypers for gradients to keep the file size low -hyperparameters["max_radial"] = 4 -hyperparameters["max_angular"] = 4 +hyperparameters["basis"]["radial"]["max_radial"] = 3 +hyperparameters["basis"]["max_angular"] = 4 frame.cell = [6.0, 6.0, 6.0] frame.pbc = [True, True, True] @@ -108,20 +107,19 @@ def sum_gradient(descriptor): frame.pop(5) hyperparameters = { - "cutoff": 4.5, - "max_radial": 6, - "max_angular": 6, - "atomic_gaussian_width": 0.5, - "center_atom_weight": 1.0, - "radial_basis": { - "Gto": { - "splined_radial_integral": False, - }, + "cutoff": { + "radius": 4.5, + "smoothing": {"type": "ShiftedCosine", "width": 0.2}, + }, + "density": { + "type": "Gaussian", + "width": 0.5, }, - "cutoff_function": { - "ShiftedCosine": { - "width": 0.2, - } + "basis": { + "type": "TensorProduct", + "max_angular": 6, + "radial": {"max_radial": 5, "type": "Gto"}, + "spline_accuracy": None, }, } diff --git a/rascaline/tests/data/spherical-harmonics.py b/featomic/tests/data/spherical-harmonics.py similarity index 100% rename from rascaline/tests/data/spherical-harmonics.py rename to featomic/tests/data/spherical-harmonics.py diff --git a/rascaline/tests/file-size-limit.rs b/featomic/tests/file-size-limit.rs similarity index 100% rename from rascaline/tests/file-size-limit.rs rename to featomic/tests/file-size-limit.rs diff --git a/rascaline/tests/lode-madelung.rs b/featomic/tests/lode-madelung.rs similarity index 71% rename from rascaline/tests/lode-madelung.rs rename to featomic/tests/lode-madelung.rs index 159407563..1c5801841 100644 --- a/rascaline/tests/lode-madelung.rs +++ b/featomic/tests/lode-madelung.rs @@ -7,13 +7,13 @@ //! for reference values and detailed explanations on these constants. use approx::assert_relative_eq; -use rascaline::calculators::RadialBasis; -use rascaline::calculators::{LodeSphericalExpansionParameters, CalculatorBase, LodeSphericalExpansion}; -use rascaline::systems::{System, SimpleSystem, UnitCell}; -use rascaline::{Calculator, Matrix3, Vector3D, CalculationOptions}; +use featomic::calculators::{Density, DensityKind, LodeRadialBasis, SphericalExpansionBasis, TensorProductBasis}; +use featomic::calculators::{LodeSphericalExpansionParameters, CalculatorBase, LodeSphericalExpansion}; +use featomic::systems::{System, SimpleSystem, UnitCell}; +use featomic::{Calculator, Matrix3, Vector3D, CalculationOptions}; struct CrystalParameters { - systems: Vec>, + systems: Vec, charges: Vec, madelung: f64, } @@ -22,42 +22,42 @@ struct CrystalParameters { /// Using a primitive unit cell, the distance between the /// closest Na-Cl pair is exactly 1. The cubic unit cell /// in these units would have a length of 2. -fn get_nacl() -> Vec> { +fn get_nacl() -> Vec { let cell = Matrix3::new([[0.0, 1.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 0.0]]); let mut system = SimpleSystem::new(UnitCell::from(cell)); system.add_atom(11, Vector3D::new(0.0, 0.0, 0.0)); system.add_atom(17, Vector3D::new(1.0, 0.0, 0.0)); - vec![Box::new(system) as Box] + vec![System::new(system)] } /// CsCl structure /// This structure is simple since the primitive unit cell /// is just the usual cubic cell with side length set to one. -fn get_cscl() -> Vec> { +fn get_cscl() -> Vec { let mut system = SimpleSystem::new(UnitCell::cubic(1.0)); system.add_atom(17, Vector3D::new(0.0, 0.0, 0.0)); system.add_atom(55, Vector3D::new(0.5, 0.5, 0.5)); - vec![Box::new(system) as Box] + vec![System::new(system)] } /// ZnS (zincblende) structure /// As for NaCl, a primitive unit cell is used which makes /// the lattice parameter of the cubic cell equal to 2. /// In these units, the closest Zn-S distance is sqrt(3)/2. -fn get_zns() -> Vec> { +fn get_zns() -> Vec { let cell = Matrix3::new([[0.0, 1.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 0.0]]); let mut system = SimpleSystem::new(UnitCell::from(cell)); system.add_atom(16, Vector3D::new(0.0, 0.0, 0.0)); system.add_atom(30, Vector3D::new(0.5, 0.5, 0.5)); - vec![Box::new(system) as Box] + vec![System::new(system)] } /// ZnS (O4) in wurtzite structure (triclinic cell) -fn get_znso4() -> Vec> { +fn get_znso4() -> Vec { let u = 3. / 8.; let c = f64::sqrt(1. / u); let cell = Matrix3::new([[0.5, -0.5 * f64::sqrt(3.0), 0.0], [0.5, 0.5 * f64::sqrt(3.0), 0.0], [0.0, 0.0, c]]); @@ -67,7 +67,7 @@ fn get_znso4() -> Vec> { system.add_atom(16, Vector3D::new(0.5, -0.5 / f64::sqrt(3.0), 0.5 * c)); system.add_atom(30, Vector3D::new(0.5, -0.5 / f64::sqrt(3.0), (0.5 + u) * c)); - vec![Box::new(system) as Box] + vec![System::new(system)] } /// Test the agreement with Madelung constant for a variety of @@ -81,21 +81,27 @@ fn madelung() { CrystalParameters{systems: get_znso4(), charges: vec![1.0, -1.0, 1.0, -1.0], madelung: 1.6413 / f64::sqrt(3. / 8.)} ]; - for cutoff in [0.01_f64, 0.027, 0.074, 0.2] { - let factor = -1.0 / (4.0 * std::f64::consts::PI * cutoff.powf(2.0)).powf(0.75); - for atomic_gaussian_width in [0.2, 0.1] { + for gto_radius in [0.01_f64, 0.027, 0.074, 0.2] { + let factor = -1.0 / (4.0 * std::f64::consts::PI * gto_radius.powf(2.0)).powf(0.75); + for smearing in [0.2, 0.1] { for crystal in crystals.iter_mut() { let lode_parameters = LodeSphericalExpansionParameters { - cutoff, k_cutoff: None, - max_radial: 1, - max_angular: 0, - atomic_gaussian_width, - center_atom_weight: 0.0, - potential_exponent: 1, - radial_basis: RadialBasis::splined_gto(1e-8), + density: Density { + kind: DensityKind::SmearedPowerLaw { + smearing, + exponent: 1, + }, + scaling: None, + center_atom_weight: 0.0, + }, + basis: SphericalExpansionBasis::TensorProduct(TensorProductBasis { + max_angular: 0, + radial: LodeRadialBasis::Gto { max_radial: 0, radius: gto_radius }, + spline_accuracy: Some(1e-8), + }) }; let mut calculator = Calculator::from(Box::new(LodeSphericalExpansion::new( @@ -128,19 +134,25 @@ fn madelung_high_accuracy() { CrystalParameters{systems: get_znso4(), charges: vec![1.0, -1.0, 1.0, -1.0], madelung: 1.6413 / f64::sqrt(3. / 8.)} ]; - let cutoff = 0.01_f64; - let factor = -1.0 / (4.0 * std::f64::consts::PI * cutoff.powf(2.0)).powf(0.75); + let gto_radius = 0.01_f64; + let factor = -1.0 / (4.0 * std::f64::consts::PI * gto_radius.powf(2.0)).powf(0.75); for crystal in crystals.iter_mut() { let lode_parameters = LodeSphericalExpansionParameters { - cutoff, - k_cutoff: Some(50.), - max_radial: 1, - max_angular: 0, - atomic_gaussian_width: 0.1, - center_atom_weight: 0.0, - potential_exponent: 1, - radial_basis: RadialBasis::splined_gto(1e-8), + k_cutoff: Some(50.0), + density: Density { + kind: DensityKind::SmearedPowerLaw { + smearing: 0.1, + exponent: 1, + }, + scaling: None, + center_atom_weight: 0.0, + }, + basis: SphericalExpansionBasis::TensorProduct(TensorProductBasis { + max_angular: 0, + radial: LodeRadialBasis::Gto { max_radial: 0, radius: gto_radius }, + spline_accuracy: Some(1e-8), + }) }; let mut calculator = Calculator::from(Box::new(LodeSphericalExpansion::new( diff --git a/rascaline/tests/lode-spherical-expansion.rs b/featomic/tests/lode-spherical-expansion.rs similarity index 89% rename from rascaline/tests/lode-spherical-expansion.rs rename to featomic/tests/lode-spherical-expansion.rs index 6e28b8186..18bb5f2fd 100644 --- a/rascaline/tests/lode-spherical-expansion.rs +++ b/featomic/tests/lode-spherical-expansion.rs @@ -6,15 +6,15 @@ use ndarray::{ArrayD, Axis, s}; use metatensor::{Labels, TensorBlockRef}; -use rascaline::{Calculator, CalculationOptions}; +use featomic::{Calculator, CalculationOptions}; mod data; #[test] fn values() { - for potential_exponent in [1, 2, 3, 4, 5, 6] { + for exponent in [1, 2, 3, 4, 5, 6] { let mut path = PathBuf::from("lode-spherical-expansion"); - path.push(format!("potential_exponent-{}", potential_exponent)); + path.push(format!("exponent-{}", exponent)); path.push("values-input.json"); let (mut systems, parameters) = data::load_calculator_input(path); @@ -35,7 +35,7 @@ fn values() { let array = block.values().to_array(); let mut path = PathBuf::from("lode-spherical-expansion"); - path.push(format!("potential_exponent-{}", potential_exponent)); + path.push(format!("exponent-{}", exponent)); path.push("values.npy.gz"); let expected = &data::load_expected_values(path); @@ -45,9 +45,9 @@ fn values() { #[test] fn gradients() { - for potential_exponent in [1, 2, 3, 4, 5, 6] { + for exponent in [1, 2, 3, 4, 5, 6] { let mut path = PathBuf::from("lode-spherical-expansion"); - path.push(format!("potential_exponent-{}", potential_exponent)); + path.push(format!("exponent-{}", exponent)); path.push("gradients-input.json"); let (mut systems, parameters) = data::load_calculator_input(path); @@ -75,7 +75,7 @@ fn gradients() { let array = sum_gradients(n_atoms, gradient); let mut path = PathBuf::from("lode-spherical-expansion"); - path.push(format!("potential_exponent-{}", potential_exponent)); + path.push(format!("exponent-{}", exponent)); path.push("positions-gradient.npy.gz"); let expected = &data::load_expected_values(path); diff --git a/rascaline/tests/lode-vs-soap.rs b/featomic/tests/lode-vs-soap.rs similarity index 58% rename from rascaline/tests/lode-vs-soap.rs rename to featomic/tests/lode-vs-soap.rs index 357e47b69..060677fd4 100644 --- a/rascaline/tests/lode-vs-soap.rs +++ b/featomic/tests/lode-vs-soap.rs @@ -1,7 +1,7 @@ use approx::assert_relative_eq; -use rascaline::systems::{System, SimpleSystem, UnitCell}; -use rascaline::{Vector3D, Calculator}; +use featomic::systems::{System, SimpleSystem, UnitCell}; +use featomic::{Vector3D, Calculator}; #[test] @@ -13,35 +13,47 @@ fn lode_vs_soap() { system.add_atom(8, Vector3D::new(2.0, 2.2, 1.0)); system.add_atom(8, Vector3D::new(2.3, 2.0, 1.5)); - let mut systems = vec![Box::new(system) as Box]; + let mut systems = vec![System::new(system)]; // reduce max_radial/max_angular for debug builds to make this test faster let (max_radial, max_angular) = if cfg!(debug_assertions) { - (3, 0) + (2, 0) } else { - (6, 2) + (5, 2) }; let lode_parameters = format!(r#"{{ - "cutoff": 3.0, "k_cutoff": 16.0, - "max_radial": {}, - "max_angular": {}, - "center_atom_weight": 1.0, - "atomic_gaussian_width": 0.3, - "potential_exponent": 0, - "radial_basis": {{"Gto": {{"splined_radial_integral": false}}}} - }}"#, max_radial, max_angular); + "density": {{ + "type": "SmearedPowerLaw", + "smearing": 0.3, + "exponent": 0 + }}, + "basis": {{ + "type": "TensorProduct", + "max_angular": {}, + "radial": {{"max_radial": {}, "type": "Gto", "radius": 3.0}}, + "spline_accuracy": null + }} + }}"#, max_angular, max_radial); let soap_parameters = format!(r#"{{ - "cutoff": 3.0, - "max_radial": {}, - "max_angular": {}, - "center_atom_weight": 1.0, - "atomic_gaussian_width": 0.3, - "radial_basis": {{"Gto": {{"splined_radial_integral": false}}}}, - "cutoff_function": {{"Step": {{}}}} - }}"#, max_radial, max_angular); + "cutoff": {{ + "radius": 3.0, + "smoothing": {{ "type": "Step" }} + }}, + "density": {{ + "type": "Gaussian", + "width": 0.3 + }}, + "basis": {{ + "type": "TensorProduct", + "max_angular": {}, + "radial": {{"max_radial": {}, "type": "Gto"}}, + "spline_accuracy": null + }} + }}"#, max_angular, max_radial); + let mut lode_calculator = Calculator::new( "lode_spherical_expansion", diff --git a/rascaline-c-api/tests/run-cxx-tests.rs b/featomic/tests/run-cxx-tests.rs similarity index 64% rename from rascaline-c-api/tests/run-cxx-tests.rs rename to featomic/tests/run-cxx-tests.rs index 64b0b2759..e5021461a 100644 --- a/rascaline-c-api/tests/run-cxx-tests.rs +++ b/featomic/tests/run-cxx-tests.rs @@ -9,35 +9,43 @@ fn run_cxx_tests() { build_dir.push("cxx-tests"); std::fs::create_dir_all(&build_dir).expect("failed to create build dir"); + // ====================================================================== // + // configure cmake + let mut source_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); source_dir.extend(["tests"]); let mut cmake_config = utils::cmake_config(&source_dir, &build_dir); cmake_config.arg("-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"); - cmake_config.arg("-DRASCALINE_FETCH_METATENSOR=ON"); + cmake_config.arg("-DFEATOMIC_FETCH_METATENSOR=ON"); let mut shared_lib = "ON"; - if let Ok(value) = std::env::var("RASCALINE_TEST_WITH_STATIC_LIB") { + if let Ok(value) = std::env::var("FEATOMIC_TEST_WITH_STATIC_LIB") { if value != "0" { shared_lib = "OFF"; } } cmake_config.arg(format!("-DBUILD_SHARED_LIBS={}", shared_lib)); - // LLVM_PROFILE_FILE is set by cargo tarpaulin, so when it is set we also - // collect code coverage for the C and C++ API. - if std::env::var("LLVM_PROFILE_FILE").is_ok() { - cmake_config.arg("-DRASCAL_ENABLE_COVERAGE=ON"); + // collect code coverage for the C and C++ API at the same time we collect + // it for Rust code + if cfg!(tarpaulin) { + cmake_config.arg("-DFEATOMIC_ENABLE_COVERAGE=ON"); } let status = cmake_config.status().expect("cmake configuration failed"); assert!(status.success()); + // ====================================================================== // + // build the code + let mut cmake_build = utils::cmake_build(&build_dir); let status = cmake_build.status().expect("cmake build failed"); assert!(status.success()); - let mut ctest = utils::ctest(&build_dir); + // ====================================================================== // + // run the tests + let mut ctest = utils::ctest(&build_dir); let status = ctest.status().expect("failed to run tests"); assert!(status.success()); } diff --git a/rascaline/tests/soap-power-spectrum.rs b/featomic/tests/soap-power-spectrum.rs similarity index 98% rename from rascaline/tests/soap-power-spectrum.rs rename to featomic/tests/soap-power-spectrum.rs index 1e5b1fd90..ae7d9c2b5 100644 --- a/rascaline/tests/soap-power-spectrum.rs +++ b/featomic/tests/soap-power-spectrum.rs @@ -3,7 +3,7 @@ use ndarray::{ArrayD, Axis, s}; use metatensor::{Labels, TensorBlockRef}; -use rascaline::{CalculationOptions, Calculator}; +use featomic::{CalculationOptions, Calculator}; mod data; diff --git a/rascaline/tests/soap-spherical-expansion.rs b/featomic/tests/soap-spherical-expansion.rs similarity index 98% rename from rascaline/tests/soap-spherical-expansion.rs rename to featomic/tests/soap-spherical-expansion.rs index 88946b927..976a6c604 100644 --- a/rascaline/tests/soap-spherical-expansion.rs +++ b/featomic/tests/soap-spherical-expansion.rs @@ -3,7 +3,7 @@ use ndarray::{ArrayD, Axis, s}; use metatensor::{Labels, TensorBlockRef}; -use rascaline::{CalculationOptions, Calculator}; +use featomic::{CalculationOptions, Calculator}; mod data; diff --git a/rascaline/tests/spherical-harmonics.rs b/featomic/tests/spherical-harmonics.rs similarity index 96% rename from rascaline/tests/spherical-harmonics.rs rename to featomic/tests/spherical-harmonics.rs index fbd6a362b..76eca36e8 100644 --- a/rascaline/tests/spherical-harmonics.rs +++ b/featomic/tests/spherical-harmonics.rs @@ -1,7 +1,7 @@ use approx::assert_relative_eq; -use rascaline::Vector3D; -use rascaline::math::{ +use featomic::Vector3D; +use featomic::math::{ SphericalHarmonics, SphericalHarmonicsArray }; diff --git a/rascaline-c-api/tests/catch/.gitattributes b/featomic/tests/utils/catch/.gitattributes similarity index 100% rename from rascaline-c-api/tests/catch/.gitattributes rename to featomic/tests/utils/catch/.gitattributes diff --git a/rascaline-c-api/tests/catch/CMakeLists.txt b/featomic/tests/utils/catch/CMakeLists.txt similarity index 100% rename from rascaline-c-api/tests/catch/CMakeLists.txt rename to featomic/tests/utils/catch/CMakeLists.txt diff --git a/rascaline-c-api/tests/catch/catch.cpp b/featomic/tests/utils/catch/catch.cpp similarity index 100% rename from rascaline-c-api/tests/catch/catch.cpp rename to featomic/tests/utils/catch/catch.cpp diff --git a/rascaline-c-api/tests/catch/catch.hpp b/featomic/tests/utils/catch/catch.hpp similarity index 100% rename from rascaline-c-api/tests/catch/catch.hpp rename to featomic/tests/utils/catch/catch.hpp diff --git a/rascaline-c-api/tests/helpers.cpp b/featomic/tests/utils/helpers.cpp similarity index 82% rename from rascaline-c-api/tests/helpers.cpp rename to featomic/tests/utils/helpers.cpp index 5c61a0966..4bdbf4d8d 100644 --- a/rascaline-c-api/tests/helpers.cpp +++ b/featomic/tests/utils/helpers.cpp @@ -4,12 +4,12 @@ #define SQRT_3 1.73205080756887729352 -rascal_system_t simple_system() { - rascal_system_t system = {}; +featomic_system_t simple_system() { + featomic_system_t system = {}; system.size = [](const void* _, uintptr_t* size) { *size = 4; - return RASCAL_SUCCESS; + return FEATOMIC_SUCCESS; }; system.positions = [](const void* _, const double** positions) { @@ -20,13 +20,13 @@ rascal_system_t simple_system() { {3, 3, 3}, }; *positions = POSITIONS[0]; - return RASCAL_SUCCESS; + return FEATOMIC_SUCCESS; }; system.types = [](const void* _, const int32_t** types) { static int32_t TYPES[4] = {6, 1, 1, 1}; *types = TYPES; - return RASCAL_SUCCESS; + return FEATOMIC_SUCCESS; }; system.cell = [](const void* _, double* cell) { @@ -36,7 +36,7 @@ rascal_system_t simple_system() { {0, 0, 10}, }; std::memcpy(cell, CELL, sizeof(CELL)); - return RASCAL_SUCCESS; + return FEATOMIC_SUCCESS; }; // basic compute_neighbors, always returning the same pairs @@ -44,12 +44,12 @@ rascal_system_t simple_system() { if (cutoff < SQRT_3 || cutoff > 3.46410161513775458704) { return -1; } else { - return RASCAL_SUCCESS; + return FEATOMIC_SUCCESS; } }; - system.pairs = [](const void* _, const rascal_pair_t** pairs, uintptr_t* count) { - static rascal_pair_t PAIRS[] = { + system.pairs = [](const void* _, const featomic_pair_t** pairs, uintptr_t* count) { + static featomic_pair_t PAIRS[] = { {0, 1, SQRT_3, {1.0, 1.0, 1.0}, {1, 1, 1}}, {1, 2, SQRT_3, {1.0, 1.0, 1.0}, {0, 0, 0}}, {2, 3, SQRT_3, {1.0, 1.0, 1.0}, {0, 0, 0}}, @@ -57,25 +57,25 @@ rascal_system_t simple_system() { *pairs = PAIRS; *count = 3; - return RASCAL_SUCCESS; + return FEATOMIC_SUCCESS; }; - system.pairs_containing = [](const void* _, uintptr_t atom, const rascal_pair_t** pairs, uintptr_t* count){ - static rascal_pair_t PAIRS_0[] = { + system.pairs_containing = [](const void* _, uintptr_t atom, const featomic_pair_t** pairs, uintptr_t* count){ + static featomic_pair_t PAIRS_0[] = { {0, 1, SQRT_3, {1.0, 1.0, 1.0}, {1, 1, 1}}, }; - static rascal_pair_t PAIRS_1[] = { + static featomic_pair_t PAIRS_1[] = { {0, 1, SQRT_3, {1.0, 1.0, 1.0}, {1, 1, 1}}, {1, 2, SQRT_3, {1.0, 1.0, 1.0}, {0, 0, 0}}, }; - static rascal_pair_t PAIRS_2[] = { + static featomic_pair_t PAIRS_2[] = { {1, 2, SQRT_3, {1.0, 1.0, 1.0}, {0, 0, 0}}, {2, 3, SQRT_3, {1.0, 1.0, 1.0}, {0, 0, 0}}, }; - static rascal_pair_t PAIRS_3[] = { + static featomic_pair_t PAIRS_3[] = { {2, 3, SQRT_3, {1.0, 1.0, 1.0}, {0, 0, 0}}, }; @@ -94,7 +94,7 @@ rascal_system_t simple_system() { } else { return -1; } - return RASCAL_SUCCESS; + return FEATOMIC_SUCCESS; }; return system; diff --git a/rascaline-c-api/tests/helpers.hpp b/featomic/tests/utils/helpers.hpp similarity index 58% rename from rascaline-c-api/tests/helpers.hpp rename to featomic/tests/utils/helpers.hpp index d6bd8b85c..4fdb7d716 100644 --- a/rascaline-c-api/tests/helpers.hpp +++ b/featomic/tests/utils/helpers.hpp @@ -1,13 +1,13 @@ -#ifndef RASCAL_TEST_HELPERS -#define RASCAL_TEST_HELPERS +#ifndef FEATOMIC_TEST_HELPERS +#define FEATOMIC_TEST_HELPERS #include #include "metatensor.h" -#include "rascaline.h" +#include "featomic.h" #define CHECK_SUCCESS(__expr__) REQUIRE((__expr__) == 0) -rascal_system_t simple_system(); +featomic_system_t simple_system(); mts_array_t empty_array(std::vector shape); #endif diff --git a/rascaline-c-api/tests/utils/mod.rs b/featomic/tests/utils/mod.rs similarity index 100% rename from rascaline-c-api/tests/utils/mod.rs rename to featomic/tests/utils/mod.rs diff --git a/rascaline-c-api/tests/valgrind.supp b/featomic/tests/utils/valgrind.supp similarity index 100% rename from rascaline-c-api/tests/valgrind.supp rename to featomic/tests/utils/valgrind.supp diff --git a/pyproject.toml b/pyproject.toml index ff3326d3e..3e7981c27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,77 +1,16 @@ -[project] -name = "rascaline" -dynamic = ["version", "authors", "optional-dependencies"] -requires-python = ">=3.9" +[tool.ruff.lint] +select = ["E", "F", "B", "I"] +ignore = ["B018", "B904"] -readme = "README.rst" -license = {text = "BSD-3-Clause"} -description = "Computing representations for atomistic machine learning" +[tool.ruff.lint.isort] +lines-after-imports = 2 +known-first-party = ["featomic", "save_data"] +known-third-party = ["torch"] -keywords = ["computational science", "machine learning", "molecular modeling", "atomistic representations"] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: BSD License", - "Operating System :: POSIX", - "Operating System :: MacOS :: MacOS X", - "Operating System :: Microsoft :: Windows", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering :: Bio-Informatics", - "Topic :: Scientific/Engineering :: Chemistry", - "Topic :: Scientific/Engineering :: Physics", - "Topic :: Software Development :: Libraries", - "Topic :: Software Development :: Libraries :: Python Modules", -] - -dependencies = [ - "metatensor-core >=0.1.0,<0.2.0", - "metatensor-operations >=0.3.0,<0.4.0", - "wigners", -] - -[project.urls] -homepage = "https://luthaf.fr/rascaline/latest/" -documentation = "https://luthaf.fr/rascaline/latest/" -repository = "https://github.com/Luthaf/rascaline" -# changelog = "TODO" - -### ======================================================================== ### - -[build-system] -requires = [ - "setuptools >=61", - "wheel >=0.38", - "cmake", -] -build-backend = "setuptools.build_meta" - -[tool.setuptools] -zip-safe = true - -[tool.setuptools.packages.find] -where = ["python/rascaline"] -include = ["rascaline*"] -namespaces = false - -### ======================================================================== ### - -[tool.black] -extend-exclude = """ - /python/rascaline/rascaline/_c_api\\.py -""" - -[tool.isort] -profile = "black" -line_length = 88 -indent = 4 -include_trailing_comma = true -lines_after_imports = 2 -known_first_party = ["rascaline", "save_data"] +[tool.ruff.format] +docstring-code-format = true ### ======================================================================== ### -[tool.pytest.ini_options] -python_files = ["*.py"] -testpaths = ["python/rascaline/tests"] +[tool.uv.pip] +reinstall-package = ["featomic", "featomic-torch"] diff --git a/python/Cargo.toml b/python/Cargo.toml index 51a10bdcf..b406d8912 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "rascaline-python" +name = "featomic-python" version = "0.0.0" edition = "2021" publish = false diff --git a/python/rascaline-torch/AUTHORS b/python/featomic/AUTHORS similarity index 100% rename from python/rascaline-torch/AUTHORS rename to python/featomic/AUTHORS diff --git a/python/rascaline-torch/LICENSE b/python/featomic/LICENSE similarity index 100% rename from python/rascaline-torch/LICENSE rename to python/featomic/LICENSE diff --git a/python/rascaline-torch/MANIFEST.in b/python/featomic/MANIFEST.in similarity index 52% rename from python/rascaline-torch/MANIFEST.in rename to python/featomic/MANIFEST.in index 8fb497af5..da1a568ee 100644 --- a/python/rascaline-torch/MANIFEST.in +++ b/python/featomic/MANIFEST.in @@ -1,10 +1,7 @@ -global-exclude *.pyc -global-exclude .DS_Store +include featomic-cxx-*.tar.gz +include git_version_info +include build-backend/backend.py include pyproject.toml include AUTHORS include LICENSE - -include rascaline-torch.tar.gz - -include build-backend/backend.py diff --git a/python/featomic/README.rst b/python/featomic/README.rst new file mode 120000 index 000000000..c768ff7d9 --- /dev/null +++ b/python/featomic/README.rst @@ -0,0 +1 @@ +../../README.rst \ No newline at end of file diff --git a/python/featomic/build-backend/backend.py b/python/featomic/build-backend/backend.py new file mode 100644 index 000000000..754daab94 --- /dev/null +++ b/python/featomic/build-backend/backend.py @@ -0,0 +1,15 @@ +from setuptools import build_meta + + +get_requires_for_build_sdist = build_meta.get_requires_for_build_sdist +prepare_metadata_for_build_wheel = build_meta.prepare_metadata_for_build_wheel +build_wheel = build_meta.build_wheel +build_sdist = build_meta.build_sdist + + +def get_requires_for_build_wheel(config_settings=None): + defaults = build_meta.get_requires_for_build_wheel(config_settings) + return defaults + [ + "cmake", + "metatensor-core >=0.1.11,<0.2.0", + ] diff --git a/python/featomic/examples/.gitignore b/python/featomic/examples/.gitignore new file mode 100644 index 000000000..f879f70bf --- /dev/null +++ b/python/featomic/examples/.gitignore @@ -0,0 +1 @@ +splined-hypers.json diff --git a/python/rascaline/examples/README.rst b/python/featomic/examples/README.rst similarity index 55% rename from python/rascaline/examples/README.rst rename to python/featomic/examples/README.rst index 46c552fd0..23770e07d 100644 --- a/python/rascaline/examples/README.rst +++ b/python/featomic/examples/README.rst @@ -1,5 +1,5 @@ -Rascaline Python Examples +Featomic Python Examples ========================= This folder consists of introductory examples and examples demonstrating -specific features of rascaline using its Python API. +specific features of featomic using its Python API. diff --git a/python/rascaline/examples/compute-soap.py b/python/featomic/examples/compute-soap.py similarity index 78% rename from python/rascaline/examples/compute-soap.py rename to python/featomic/examples/compute-soap.py index bfd246d2c..b1d98520f 100644 --- a/python/rascaline/examples/compute-soap.py +++ b/python/featomic/examples/compute-soap.py @@ -7,7 +7,7 @@ import chemfiles -from rascaline import SoapPowerSpectrum +from featomic import SoapPowerSpectrum # %% @@ -20,7 +20,7 @@ # %% # -# Rascaline can also handles systems read by `ASE +# Featomic can also handles systems read by `ASE # `_ using # # ``systems = ase.io.read("dataset.xyz", ":")``. @@ -28,16 +28,18 @@ # We can now define hyper parameters for the calculation HYPER_PARAMETERS = { - "cutoff": 5.0, - "max_radial": 6, - "max_angular": 4, - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "radial_basis": { - "Gto": {}, + "cutoff": { + "radius": 5.0, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, }, - "cutoff_function": { - "ShiftedCosine": {"width": 0.5}, + "density": { + "type": "Gaussian", + "width": 0.3, + }, + "basis": { + "type": "TensorProduct", + "max_angular": 4, + "radial": {"type": "Gto", "max_radial": 6}, }, } diff --git a/python/rascaline/examples/dataset.xyz b/python/featomic/examples/dataset.xyz similarity index 100% rename from python/rascaline/examples/dataset.xyz rename to python/featomic/examples/dataset.xyz diff --git a/python/rascaline/examples/first-calculation.py b/python/featomic/examples/first-calculation.py similarity index 86% rename from python/rascaline/examples/first-calculation.py rename to python/featomic/examples/first-calculation.py index d55cbb3a6..759756fb2 100644 --- a/python/rascaline/examples/first-calculation.py +++ b/python/featomic/examples/first-calculation.py @@ -4,7 +4,7 @@ First descriptor computation ============================ -This is an introduction to the rascaline interface using a molecular crystals +This is an introduction to the featomic interface using a molecular crystals dataset using the Python interface. If you are interested in another programming language we recommend you first follow this tutorial and afterward take a look at the how-to guide on :ref:`userdoc-how-to-computing-soap`. @@ -26,13 +26,13 @@ # %% # # We will start by importing all the required packages: the classic numpy; -# chemfiles to load data, and rascaline to compute representations. Afterward +# chemfiles to load data, and featomic to compute representations. Afterward # we will load the dataset using chemfiles. import chemfiles import numpy as np -from rascaline import SphericalExpansion +from featomic import SphericalExpansion with chemfiles.Trajectory("dataset.xyz") as trajectory: @@ -44,7 +44,7 @@ # # We will not explain here how to use chemfiles in detail, as we only use a few # functions. Briefly, :class:`chemfiles.Trajectory` loads structure data in a -# format rascaline can use. If you want to learn more about the possibilities +# format featomic can use. If you want to learn more about the possibilities # take a look at the `chemfiles documentation `_. # # Let us now take a look at the first frame of the dataset. @@ -77,35 +77,44 @@ # spherical expansion as introduced by `Bartók, Kondor, and Csányi # `_. # -# To do so we define below a set of parameters telling rascaline how the +# To do so we define below a set of parameters telling featomic how the # spherical expansion should be calculated. These parameters are also called # hyper parameters since they are parameters of the representation, in # opposition to parameters of machine learning models. Hyper parameters are a # crucial part of calculating descriptors. Poorly selected hyper parameters will # lead to a poor description of your dataset as discussed in the `literature -# `_. The effect of changing some hyper -# parameters is discussed in a :ref:`second tutorial -# `. - -HYPER_PARAMETERS = { - "cutoff": 4.5, - "max_radial": 9, - "max_angular": 6, - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "radial_basis": {"Gto": {"spline_accuracy": 1e-6}}, - "cutoff_function": {"ShiftedCosine": {"width": 0.5}}, - "radial_scaling": {"Willatt2018": {"scale": 2.0, "rate": 1.0, "exponent": 4}}, +# `_. + +cutoff = { + "radius": 4.5, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, +} + +density = { + "type": "Gaussian", + "width": 0.3, + "radial_scaling": { + "type": "Willatt2018", + "scale": 2.0, + "rate": 1.0, + "exponent": 4, + }, +} + +basis = { + "type": "TensorProduct", + "max_angular": 5, + "radial": {"type": "Gto", "max_radial": 8}, } # %% # # After we set the hyper parameters we initialize a -# :class:`rascaline.calculators.SphericalExpansion` object with hyper parameters +# :class:`featomic.calculators.SphericalExpansion` object with hyper parameters # defined above and run the -# :py:func:`rascaline.calculators.CalculatorBase.compute()` method. +# :py:func:`featomic.calculators.CalculatorBase.compute()` method. -calculator = SphericalExpansion(**HYPER_PARAMETERS) +calculator = SphericalExpansion(cutoff=cutoff, density=density, basis=basis) descriptor0 = calculator.compute(frame0) print(type(descriptor0)) @@ -224,7 +233,7 @@ # ``-1.0`` and ``1.0``. Values in this range are reasonable and can be directly # used as input for a machine learning algorithm. # -# Rascaline is also able to process more than one structure within one function +# Featomic is also able to process more than one structure within one function # call. You can process a whole dataset with descriptor_full = calculator.compute(frames) @@ -239,17 +248,17 @@ # have 420 hydrogen atoms in the whole dataset. # # If you want to use another calculator instead of -# :class:`rascaline.calculators.SphericalExpansion` shown here check out the +# :class:`featomic.calculators.SphericalExpansion` shown here check out the # :ref:`userdoc-references` section. # # Computing gradients # ------------------- # -# Additionally, rascaline is also able to calculate gradients on top of the +# Additionally, featomic is also able to calculate gradients on top of the # values. Gradients are useful for constructing an ML potential and running # simulations. For example ``gradients`` of the representation with respect to # atomic positions can be calculated by setting the ``gradients`` parameter of -# the :py:func:`rascaline.calculators.CalculatorBase.compute()` method to +# the :py:func:`featomic.calculators.CalculatorBase.compute()` method to # ``["positions"]``. descriptor_gradients = calculator.compute(frame0, gradients=["positions"]) @@ -273,7 +282,7 @@ # much more compared to features where we only have eight samples. This arises # from the fact that we calculate the position gradient for each pair in the # structure. For our selected block these are all hydrogen-hydrogen pairs. -# Naively one would come up with ``8 * 8 = 64`` samples, but rascaline already +# Naively one would come up with ``8 * 8 = 64`` samples, but featomic already # ignores pairs that are outside of the cutoff radius. Their position gradient # is always zero. The :attr:`metatensor.block.Gradient.samples` attribute shows # this in detail. @@ -308,9 +317,9 @@ # %% # -# Rascaline can also calculate gradients with respect to the strain (i.e. the virial). +# Featomic can also calculate gradients with respect to the strain (i.e. the virial). # For this, you have to add ``"strain"`` to the list parsed to the ``gradients`` -# parameter of the :py:func:`rascaline.calculators.CalculatorBase.compute()` method. +# parameter of the :py:func:`featomic.calculators.CalculatorBase.compute()` method. # Strain gradients/virial are useful when computing the stress and the pressure. # # If you want to know about the effect of changing hypers take a look at the next diff --git a/python/rascaline/examples/keys-selection.py b/python/featomic/examples/keys-selection.py similarity index 88% rename from python/rascaline/examples/keys-selection.py rename to python/featomic/examples/keys-selection.py index d66a350d6..f323d5f97 100644 --- a/python/rascaline/examples/keys-selection.py +++ b/python/featomic/examples/keys-selection.py @@ -9,7 +9,7 @@ import numpy as np from metatensor import Labels, TensorBlock, TensorMap -from rascaline import SoapPowerSpectrum +from featomic import SoapPowerSpectrum # %% @@ -24,16 +24,18 @@ # and define the hyper parameters of the representation HYPER_PARAMETERS = { - "cutoff": 5.0, - "max_radial": 6, - "max_angular": 4, - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "radial_basis": { - "Gto": {}, + "cutoff": { + "radius": 5.0, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, }, - "cutoff_function": { - "ShiftedCosine": {"width": 0.5}, + "density": { + "type": "Gaussian", + "width": 0.3, + }, + "basis": { + "type": "TensorProduct", + "max_angular": 4, + "radial": {"type": "Gto", "max_radial": 6}, }, } @@ -50,7 +52,7 @@ # %% # # We can use these names to define a selection, and only blocks matching the -# labels in this selection will be used by rascaline. Here, only blocks with +# labels in this selection will be used by featomic. Here, only blocks with # keys ``[1,1,1]`` and ``[4,4,4]`` will be calculated. selection = Labels( diff --git a/python/rascaline/examples/long-range-descriptor.py b/python/featomic/examples/long-range-descriptor.py similarity index 52% rename from python/rascaline/examples/long-range-descriptor.py rename to python/featomic/examples/long-range-descriptor.py index 8bac8dae2..a31191746 100644 --- a/python/rascaline/examples/long-range-descriptor.py +++ b/python/featomic/examples/long-range-descriptor.py @@ -18,29 +18,23 @@ from ase.build import molecule from metatensor import LabelsEntry, TensorMap -from rascaline import LodeSphericalExpansion, SphericalExpansion -from rascaline.utils import ( - GaussianDensity, - LodeDensity, - LodeSpliner, - MonomialBasis, - SoapSpliner, -) +import featomic +from featomic import LodeSphericalExpansion, SphericalExpansion # %% # # **Single water molecule (short range) system** # -# Our first test system is a single water molecule with a :math:`15\,\mathrm{Å}` vacuum -# layer around it. +# Our first test system is a single water molecule with a :math:`15\,\mathrm{Å}` +# vacuum layer around it. atoms = molecule("H2O", vacuum=15, pbc=True) # %% -# We choose a ``cutoff`` for the projection of the spherical expansion and the neighbor -# search of the real space spherical expansion. +# We choose a ``cutoff`` for the projection of the spherical expansion and the +# neighbor search of the real space spherical expansion. cutoff = 3 @@ -69,82 +63,50 @@ # %% # -# As you can see, for a single water molecule, the ``cutoff`` includes all atoms of the -# system. The combination of the test system and the ``cutoff`` aims to demonstrate that -# the full atomic fingerprint is contained within the ``cutoff``. By later subtracting -# the short-range density from the LODE density, we will observe that the difference -# between them is almost zero, indicating that a single water molecule is a short-range -# system. -# -# To start this construction we choose a high potential exponent to emulate the rapidly -# decaying LODE density and mimic the polar-polar interactions of water. - - -potential_exponent = 3 - - -# %% -# We now define some typical hyperparameters to compute the spherical expansions. - -max_radial = 5 -max_angular = 1 -atomic_gaussian_width = 1.2 -center_atom_weight = 1.0 - - -# %% -# We choose a relatively low spline accuracy (default is ``1e-8``) to achieve quick -# computation of the spline points. You can increase the spline accuracy if required, -# but be aware that the time to compute these points will increase significantly! - - -spline_accuracy = 1e-2 - - -# %% -# As a projection basis, we don't use the usual :py:class:`GtoBasis -# ` which is commonly used for short range descriptors. -# Instead, we select the :py:class:`MonomialBasis ` which -# is the optimal radial basis for the LODE descriptor as discussed in `Huguenin-Dumittan -# et al. `_ - - -basis = MonomialBasis(cutoff=cutoff) +# As you can see, for a single water molecule, the ``cutoff`` includes all atoms of +# the system. The combination of the test system and the ``cutoff`` aims to +# demonstrate that the full atomic fingerprint is contained within the ``cutoff``. +# By later subtracting the short-range density from the LODE density, we will observe +# that the difference between them is almost zero, indicating that a single water +# molecule is a short-range system. # %% +# # For the density, we choose a smeared power law as used in LODE, which does not decay -# exponentially like a :py:class:`Gaussian density ` -# and is therefore suited to describe long-range interactions between atoms. +# exponentially like a :py:class:`Gaussian ` density and is +# therefore suited to describe long-range interactions between atoms. -density = LodeDensity( - atomic_gaussian_width=atomic_gaussian_width, - potential_exponent=potential_exponent, -) +density = featomic.density.SmearedPowerLaw(smearing=1.2, exponent=3) # %% +# # To visualize this we plot ``density`` together with a Gaussian density -# (``gaussian_density``) with the same ``atomic_gaussian_width`` in a log-log plot. +# (``gaussian_density``) with the same ``width`` in a log-log plot. radial_positions = np.geomspace(1e-5, 10, num=1000) -gaussian_density = GaussianDensity(atomic_gaussian_width=atomic_gaussian_width) +gaussian_density = featomic.density.Gaussian(width=density.smearing) -plt.plot(radial_positions, density.compute(radial_positions), label="LodeDensity") plt.plot( radial_positions, - gaussian_density.compute(radial_positions), - label="GaussianDensity", + density.compute(radial_positions, derivative=False), + label="SmearedPowerLaw", +) +plt.plot( + radial_positions, + gaussian_density.compute(radial_positions, derivative=False), + label="Gaussian", ) positions_indicator = np.array([3.0, 8.0]) plt.plot( positions_indicator, - 2 * positions_indicator**-potential_exponent, + 2 * positions_indicator ** (-density.exponent), c="k", - label=f"p={potential_exponent}", + label=f"exponent={density.exponent}", ) plt.legend() @@ -159,98 +121,89 @@ plt.yscale("log") # %% -# We see that the ``LodeDensity`` decays with a power law of 3, which is the potential -# exponent we picked above, wile the :py:class:`Gaussian density -# ` decays exponentially and is therefore not suited +# +# We see that the ``SmearedPowerLaw`` decays with a power law of 3, which is the +# potential exponent we picked above, wile the :py:class:`Gaussian +# ` density decays exponentially and is therefore not suited # for long-range descriptors. + +# %% # -# We now have all building blocks to construct the spline points for the real and +# As a projection basis, we don't use the usual :py:class:`Gto ` +# which is commonly used for short range descriptors. Instead, we select the +# :py:class:`Monomials ` which is the optimal radial basis +# for the LODE descriptor as discussed in `Huguenin-Dumittan et al. +# `_ + +by_angular = {} +for angular in range(2): + by_angular[angular] = featomic.basis.Monomials( + radius=cutoff, angular_channel=angular, max_radial=4 + ) + + +basis = featomic.basis.Explicit( + by_angular=by_angular, + # We choose a relatively low spline accuracy (default is ``1e-8``) to achieve quick + # computation of the spline points. You can increase the spline accuracy if + # required, but be aware that the time to compute these points will increase + # significantly! + spline_accuracy=1e-2, +) + +# %% +# +# We now have all building blocks to construct the spline for the real and # Fourier space spherical expansions. -real_space_splines = SoapSpliner( - cutoff=cutoff, - max_radial=max_radial, - max_angular=max_angular, +real_space_spliner = featomic.splines.SoapSpliner( + # We don't use a ``smoothing`` function in the cutoff or a ``radial_scaling`` in the + # density to ensure the correct construction of the long-range only descriptor + cutoff=featomic.cutoff.Cutoff(radius=cutoff, smoothing=None), basis=basis, density=density, - accuracy=spline_accuracy, -).compute() - - -# This value gives good convergences for the Fourier space version -k_cutoff = 1.2 * np.pi / atomic_gaussian_width +) +real_space_hypers = real_space_spliner.get_hypers() -fourier_space_splines = LodeSpliner( - k_cutoff=k_cutoff, - max_radial=max_radial, - max_angular=max_angular, +fourier_space_spliner = featomic.splines.LodeSpliner( basis=basis, density=density, - accuracy=spline_accuracy, -).compute() - +) +fourier_space_hypers = fourier_space_spliner.get_hypers() # %% -# .. note:: -# You might want to save the spline points using :py:func:`json.dump` to a file and -# load them with :py:func:`json.load` later without recalculating them. Saving them is -# especially useful if the spline calculations are expensive, i.e., if you increase -# the ``spline_accuracy``. # -# With the spline points ready, we now compute the real space spherical expansion - - -real_space_calculator = SphericalExpansion( - cutoff=cutoff, - max_radial=max_radial, - max_angular=max_angular, - atomic_gaussian_width=atomic_gaussian_width, - radial_basis=real_space_splines, - center_atom_weight=center_atom_weight, - cutoff_function={"Step": {}}, - radial_scaling=None, -) +# With the splines ready, we now compute the two spherical expansions + +real_space_calculator = SphericalExpansion(**real_space_hypers) real_space_expansion = real_space_calculator.compute(atoms) -# %% -# where we don't use a smoothing ``cutoff_function`` or a ``radial_scaling`` to ensure -# the correct construction of the long-range only descriptor. Next, we compute the -# Fourier Space / LODE spherical expansion - - -fourier_space_calculator = LodeSphericalExpansion( - cutoff=cutoff, - max_radial=max_radial, - max_angular=max_angular, - atomic_gaussian_width=atomic_gaussian_width, - center_atom_weight=center_atom_weight, - potential_exponent=potential_exponent, - radial_basis=fourier_space_splines, - k_cutoff=k_cutoff, -) - +fourier_space_calculator = LodeSphericalExpansion(**fourier_space_hypers) fourier_space_expansion = fourier_space_calculator.compute(atoms) # %% -# As described in the beginning, we now subtract the real space LODE contributions from -# Fourier space to obtain a descriptor that only contains the contributions from atoms -# outside of the ``cutoff``. +# As described in the beginning, we now subtract the real space LODE contributions +# from Fourier space to obtain a descriptor that only contains the contributions from +# atoms outside of the ``cutoff``. -subtracted_expansion = fourier_space_expansion - real_space_expansion +delta_expansion = fourier_space_expansion - real_space_expansion -# %% You can now use the ``subtracted_expansion`` as a long-range descriptor in +# %% +# +# You can now use the ``delta_expansion`` as a purely long-range descriptor in # combination with a short-range descriptor like -# :py:class:`rascaline.SphericalExpansion` for your machine learning models. We now -# verify that for our test ``atoms`` the LODE spherical expansion only contains +# :py:class:`featomic.SphericalExpansion` for your machine learning models. +# +# We now verify that for our test ``atoms`` the LODE spherical expansion only contains # short-range contributions. To demonstrate this, we densify the # :py:class:`metatensor.TensorMap` to have only one block per ``"center_type"`` and # visualize our result. Since we have to perform the densify operation several times in -# thi show-to, we define a helper function ``densify_tensormap``. +# this how-to, we define a helper function ``densify_tensormap``. def densify_tensormap(tensor: TensorMap) -> TensorMap: @@ -267,14 +220,14 @@ def densify_tensormap(tensor: TensorMap) -> TensorMap: fourier_space_expansion = densify_tensormap(fourier_space_expansion) -subtracted_expansion = densify_tensormap(subtracted_expansion) +delta_expansion = densify_tensormap(delta_expansion) # %% -# Finally, we plot the values of each block for the Fourier Space spherical expansion in -# the upper panel and the difference between the Fourier Space and the real space in the -# lower panel. And since we will do this plot several times we again define a small plot -# function to help us +# Finally, we plot the values of each block for the Fourier Space spherical expansion +# in the upper panel and the difference between the Fourier Space and the real space +# in the lower panel. And since we will do this plot several times we again define a +# small plot function to help us def plot_value_comparison( @@ -303,14 +256,14 @@ def plot_value_comparison( # We first plot the values of the TensorMaps for center_type=1 (hydrogen) plot_value_comparison( - fourier_space_expansion.keys[0], fourier_space_expansion, subtracted_expansion + fourier_space_expansion.keys[0], fourier_space_expansion, delta_expansion ) # %% # and for center_type=8 (oxygen) plot_value_comparison( - fourier_space_expansion.keys[1], fourier_space_expansion, subtracted_expansion + fourier_space_expansion.keys[1], fourier_space_expansion, delta_expansion ) @@ -325,9 +278,9 @@ def plot_value_comparison( # # **Two water molecule (long range) system** # -# We now add a second water molecule shifted by :math:`3\,\mathrm{Å}` in each direction -# from our first water molecule to show that such a system has non negliable long range -# effects. +# We now add a second water molecule shifted by :math:`3\,\mathrm{Å}` in each +# direction from our first water molecule to show that such a system has non +# negligible long range effects. atoms_shifted = molecule("H2O", vacuum=10, pbc=True) @@ -377,9 +330,9 @@ def plot_value_comparison( fourier_space_expansion_long_range = fourier_space_calculator.compute(atoms_long_range) # %% -# We now firdt verify that the contribution from the short-range descriptors is the same -# as for a single water molecule. Exemplarily, we compare only the first (Hydrogen) -# block of each tensor. +# We now first verify that the contribution from the short-range descriptors is the +# same as for a single water molecule. Exemplarily, we compare only the first +# (Hydrogen) block of each tensor. print("Single water real space spherical expansion") @@ -391,22 +344,22 @@ def plot_value_comparison( # %% # Since the values of the block are the same, we can conclude that there is no # information shared between the two molecules and that the short-range descriptor is -# not able to distinguish the system with only one or two water molecules. Note that the -# different number of `samples` in ``real_space_expansion_long_range`` reflects the fact -# that the second system has more atoms then the first. +# not able to distinguish the system with only one or two water molecules. Note that +# the different number of `samples` in ``real_space_expansion_long_range`` reflects +# the fact that the second system has more atoms then the first. # # As above, we construct a long-range only descriptor and densify the result for # plotting the values. -subtracted_expansion_long_range = ( +delta_expansion_long_range = ( fourier_space_expansion_long_range - real_space_expansion_long_range ) fourier_space_expansion_long_range = densify_tensormap( fourier_space_expansion_long_range ) -subtracted_expansion_long_range = densify_tensormap(subtracted_expansion_long_range) +delta_expansion_long_range = densify_tensormap(delta_expansion_long_range) # %% @@ -417,7 +370,7 @@ def plot_value_comparison( plot_value_comparison( fourier_space_expansion_long_range.keys[0], fourier_space_expansion_long_range, - subtracted_expansion_long_range, + delta_expansion_long_range, ) # %% @@ -426,13 +379,13 @@ def plot_value_comparison( plot_value_comparison( fourier_space_expansion_long_range.keys[1], fourier_space_expansion_long_range, - subtracted_expansion_long_range, + delta_expansion_long_range, ) # %% -# We clearly see that the values of the subtracted spherical are much larger compared to -# the system with only a single water molecule, thus confirming the presence of +# We clearly see that the values of the subtracted spherical are much larger compared +# to the system with only a single water molecule, thus confirming the presence of # long-range contributions in the descriptor for a system with two water molecules. # # .. end-body diff --git a/python/rascaline/examples/profiling.py b/python/featomic/examples/profiling.py similarity index 68% rename from python/rascaline/examples/profiling.py rename to python/featomic/examples/profiling.py index 4ef411047..9253a32cb 100644 --- a/python/rascaline/examples/profiling.py +++ b/python/featomic/examples/profiling.py @@ -7,8 +7,8 @@ import chemfiles -import rascaline -from rascaline import SoapPowerSpectrum +import featomic +from featomic import SoapPowerSpectrum def compute_soap(path): @@ -20,16 +20,18 @@ def compute_soap(path): frames = [f for f in trajectory] HYPER_PARAMETERS = { - "cutoff": 5.0, - "max_radial": 6, - "max_angular": 4, - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "radial_basis": { - "Gto": {}, + "cutoff": { + "radius": 5.0, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, }, - "cutoff_function": { - "ShiftedCosine": {"width": 0.5}, + "density": { + "type": "Gaussian", + "width": 0.3, + }, + "basis": { + "type": "TensorProduct", + "max_angular": 4, + "radial": {"type": "Gto", "max_radial": 6}, }, } @@ -45,7 +47,7 @@ def compute_soap(path): # # Run the calculation with profiling enabled. -with rascaline.Profiler() as profiler: +with featomic.Profiler() as profiler: descriptor = compute_soap("dataset.xyz") # %% # diff --git a/python/rascaline/examples/property-selection.py b/python/featomic/examples/property-selection.py similarity index 90% rename from python/rascaline/examples/property-selection.py rename to python/featomic/examples/property-selection.py index 7160afaa7..e50407f31 100644 --- a/python/rascaline/examples/property-selection.py +++ b/python/featomic/examples/property-selection.py @@ -10,7 +10,7 @@ from metatensor import Labels, MetatensorError, TensorBlock, TensorMap from skmatter.feature_selection import FPS -from rascaline import SoapPowerSpectrum +from featomic import SoapPowerSpectrum # %% @@ -25,16 +25,18 @@ # and define the hyper parameters of the representation HYPER_PARAMETERS = { - "cutoff": 5.0, - "max_radial": 6, - "max_angular": 4, - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "radial_basis": { - "Gto": {}, + "cutoff": { + "radius": 5.0, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, }, - "cutoff_function": { - "ShiftedCosine": {"width": 0.5}, + "density": { + "type": "Gaussian", + "width": 0.3, + }, + "basis": { + "type": "TensorProduct", + "max_angular": 4, + "radial": {"type": "Gto", "max_radial": 6}, }, } @@ -53,7 +55,7 @@ # %% # # We can use a subset of these names to define a selection. In this case, only -# properties matching the labels in this selection will be used by rascaline +# properties matching the labels in this selection will be used by featomic # (here, only properties with ``l = 0`` will be used) selection = Labels( @@ -112,7 +114,7 @@ def fps_feature_selection(descriptor, n_to_select): """ Select ``n_to_select`` features block by block in the ``descriptor``, using Farthest Point Sampling to do the selection; and return a ``TensorMap`` with - the right structure to be used as properties selection with rascaline calculators + the right structure to be used as properties selection with featomic calculators """ blocks = [] for block in descriptor: @@ -146,7 +148,7 @@ def fps_feature_selection(descriptor, n_to_select): # %% # -# and use the selection with rascaline, potentially running the calculation on a +# and use the selection with featomic, potentially running the calculation on a # different set of systems selected_descriptor = calculator.compute(frames, selected_properties=selection) diff --git a/python/rascaline/examples/sample-selection.py b/python/featomic/examples/sample-selection.py similarity index 89% rename from python/rascaline/examples/sample-selection.py rename to python/featomic/examples/sample-selection.py index 471d4370f..6f5226aad 100644 --- a/python/rascaline/examples/sample-selection.py +++ b/python/featomic/examples/sample-selection.py @@ -9,7 +9,7 @@ import numpy as np from metatensor import Labels -from rascaline import SoapPowerSpectrum +from featomic import SoapPowerSpectrum # %% @@ -24,16 +24,18 @@ # and define the hyper parameters of the representation HYPER_PARAMETERS = { - "cutoff": 5.0, - "max_radial": 6, - "max_angular": 4, - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "radial_basis": { - "Gto": {}, + "cutoff": { + "radius": 5.0, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, }, - "cutoff_function": { - "ShiftedCosine": {"width": 0.5}, + "density": { + "type": "Gaussian", + "width": 0.3, + }, + "basis": { + "type": "TensorProduct", + "max_angular": 4, + "radial": {"type": "Gto", "max_radial": 6}, }, } @@ -52,7 +54,7 @@ # %% # # We can use a subset of these names to define a selection. In this case, only -# samples matching the labels in this selection will be used by rascaline (here, +# samples matching the labels in this selection will be used by featomic (here, # only atoms from system 0, 2, and 3) selection = Labels( diff --git a/python/featomic/examples/splined-radial-integral.py b/python/featomic/examples/splined-radial-integral.py new file mode 100644 index 000000000..677577ffa --- /dev/null +++ b/python/featomic/examples/splined-radial-integral.py @@ -0,0 +1,201 @@ +""" +Splined radial integrals +======================== + +.. start-body + +This example illustrates how to generate splines and use custom basis function and +density when computing density-based representations, such as SOAP or LODE. +""" + +# %% + +import json + +import ase.build +import matplotlib.pyplot as plt +import numpy as np +import scipy + +import featomic +from featomic import SphericalExpansion +from featomic.basis import RadialBasis +from featomic.splines import SoapSpliner + + +# %% +# +# For this example, we will define a new custom radial basis for the SOAP spherical +# expansion, based on Chebyshev polynomials of the first kind. This basis will then be +# used in combination with spherical harmonics to expand the density of neighboring +# atoms around a central atom. +# +# In featomic, defining custom radial basis is done by creating a class inheriting from +# :py:class:`featomic.basis.RadialBasis`, and implementing the required method. The +# main one is ``compute_primitive``, which evaluates the radial basis on a set of +# points. This function should also be able to evaluate the derivative of the radial +# basis. If needed :py:meth:`featomic.basis.RadialBasis.finite_differences_derivative` +# can be used to compute the derivative with finite differences. + + +class Chebyshev(RadialBasis): + def __init__(self, max_radial, radius): + # initialize `RadialBasis` + super().__init__(max_radial=max_radial, radius=radius) + + def compute_primitive(self, positions, n, *, derivative=False): + # map argument from [0, cutoff] to [-1, 1] + z = 2 * positions / self.radius - 1 + if derivative: + return -2 * n / self.radius * scipy.special.chebyu(n)(z) + else: + return scipy.special.chebyt(n + 1)(z) + + @property + def integration_radius(self): + return self.radius + + +# %% +# +# We can now look at the basis functions and their derivatives +radius = 4.5 +basis = Chebyshev(max_radial=4, radius=radius) + +r = np.linspace(0, radius) +for n in range(basis.size): + plt.plot(r, basis.compute_primitive(r, n, derivative=False)) + +plt.title("Chebyshev radial basis functions") +plt.show() + +# %% +for n in range(basis.size): + plt.plot(r, basis.compute_primitive(r, n, derivative=True)) +plt.title("Chebyshev radial basis functions' derivatives") +plt.show() + +# %% +# +# Before being used by featomic, the basis functions we implemented will be +# orthogonalized and normalized, to improve conditioning of the produced features. This +# is done automatically, and one can access the orthonormalized basis functions with the +# :py:meth:`featomic.basis.RadialBasis.compute` method. + +basis_orthonormal = basis.compute(r, derivative=False) +for n in range(basis.size): + plt.plot(r, basis_orthonormal[:, n]) + +plt.title("Orthonormalized Chebyshev radial basis functions") +plt.show() + + +# %% +# +# With this, our new radial basis definition is ready to be used with +# :py:class:`featomic.splines.SoapSpliner`. This class will take the whole set of hyper +# parameters, use them to compute a spline of the radial integral, and give us back new +# hypers that can be used with the native calculators to compute the expansion with our +# custom basis. +# + +spliner = SoapSpliner( + cutoff=featomic.cutoff.Cutoff( + radius=radius, + smoothing=featomic.cutoff.ShiftedCosine(width=0.3), + ), + density=featomic.density.Gaussian(width=0.5), + basis=featomic.basis.TensorProduct( + max_angular=4, + radial=Chebyshev(max_radial=4, radius=radius), + spline_accuracy=1e-4, + ), +) + +hypers = spliner.get_hypers() + +# %% +# +# The hyper parameters have been transformed from what we gave to the +# :py:class:`featomic.splines.SoapSpliner`: + +print("hypers['basis'] is", type(hypers["basis"])) +print("hypers['density'] is", type(hypers["density"])) + +# %% +# +# And the new hypers can be used directly with the calculators: + +calculator_splined = SphericalExpansion(**hypers) + +# %% +# +# As a comparison, let's look at the expansion coefficient for formic acid, using both +# our splined radial basis and the classic GTO radial basis: + +atoms = ase.build.molecule("HCOOH", vacuum=4, pbc=True) + +calculator_gto = SphericalExpansion( + # same parameters, only the radial basis changed + cutoff=featomic.cutoff.Cutoff( + radius=radius, + smoothing=featomic.cutoff.ShiftedCosine(width=0.3), + ), + density=featomic.density.Gaussian(width=0.5), + basis=featomic.basis.TensorProduct( + max_angular=4, + radial=featomic.basis.Gto(max_radial=4, radius=radius), + spline_accuracy=1e-4, + ), +) + +expansion_splined = calculator_splined.compute(atoms) +expansion_gto = calculator_gto.compute(atoms) + +# %% +# +# As you can see, the coefficients ends up different, with values assigned to different +# basis functions. In practice, which basis function will be the best will depend on the +# use case and exact dataset, so you should try a couple and check how they performe for +# you! + +selection = dict(o3_lambda=0, center_type=8, neighbor_type=1) + +plt.matshow(expansion_splined.block(selection).values.reshape(2, 5)) +plt.matshow(expansion_gto.block(selection).values.reshape(2, 5)) + + +# %% +# +# Since the calculation of the splines requires computing some integral numerically, the +# creation of the splines might take a while. After an initial calculation, you can save +# the splines data in JSON files; and then reload them later to re-use: + +# convert the hypers from classes to a pure JSON-compatible dictionary +json_hypers = featomic.hypers_to_json(hypers) + +# save the data to a file +with open("splined-hypers.json", "w") as fp: + json.dump(json_hypers, fp) + + +# load the data from the file +with open("splined-hypers.json", "r") as fp: + json_hypers = json.load(fp) + +# the hypers can be used directly with the calculators +calculator = featomic.SphericalExpansion(**json_hypers) + + +# %% +# +# Finally, you can use the same method to define custom +# :py:class:`featomic.basis.ExpansionBasis` and custom +# :py:class:`featomic.density.AtomicDensity`; by creating a new class inheriting from +# the corresponding base class and implementing the corresponding methods. This allow +# you to create a fully custom spherical expansion, and evaluate them efficiently +# through the splines. + +# %% +# +# .. end-body diff --git a/python/featomic/featomic/__init__.py b/python/featomic/featomic/__init__.py new file mode 100644 index 000000000..caeb647d8 --- /dev/null +++ b/python/featomic/featomic/__init__.py @@ -0,0 +1,42 @@ +from . import ( # noqa: F401 + basis, + clebsch_gordan, + cutoff, + density, + splines, + utils, +) +from ._hypers import BadHyperParameters, convert_hypers, hypers_to_json # noqa: F401 +from .calculator_base import CalculatorBase # noqa: F401 + +# don't forget to also update `featomic/torch/__init__.py` and +# `featomic/torch/calculators.py` when modifying this file +from .calculators import ( + AtomicComposition, + LodeSphericalExpansion, + NeighborList, + SoapPowerSpectrum, + SoapRadialSpectrum, + SortedDistances, + SphericalExpansion, + SphericalExpansionByPair, + SphericalExpansionForBonds, +) +from .log import set_logging_callback # noqa: F401 +from .profiling import Profiler # noqa: F401 +from .status import FeatomicError # noqa: F401 +from .systems import IntoSystem, SystemBase # noqa: F401 +from .version import __version__ # noqa: F401 + + +__all__ = [ + "AtomicComposition", + "LodeSphericalExpansion", + "NeighborList", + "SoapPowerSpectrum", + "SoapRadialSpectrum", + "SortedDistances", + "SphericalExpansion", + "SphericalExpansionByPair", + "SphericalExpansionForBonds", +] diff --git a/python/featomic/featomic/_c_api.py b/python/featomic/featomic/_c_api.py new file mode 100644 index 000000000..ed256be33 --- /dev/null +++ b/python/featomic/featomic/_c_api.py @@ -0,0 +1,152 @@ +# fmt: off +# flake8: noqa +'''Automatically-generated file, do not edit!!!''' + +import ctypes +import enum +import platform +from ctypes import CFUNCTYPE, POINTER + +from metatensor._c_api import mts_labels_t, mts_tensormap_t +from numpy.ctypeslib import ndpointer + + +arch = platform.architecture()[0] +if arch == "32bit": + c_uintptr_t = ctypes.c_uint32 +elif arch == "64bit": + c_uintptr_t = ctypes.c_uint64 + +FEATOMIC_SUCCESS = 0 +FEATOMIC_INVALID_PARAMETER_ERROR = 1 +FEATOMIC_JSON_ERROR = 2 +FEATOMIC_UTF8_ERROR = 3 +FEATOMIC_SYSTEM_ERROR = 128 +FEATOMIC_BUFFER_SIZE_ERROR = 254 +FEATOMIC_INTERNAL_ERROR = 255 +FEATOMIC_LOG_LEVEL_ERROR = 1 +FEATOMIC_LOG_LEVEL_WARN = 2 +FEATOMIC_LOG_LEVEL_INFO = 3 +FEATOMIC_LOG_LEVEL_DEBUG = 4 +FEATOMIC_LOG_LEVEL_TRACE = 5 + + +featomic_status_t = ctypes.c_int32 +featomic_logging_callback_t = CFUNCTYPE(None, ctypes.c_int32, ctypes.c_char_p) + + +class featomic_calculator_t(ctypes.Structure): + pass + + +class featomic_pair_t(ctypes.Structure): + _fields_ = [ + ("first", c_uintptr_t), + ("second", c_uintptr_t), + ("distance", ctypes.c_double), + ("vector", ctypes.c_double * 3), + ("cell_shift_indices", ctypes.c_int32 * 3), + ] + + +class featomic_system_t(ctypes.Structure): + _fields_ = [ + ("user_data", ctypes.c_void_p), + ("size", CFUNCTYPE(featomic_status_t, ctypes.c_void_p, POINTER(c_uintptr_t))), + ("types", CFUNCTYPE(featomic_status_t, ctypes.c_void_p, POINTER(ndpointer(ctypes.c_int32, flags='C_CONTIGUOUS')))), + ("positions", CFUNCTYPE(featomic_status_t, ctypes.c_void_p, POINTER(ndpointer(ctypes.c_double, flags='C_CONTIGUOUS')))), + ("cell", CFUNCTYPE(featomic_status_t, ctypes.c_void_p, POINTER(ctypes.c_double))), + ("compute_neighbors", CFUNCTYPE(featomic_status_t, ctypes.c_void_p, ctypes.c_double)), + ("pairs", CFUNCTYPE(featomic_status_t, ctypes.c_void_p, POINTER(ndpointer(featomic_pair_t, flags='C_CONTIGUOUS')), POINTER(c_uintptr_t))), + ("pairs_containing", CFUNCTYPE(featomic_status_t, ctypes.c_void_p, c_uintptr_t, POINTER(ndpointer(featomic_pair_t, flags='C_CONTIGUOUS')), POINTER(c_uintptr_t))), + ] + + +class featomic_labels_selection_t(ctypes.Structure): + _fields_ = [ + ("subset", POINTER(mts_labels_t)), + ("predefined", POINTER(mts_tensormap_t)), + ] + + +class featomic_calculation_options_t(ctypes.Structure): + _fields_ = [ + ("gradients", POINTER(ctypes.c_char_p)), + ("gradients_count", c_uintptr_t), + ("use_native_system", ctypes.c_bool), + ("selected_samples", featomic_labels_selection_t), + ("selected_properties", featomic_labels_selection_t), + ("selected_keys", POINTER(mts_labels_t)), + ] + + +def setup_functions(lib): + from .status import _check_featomic_status_t + + lib.featomic_last_error.argtypes = [ + + ] + lib.featomic_last_error.restype = ctypes.c_char_p + + lib.featomic_set_logging_callback.argtypes = [ + featomic_logging_callback_t + ] + lib.featomic_set_logging_callback.restype = _check_featomic_status_t + + lib.featomic_calculator.argtypes = [ + ctypes.c_char_p, + ctypes.c_char_p + ] + lib.featomic_calculator.restype = POINTER(featomic_calculator_t) + + lib.featomic_calculator_free.argtypes = [ + POINTER(featomic_calculator_t) + ] + lib.featomic_calculator_free.restype = _check_featomic_status_t + + lib.featomic_calculator_name.argtypes = [ + POINTER(featomic_calculator_t), + ctypes.c_char_p, + c_uintptr_t + ] + lib.featomic_calculator_name.restype = _check_featomic_status_t + + lib.featomic_calculator_parameters.argtypes = [ + POINTER(featomic_calculator_t), + ctypes.c_char_p, + c_uintptr_t + ] + lib.featomic_calculator_parameters.restype = _check_featomic_status_t + + lib.featomic_calculator_cutoffs.argtypes = [ + POINTER(featomic_calculator_t), + POINTER(POINTER(ctypes.c_double)), + POINTER(c_uintptr_t) + ] + lib.featomic_calculator_cutoffs.restype = _check_featomic_status_t + + lib.featomic_calculator_compute.argtypes = [ + POINTER(featomic_calculator_t), + POINTER(POINTER(mts_tensormap_t)), + POINTER(featomic_system_t), + c_uintptr_t, + featomic_calculation_options_t + ] + lib.featomic_calculator_compute.restype = _check_featomic_status_t + + lib.featomic_profiling_clear.argtypes = [ + + ] + lib.featomic_profiling_clear.restype = _check_featomic_status_t + + lib.featomic_profiling_enable.argtypes = [ + ctypes.c_bool + ] + lib.featomic_profiling_enable.restype = _check_featomic_status_t + + lib.featomic_profiling_get.argtypes = [ + ctypes.c_char_p, + ctypes.c_char_p, + c_uintptr_t + ] + lib.featomic_profiling_get.restype = _check_featomic_status_t diff --git a/python/rascaline/rascaline/_c_lib.py b/python/featomic/featomic/_c_lib.py similarity index 76% rename from python/rascaline/rascaline/_c_lib.py rename to python/featomic/featomic/_c_lib.py index f51269c19..8620e90ff 100644 --- a/python/rascaline/rascaline/_c_lib.py +++ b/python/featomic/featomic/_c_lib.py @@ -11,14 +11,14 @@ _HERE = os.path.realpath(os.path.dirname(__file__)) -class RascalFinder(object): +class FeatomicFinder(object): def __init__(self): self._cached_dll = None def __call__(self): if self._cached_dll is None: # Load metatensor shared library in the process first, to ensure - # the rascaline shared library can find it + # the featomic shared library can find it metatensor._c_lib._get_library() path = _lib_path() @@ -32,13 +32,13 @@ def __call__(self): def _lib_path(): if sys.platform.startswith("darwin"): windows = False - path = os.path.join(_HERE, "lib", "librascaline.dylib") + path = os.path.join(_HERE, "lib", "libfeatomic.dylib") elif sys.platform.startswith("linux"): windows = False - path = os.path.join(_HERE, "lib", "librascaline.so") + path = os.path.join(_HERE, "lib", "libfeatomic.so") elif sys.platform.startswith("win"): windows = True - path = os.path.join(_HERE, "bin", "rascaline.dll") + path = os.path.join(_HERE, "bin", "featomic.dll") else: raise ImportError("Unknown platform. Please edit this file") @@ -47,7 +47,7 @@ def _lib_path(): _check_dll(path) return path - raise ImportError("Could not find rascaline shared library at " + path) + raise ImportError("Could not find featomic shared library at " + path) def _check_dll(path): @@ -75,18 +75,18 @@ def _check_dll(path): python_machine = platform.machine() if python_machine == "x86": if machine != IMAGE_FILE_MACHINE_I386: - raise ImportError("Python is 32-bit x86, but rascaline.dll is not") + raise ImportError("Python is 32-bit x86, but featomic.dll is not") elif python_machine == "AMD64": if machine != IMAGE_FILE_MACHINE_AMD64: - raise ImportError("Python is 64-bit x86_64, but rascaline.dll is not") + raise ImportError("Python is 64-bit x86_64, but featomic.dll is not") elif python_machine == "ARM64": if machine != IMAGE_FILE_MACHINE_ARM64: - raise ImportError("Python is 64-bit ARM, but rascaline.dll is not") + raise ImportError("Python is 64-bit ARM, but featomic.dll is not") else: raise ImportError( - f"Rascaline doesn't provide a version for {python_machine} CPU. " + f"Featomic doesn't provide a version for {python_machine} CPU. " "If you are compiling from source on a new architecture, edit this file" ) -_get_library = RascalFinder() +_get_library = FeatomicFinder() diff --git a/python/featomic/featomic/_hypers/__init__.py b/python/featomic/featomic/_hypers/__init__.py new file mode 100644 index 000000000..2ce51e920 --- /dev/null +++ b/python/featomic/featomic/_hypers/__init__.py @@ -0,0 +1,69 @@ +import json +import re +from typing import Any, Dict + +from . import _rascaline + + +class BadHyperParameters(Exception): + pass + + +def convert_hypers(origin, representation=None, hypers=None): + """Convert hyper-parameters from other software into the format used by featomic. + + :param origin: which software do the hyper-parameters come from? Valid values are: + + - ``"rascaline"`` for old rascaline format; + :param representation: which representation are these hyper for? The meaning depend + on the ``origin``: + + - for ``origin="rascaline"``, this is the name of the calculator class; + :param hypers: the hyper parameter to convert. The type depend on the ``origin``: + + - for ``origin="rascaline"``, this should be a dictionary; + + :return: A string containing the code corresponding to the requested representation + and hypers + """ + if origin == "rascaline": + if representation in [ + "SphericalExpansion", + "SphericalExpansionByPair", + "SoapPowerSpectrum", + ]: + hypers = _rascaline.convert_soap(hypers) + elif representation == "SoapRadialSpectrum": + hypers = _rascaline.convert_radial_spectrum(hypers) + elif representation == "LodeSphericalExpansion": + hypers = _rascaline.convert_lode(hypers) + else: + raise ValueError( + "no hyper conversion exists for rascaline representation " + f"'{representation}'" + ) + + hypers_dict = json.dumps(hypers, indent=4) + hypers_dict = re.sub(r"\bnull\b", "None", hypers_dict) + return f"{representation}(**{hypers_dict})" + else: + raise ValueError(f"no hyper conversion exists for {origin} software") + + +def hypers_to_json(hypers_dict: Dict[str, Any]): + """ + Convert from class version of featomic hyper-parameters to the JSON version. + + The class version would contain something like ``{"cutoff": Cutoff(radius=3.4)}``, + which this function transforms into ``{"cutoff": {"radius": 3.4", "smoothing": + {"type": "Step"}}}``. + """ + json = {} + for key, value in hypers_dict.items(): + if hasattr(value, "_featomic_hypers"): + value = value._featomic_hypers() + + if isinstance(value, dict): + value = hypers_to_json(value) + json[key] = value + return json diff --git a/python/featomic/featomic/_hypers/_rascaline.py b/python/featomic/featomic/_hypers/_rascaline.py new file mode 100644 index 000000000..aaf98b54d --- /dev/null +++ b/python/featomic/featomic/_hypers/_rascaline.py @@ -0,0 +1,166 @@ +def convert_soap(hypers): + """convert from old style rascaline hypers for SOAP-related representations""" + cleaned = { + "cutoff": _process_cutoff(hypers), + "density": _process_density(hypers), + } + + max_angular = _get_or_error(hypers, "max_angular", "") + radial, spline_accuracy = _process_radial_basis(hypers) + cleaned["basis"] = { + "type": "TensorProduct", + "max_angular": max_angular, + "radial": radial, + } + if spline_accuracy is not None: + if isinstance(spline_accuracy, float): + cleaned["basis"]["spline_accuracy"] = spline_accuracy + else: + cleaned["basis"]["spline_accuracy"] = None + + return cleaned + + +def convert_radial_spectrum(hypers): + """convert from old style rascaline hypers for SOAP radial spectrum""" + cleaned = { + "cutoff": _process_cutoff(hypers), + "density": _process_density(hypers), + } + + radial, spline_accuracy = _process_radial_basis(hypers) + cleaned["basis"] = {"radial": radial} + if spline_accuracy is not None: + if isinstance(spline_accuracy, float): + cleaned["basis"]["spline_accuracy"] = spline_accuracy + else: + cleaned["basis"]["spline_accuracy"] = None + + return cleaned + + +def convert_lode(hypers): + """convert from old style rascaline hypers for LODE spherical expansion""" + + cleaned = { + "density": _process_density(hypers), + } + + max_angular = _get_or_error(hypers, "max_angular", "") + radial, spline_accuracy = _process_radial_basis(hypers, lode=True) + cleaned["basis"] = { + "type": "TensorProduct", + "max_angular": max_angular, + "radial": radial, + } + if spline_accuracy is not None: + if isinstance(spline_accuracy, float): + cleaned["basis"]["spline_accuracy"] = spline_accuracy + else: + cleaned["basis"]["spline_accuracy"] = None + + k_cutoff = hypers.get("k_cutoff") + if k_cutoff is not None: + cleaned["k_cutoff"] = k_cutoff + + return cleaned + + +def _process_cutoff(hypers): + cutoff = { + "radius": _get_or_error(hypers, "cutoff", ""), + } + + cutoff_fn = _get_or_error(hypers, "cutoff_function", "") + if "Step" in cutoff_fn: + cutoff["smoothing"] = {"type": "Step"} + if "ShiftedCosine" in cutoff_fn: + width = _get_or_error( + cutoff_fn["ShiftedCosine"], "width", "cutoff_function.ShiftedCosine" + ) + cutoff["smoothing"] = {"type": "ShiftedCosine", "width": width} + + return cutoff + + +def _process_density(hypers): + gaussian_width = _get_or_error(hypers, "atomic_gaussian_width", "") + center_weight = _get_or_error(hypers, "center_atom_weight", "") + exponent = hypers.get("potential_exponent") + + if exponent is None: + density = { + "type": "Gaussian", + "width": gaussian_width, + } + else: + density = { + "type": "SmearedPowerLaw", + "smearing": gaussian_width, + "exponent": exponent, + } + + if center_weight != 1.0: + density["center_atom_weight"] = center_weight + + if "radial_scaling" in hypers: + radial_scaling = hypers["radial_scaling"] + if radial_scaling is None: + pass + + if "None" in radial_scaling: + pass + + if "Willatt2018" in radial_scaling: + exponent = _get_or_error( + radial_scaling["Willatt2018"], "exponent", "radial_scaling.Willatt2018" + ) + rate = _get_or_error( + radial_scaling["Willatt2018"], "rate", "radial_scaling.Willatt2018" + ) + scale = _get_or_error( + radial_scaling["Willatt2018"], "scale", "radial_scaling.Willatt2018" + ) + + density["scaling"] = { + "type": "Willatt2018", + "exponent": exponent, + "rate": rate, + "scale": scale, + } + + return density + + +def _process_radial_basis(hypers, lode=False): + spline_accuracy = None + max_radial = _get_or_error(hypers, "max_radial", "") - 1 + radial_basis = _get_or_error(hypers, "radial_basis", "") + + if "Gto" in radial_basis: + radial = {"type": "Gto", "max_radial": max_radial} + + if lode: + cutoff = _get_or_error(hypers, "cutoff", "") - 1 + radial["radius"] = cutoff + + gto_basis = radial_basis["Gto"] + do_splines = gto_basis.get("splined_radial_integral", True) + if do_splines: + spline_accuracy = gto_basis.get("spline_accuracy") + else: + spline_accuracy = False + + elif "TabulatedRadialIntegral" in radial_basis: + raise NotImplementedError("TabulatedRadialIntegral radial basis") + + return radial, spline_accuracy + + +def _get_or_error(hypers, name, path): + from . import BadHyperParameters + + if name not in hypers: + raise BadHyperParameters(f"missing {name} at {path} in hypers") + + return hypers.pop(name) diff --git a/python/featomic/featomic/basis.py b/python/featomic/featomic/basis.py new file mode 100644 index 000000000..36638b9be --- /dev/null +++ b/python/featomic/featomic/basis.py @@ -0,0 +1,621 @@ +import abc +from typing import Dict, List, Optional + +import numpy as np + + +try: + import scipy.integrate + import scipy.optimize + import scipy.special + + HAS_SCIPY = True +except ImportError: + HAS_SCIPY = False + + +class RadialBasis(metaclass=abc.ABCMeta): + """ + Base class representing a set of radial basis functions, indexed by a radial index + ``n``. + + You can inherit from this class to define new custom radial basis function, by + implementing the :py:meth:`compute_primitive` method. If needed, + :py:meth:`finite_differences_derivative` can be used to compute the derivatives of a + radial basis. + + Overriding :py:attr:`integration_radius` can be useful to control the integration + radius when evaluating the radial integral numerically. See this :ref:`explanation + ` for more information. + + If the new radial basis function has corresponding hyper parameters in the native + calculators, you should also implement :py:meth:`get_hypers`. + """ + + def __init__(self, *, max_radial: int, radius: float): + """ + :parameter max_radial: maximal radial basis index to include (there will be + ``N = max_radial + 1`` basis functions overall) + :parameter radius: radius of the radial basis. For local spherical expansions, + this is typically the same as the spherical cutoff radius. + """ + self.max_radial = int(max_radial) + self.radius = float(radius) + + assert self.max_radial >= 0 + assert self.radius > 0.0 + self._orthonormalization_matrix = None + + def _featomic_hypers(self): + return self.get_hypers() + + def get_hypers(self): + """ + Return the native hyper parameters corresponding to this set of basis functions + """ + raise NotImplementedError( + f"This radial basis function ({self.__class__.__name__}) does not have " + "matching hyper parameters in the native calculators. It should be used " + "through one of the spliner class instead of directly." + ) + + @property + def size(self) -> int: + """Get the size of the basis set (i.e. the total number of basis functions)""" + return self.max_radial + 1 + + @property + def integration_radius(self) -> float: + """ + Get the radius to use for numerical evaluation of radial integrals. + + This default to the ``radius`` given as a class parameter, but can be overridden + by child classes as needed. + """ + return self.radius + + @abc.abstractmethod + def compute_primitive( + self, positions: np.ndarray, n: int, *, derivative: bool + ) -> np.ndarray: + """ + Evaluate the primitive (not normalized not orthogonalized) radial basis for + index ``n`` on grid points at the given ``positions``. + + :param n: index of the radial basis to evaluate + :param positions: positions of the grid points where the basis should be + evaluated + :param derivative: should this function return the values of the radial basis or + its derivatives. + :return: the values (or derivative) of radial basis on the grid points + """ + + def compute(self, positions: np.ndarray, *, derivative: bool) -> np.ndarray: + """ + Evaluate the orthogonalized and normalized radial basis on grid points at the + given ``positions``. The returned array contains all the radial basis, from + ``n=0`` to ``n = max_radial``. + + :param positions: positions of the grid points where the basis should be + evaluated + :param derivative: should this function return the values of the radial basis or + its derivatives. + :return: the values (or derivative) of radial basis on the grid points + """ + if self._orthonormalization_matrix is None: + self._orthonormalization_matrix = self._get_orthonormalization_matrix() + + basis = np.vstack( + [ + self.compute_primitive(positions, n, derivative=derivative) + for n in range(self.size) + ] + ) + return (self._orthonormalization_matrix @ basis).T + + def finite_differences_derivative( + self, + positions: np.ndarray, + n: int, + *, + displacement=1e-6, + ) -> np.ndarray: + """ + Helper function to compute derivate of the radial function using finite + differences. This can be used by child classes to implement the + ``derivative=True`` branch of :py:meth:`compute` function. + """ + value_pos = self.compute_primitive( + positions + displacement / 2, n, derivative=False + ) + value_neg = self.compute_primitive( + positions - displacement / 2, n, derivative=False + ) + + return (value_pos - value_neg) / displacement + + def _get_orthonormalization_matrix(self) -> np.ndarray: + """ + Compute the ``(self.size, self.size)`` orthonormalization matrix for this radial + basis using numerical integration. + """ + if not HAS_SCIPY: + raise ValueError("Orthonormalization requires scipy!") + + gram_matrix = self._get_gram_matrix() + + # Get the normalization constants from the diagonal entries + normalizations = np.zeros(self.size) + + for n in range(self.size): + normalizations[n] = 1 / np.sqrt(gram_matrix[n, n]) + # Rescale gram matrix to be defined in terms of the normalized + # (but not yet orthonormalized) basis functions + gram_matrix[n, :] *= normalizations[n] + gram_matrix[:, n] *= normalizations[n] + + eigvals, eigvecs = np.linalg.eigh(gram_matrix) + if np.any(eigvals < 1e-12): + raise ValueError( + "Unable to orthonormalize the radial basis, gram matrix is singular. " + "You can try decreasing the number of radial basis function, or " + "changing some of the basis function parameters" + ) + + orthonormalization_matrix = ( + eigvecs @ np.diag(np.sqrt(1.0 / eigvals)) @ eigvecs.T + ) + + # Rescale the orthonormalization matrix so that it + # works with respect to the primitive (i.e. not normalized) + # radial basis functions + for n in range(self.size): + orthonormalization_matrix[:, n] *= normalizations[n] + + return orthonormalization_matrix + + def _get_gram_matrix(self) -> np.ndarray: + """compute the Gram matrix of the current basis.""" + # Gram matrix (also called overlap matrix or inner product matrix) + gram_matrix = np.zeros((self.size, self.size)) + + def integrand( + positions: np.ndarray, + n1: int, + n2: int, + ) -> np.ndarray: + r1 = self.compute_primitive(positions, n1, derivative=False) + r2 = self.compute_primitive(positions, n2, derivative=False) + return positions**2 * r1 * r2 + + for n1 in range(self.size): + for n2 in range(self.size): + gram_matrix[n1, n2] = scipy.integrate.quad( + func=integrand, + a=0, + b=self.integration_radius, + args=(n1, n2), + )[0] + + return gram_matrix + + +class Gto(RadialBasis): + r""" + Gaussian Type Orbital (GTO) radial basis. + + It is defined as + + .. math:: + + R_n(r) = r^n e^{-\frac{r^2}{2\sigma_n^2}}, + + where :math:`\sigma_n = \sqrt{n} r_0 / N` with :math:`r_0` being the basis + ``radius`` and :math:`N` the number of radial basis functions. + """ + + def __init__(self, *, max_radial: int, radius: Optional[float] = None): + """ + :parameter max_radial: maximal radial basis index to include (there will be + ``N = max_radial + 1`` basis functions overall) + :parameter radius: radius of the GTO basis functions. This is only required for + LODE spherical expansion or splining the radial integral. + """ + if radius is None: + super().__init__(max_radial=max_radial, radius=float("inf")) + else: + super().__init__(max_radial=max_radial, radius=radius) + + self._gto_sigmas = np.ones(self.size, dtype=np.float64) + + if radius is not None: + self._gto_radius = float(radius) + for n in range(1, self.size): + self._gto_sigmas[n] = np.sqrt(n) + self._gto_sigmas *= self._gto_radius / self.size + else: + self._gto_radius = None + + def get_hypers(self): + hypers = { + "type": "Gto", + "max_radial": self.max_radial, + } + + if self._gto_radius is not None: + hypers["radius"] = self._gto_radius + + return hypers + + @property + def integration_radius(self) -> float: + if self._gto_radius is None: + raise ValueError( + "`radius` needs to be specified for numerical evaluation " + "of the GTO radial basis" + ) + + # We would ideally infinity as the ``integration_radius``, but this leads to + # problems when calculating the radial integral with `quad`. So we pick + # something large enough instead + return 5 * self._gto_radius + + def compute_primitive( + self, positions: np.ndarray, n: int, *, derivative: bool + ) -> np.ndarray: + if self._gto_radius is None: + raise ValueError( + "`radius` needs to be specified for numerical evaluation " + "of the GTO radial basis" + ) + + values = positions**n * np.exp(-0.5 * (positions / self._gto_sigmas[n]) ** 2) + if derivative: + return ( + n / positions * values - positions / self._gto_sigmas[n] ** 2 * values + ) + + return values + + +class Monomials(RadialBasis): + r""" + Monomial radial basis, consisting of functions: + + .. math:: + R_{nl}(r) = r^{l+2n} + + These capture precisely the radial dependence if we compute the Taylor expansion of + a generic function defined in 3D space. + + :parameter angular_channel: index of the angular channel associated with this radial + basis, i.e. :math:`l` in the equation above. + """ + + def __init__(self, *, angular_channel: int, max_radial: int, radius: float): + super().__init__(max_radial=max_radial, radius=radius) + self.angular_channel = int(angular_channel) + assert self.angular_channel >= 0 + + def compute_primitive( + self, positions: np.ndarray, n: int, *, derivative: bool + ) -> np.ndarray: + ell = self.angular_channel + if derivative: + return (ell + 2 * n) * positions ** (ell + 2 * n - 1) + else: + return positions ** (ell + 2 * n) + + +class SphericalBessel(RadialBasis): + r"""Spherical Bessel functions as a radial basis. + + This is used among others in the `Laplacian eigenstate + `_ basis. The basis functions have the following + form: + + .. math:: + + R_{ln}(r) = j_l \left( \frac{r}{r_0} \text{zero}(J_l, n) \right) + + where :math:`j_l` is the spherical bessel function of the first kind of order + :math:`l`, :math:`r_0` is the basis function ``radius``, and :math:`\text{zero}(J_l, + n)` is the :math:`n`-th zero of (non-spherical) bessel function of first kind and + order :math:`l`. + + :parameter angular_channel: index of the angular channel associated with this radial + basis, i.e. :math:`l` in the equation above. + """ + + def __init__(self, *, angular_channel: int, max_radial: int, radius: float): + super().__init__(max_radial=max_radial, radius=radius) + self.angular_channel = int(angular_channel) + assert self.angular_channel >= 0 + + # this is computing all roots for all `l` up to `angular_channel` to then throw + # away most of them. Maybe there is a better way to do this + self._roots = SphericalBessel._compute_zeros(angular_channel + 1, self.size) + + def compute_primitive( + self, positions: np.ndarray, n: int, *, derivative: bool + ) -> np.ndarray: + ell = self.angular_channel + values = scipy.special.spherical_jn( + ell, positions * self._roots[ell, n] / self.radius, derivative=derivative + ) + if derivative: + values *= self._roots[ell, n] / self.radius + + return values + + @staticmethod + def _compute_zeros(angular_size: int, radial_size: int) -> np.ndarray: + """Zeros of spherical bessel functions. + + Code is taken from the `Scipy Cookbook `_ + + .. _spc: https://scipy-cookbook.readthedocs.io/items/SphericalBesselZeros.html + + :parameter angular_size: number of angular components + :parameter radial_size: number of radial components + :returns: computed zeros of the spherical bessel functions + """ + + def Jn(r: float, ell: int) -> float: + return np.sqrt(np.pi / (2 * r)) * scipy.special.jv(ell + 0.5, r) + + def Jn_zeros(angular_size: int, radial_size: int) -> np.ndarray: + zeros_j = np.zeros((angular_size, radial_size), dtype=np.float64) + zeros_j[0] = np.arange(1, radial_size + 1) * np.pi + points = np.arange(1, radial_size + angular_size + 1) * np.pi + roots = np.zeros(radial_size + angular_size, dtype=np.float64) + for ell in range(1, angular_size): + for j in range(radial_size + angular_size - ell): + roots[j] = scipy.optimize.brentq( + Jn, points[j], points[j + 1], (ell,) + ) + points = roots + zeros_j[ell][:radial_size] = roots[:radial_size] + return zeros_j + + return Jn_zeros(angular_size, radial_size) + + +######################################################################################## +######################################################################################## + + +class ExpansionBasis(metaclass=abc.ABCMeta): + """ + Base class representing a set of basis functions used by spherical expansions. + + A full basis typically uses both a set of radial basis functions, and angular basis + functions; combined in various ways. The angular basis functions are almost always + spherical harmonics, while the radial basis function can be freely picked. + + You can inherit from this class to define new sets of basis functions, implementing + :py:meth:`get_hypers` to create the right hyper parameters for the underlying native + calculator, as well as :py:meth:`angular_channels` and :py:meth:`radial_basis` to + define the set of basis functions to use. + """ + + def _featomic_hypers(self): + return self.get_hypers() + + def get_hypers(self): + """ + Return the native hyper parameters corresponding to this set of basis functions + """ + raise NotImplementedError( + f"This basis functions set ({self.__class__.__name__}) does not have " + "matching hyper parameters in the native calculators. It should be used " + "through one of the spliner class instead of directly." + ) + + @abc.abstractmethod + def angular_channels(self) -> List[int]: + """Get the list of angular channels that are included in the expansion basis.""" + + @abc.abstractmethod + def radial_basis(self, angular: int) -> RadialBasis: + """Get the radial basis used for a given ``angular`` channel""" + + +class TensorProduct(ExpansionBasis): + r""" + Basis function set combining spherical harmonics with a radial basis functions set, + taking all possible combinations of radial and angular basis function. + + Using ``N`` radial basis functions and ``L`` angular basis functions, this will + create ``N x L`` basis functions (:math:`B_{nlm}`) for the overall expansion: + + .. math:: + + B_{nlm}(\boldsymbol{r}) = R_{nl}(r)Y_{lm}(\hat{r}) \, + """ + + def __init__( + self, + *, + max_angular: int, + radial: RadialBasis, + spline_accuracy: Optional[float] = 1e-8, + ): + """ + :param max_angular: Largest angular channel to include in the basis + :param radial: radial basis to use for all angular channels + :param spline_accuracy: requested accuracy of the splined radial integrals, + defaults to 1e-8 + """ + self.max_angular = int(max_angular) + self.radial = radial + + if spline_accuracy is None: + self.spline_accuracy = None + else: + self.spline_accuracy = float(spline_accuracy) + assert self.spline_accuracy > 0 + + assert self.max_angular >= 0 + assert isinstance(self.radial, RadialBasis) + + def get_hypers(self): + return { + "type": "TensorProduct", + "max_angular": self.max_angular, + "radial": self.radial, + "spline_accuracy": self.spline_accuracy, + } + + def angular_channels(self) -> List[int]: + return list(range(self.max_angular + 1)) + + def radial_basis(self, angular: int) -> RadialBasis: + return self.radial + + +class Explicit(ExpansionBasis): + """ + An expansion basis where combinations of radial and angular functions is picked + explicitly. + + The angular basis functions are still spherical harmonics, but only the degrees + included as keys in ``by_angular`` will be part of the output. Each of these angular + basis function can then be associated with a set of different radial basis function, + potentially of different sizes. + """ + + def __init__( + self, + *, + by_angular: Dict[int, RadialBasis], + spline_accuracy: Optional[float] = 1e-8, + ): + """ + :param by_angular: definition of the radial basis for each angular channel to + include. + :param spline_accuracy: requested accuracy of the splined radial integrals, + defaults to 1e-8 + """ + self.by_angular = by_angular + + if spline_accuracy is None: + self.spline_accuracy = None + else: + self.spline_accuracy = float(spline_accuracy) + assert self.spline_accuracy > 0 + + for angular, radial in self.by_angular.items(): + assert angular >= 0 + assert isinstance(radial, RadialBasis) + + def get_hypers(self): + return { + "type": "Explicit", + "by_angular": self.by_angular, + "spline_accuracy": self.spline_accuracy, + } + + def angular_channels(self) -> List[int]: + return list(self.by_angular.keys()) + + def radial_basis(self, angular: int) -> RadialBasis: + return self.by_angular[angular] + + +class LaplacianEigenstate(ExpansionBasis): + """ + The Laplacian eigenstate basis, introduced in https://doi.org/10.1063/5.0124363, is + a set of basis functions for spherical expansion which is both *smooth* (in the same + sense as the smoothness of a low-pass-truncated Fourier expansion) and *ragged*, + using a different number of radial function for each angular channel. This is + intended to obtain a more balanced smoothness level in the radial and angular + direction for a given total number of basis functions. + + This expansion basis is not directly implemented in the native calculators, but is + intended to be used with the :py:class:`featomic.splines.SoapSpliner` to create + splines of the radial integrals. + """ + + def __init__( + self, + *, + radius: float, + max_radial: int, + max_angular: Optional[int] = None, + spline_accuracy: Optional[float] = 1e-8, + ): + """ + :param radius: radius of the basis functions + :param max_radial: number of radial basis function for the ``L=0`` angular + channel. All other angular channels will have fewer radial basis functions. + :param max_angular: Truncate the set of radial functions at this angular + channel. If ``None``, this will be set to a high enough value to include all + basis functions with an Laplacian eigenvalue below the one for ``l=0, + n=max_radial``. + :param spline_accuracy: requested accuracy of the splined radial integrals, + defaults to 1e-8 + """ + self.radius = float(radius) + assert self.radius >= 0 + + self.max_radial = int(max_radial) + assert self.max_radial >= 0 + + if max_angular is None: + self.max_angular = self.max_radial + else: + self.max_angular = int(max_angular) + assert self.max_angular >= 0 + + if spline_accuracy is None: + self.spline_accuracy = None + else: + self.spline_accuracy = float(spline_accuracy) + assert self.spline_accuracy > 0 + + # compute the zeros of the spherical Bessel functions + zeros_ln = SphericalBessel._compute_zeros( + self.max_angular + 1, self.max_radial + 1 + ) + + # determine the eigenvalue cutoff + eigenvalues = zeros_ln**2 / self.radius**2 + max_eigenvalue = eigenvalues[0, max_radial] + + # find the actual `max_angular` if the user did not specify one by repeatedly + # increasing the size of `eigenvalues`, until we find an angular channel where + # all eigenvalues are above the cutoff. + if max_angular is None: + while eigenvalues[-1, 0] < max_eigenvalue: + self.max_angular += self.max_radial + zeros_ln = SphericalBessel._compute_zeros( + self.max_angular + 1, self.max_radial + 1 + ) + eigenvalues = zeros_ln**2 / self.radius**2 + + self.max_angular = len(np.where(eigenvalues[:, 0] <= max_eigenvalue)[0]) - 1 + assert self.max_angular >= 0 + + by_angular = {} + for angular in range(self.max_angular + 1): + max_radial = len(np.where(eigenvalues[angular, :] <= max_eigenvalue)[0]) - 1 + by_angular[angular] = SphericalBessel( + angular_channel=angular, + max_radial=max_radial, + radius=self.radius, + ) + + self._explicit = Explicit( + by_angular=by_angular, + spline_accuracy=self.spline_accuracy, + ) + + def get_hypers(self): + return self._explicit.get_hypers() + + def angular_channels(self) -> List[int]: + return self._explicit.angular_channels() + + def radial_basis(self, angular: int) -> RadialBasis: + return self._explicit.radial_basis(angular) diff --git a/python/rascaline/rascaline/calculator_base.py b/python/featomic/featomic/calculator_base.py similarity index 90% rename from python/rascaline/rascaline/calculator_base.py rename to python/featomic/featomic/calculator_base.py index c9509aaae..b07b0a8f0 100644 --- a/python/rascaline/rascaline/calculator_base.py +++ b/python/featomic/featomic/calculator_base.py @@ -5,12 +5,12 @@ from metatensor._c_api import c_uintptr_t, mts_tensormap_t from ._c_api import ( - RASCAL_BUFFER_SIZE_ERROR, - rascal_calculation_options_t, - rascal_system_t, + FEATOMIC_BUFFER_SIZE_ERROR, + featomic_calculation_options_t, + featomic_system_t, ) from ._c_lib import _get_library -from .status import RascalError, _check_rascal_pointer +from .status import FeatomicError, _check_featomic_pointer from .systems import IntoSystem, wrap_system @@ -22,8 +22,8 @@ def _call_with_growing_buffer(callback, initial=1024): try: callback(buffer, bufflen) break - except RascalError as e: - if e.status == RASCAL_BUFFER_SIZE_ERROR: + except FeatomicError as e: + if e.status == FEATOMIC_BUFFER_SIZE_ERROR: # grow the buffer and retry bufflen *= 2 else: @@ -33,11 +33,11 @@ def _call_with_growing_buffer(callback, initial=1024): def _convert_systems(systems): try: - return (rascal_system_t * 1)(wrap_system(systems)._as_rascal_system_t()) + return (featomic_system_t * 1)(wrap_system(systems)._as_featomic_system_t()) except TypeError: # try iterating over the systems - return (rascal_system_t * len(systems))( - *list(wrap_system(s)._as_rascal_system_t() for s in systems) + return (featomic_system_t * len(systems))( + *list(wrap_system(s)._as_featomic_system_t() for s in systems) ) @@ -66,7 +66,7 @@ def _options_to_c( for i, parameter in enumerate(gradients): c_gradients[i] = parameter.encode("utf8") - c_options = rascal_calculation_options_t() + c_options = featomic_calculation_options_t() c_options.gradients = c_gradients c_options.gradients_count = c_gradients._length_ c_options.use_native_system = bool(use_native_system) @@ -118,7 +118,7 @@ def _options_to_c( class CalculatorBase: """ - This is the base class for all calculators in rascaline, providing the + This is the base class for all calculators in featomic, providing the :py:meth:`CalculatorBase.compute` function. One can initialize a ``Calculator`` in two ways: either directly with the registered @@ -132,10 +132,10 @@ class CalculatorBase: def __init__(self, name: str, parameters: str): self._c_name = name self._lib = _get_library() - self._as_parameter_ = self._lib.rascal_calculator( + self._as_parameter_ = self._lib.featomic_calculator( name.encode("utf8"), parameters.encode("utf8") ) - _check_rascal_pointer(self._as_parameter_) + _check_featomic_pointer(self._as_parameter_) self._selected_samples = None self._selected_features = None @@ -143,15 +143,15 @@ def __init__(self, name: str, parameters: str): def __del__(self): if hasattr(self, "_lib"): # if we failed to load the lib, don't double error by trying to call - # ``self._lib.rascal_calculator_free`` - self._lib.rascal_calculator_free(self) + # ``self._lib.featomic_calculator_free`` + self._lib.featomic_calculator_free(self) self._as_parameter_ = 0 @property def name(self) -> str: """name of this calculator""" return _call_with_growing_buffer( - lambda buffer, bufflen: self._lib.rascal_calculator_name( + lambda buffer, bufflen: self._lib.featomic_calculator_name( self, buffer, bufflen ) ) @@ -165,7 +165,7 @@ def c_name(self) -> str: def parameters(self): """parameters (formatted as JSON) used to create this calculator""" return _call_with_growing_buffer( - lambda buffer, bufflen: self._lib.rascal_calculator_parameters( + lambda buffer, bufflen: self._lib.featomic_calculator_parameters( self, buffer, bufflen ) ) @@ -176,7 +176,7 @@ def cutoffs(self) -> List[float]: cutoffs = ctypes.POINTER(ctypes.c_double)() cutoffs_count = c_uintptr_t() - self._lib.rascal_calculator_cutoffs(self, cutoffs, cutoffs_count) + self._lib.featomic_calculator_cutoffs(self, cutoffs, cutoffs_count) result = [] for i in range(cutoffs_count.value): @@ -198,9 +198,9 @@ def compute( :param systems: single system or list of systems on which to run the calculation. The systems will automatically be wrapped into compatible - classes (using :py:func:`rascaline.systems.wrap_system`). Multiple types of + classes (using :py:func:`featomic.systems.wrap_system`). Multiple types of systems are supported, see the documentation of - :py:class:`rascaline.IntoSystem` to get the full list. + :py:class:`featomic.IntoSystem` to get the full list. :param use_native_system: If ``True`` (this is the default), copy data from the ``systems`` into Rust ``SimpleSystem``. This can be a lot faster than having @@ -249,7 +249,7 @@ def compute( - ``"cell"``, for gradients of the representation with respect to the system's cell parameters. These gradients are computed at fixed positions, and often not what you want when computing gradients explicitly (they are - mainly used in ``rascaline.torch`` to integrate with backward + mainly used in ``featomic.torch`` to integrate with backward propagation). If you are trying to compute the virial or the stress, you should use ``"strain"`` gradients instead. @@ -307,7 +307,7 @@ def compute( selected_properties=selected_properties, selected_keys=selected_keys, ) - self._lib.rascal_calculator_compute( + self._lib.featomic_calculator_compute( self, tensor_map_ptr, c_systems, c_systems._length_, c_options ) diff --git a/python/rascaline/rascaline/calculators.py b/python/featomic/featomic/calculators.py similarity index 57% rename from python/rascaline/rascaline/calculators.py rename to python/featomic/featomic/calculators.py index 6ecd94959..9f9bf9456 100644 --- a/python/rascaline/rascaline/calculators.py +++ b/python/featomic/featomic/calculators.py @@ -1,8 +1,10 @@ import json +from featomic._hypers import BadHyperParameters, convert_hypers, hypers_to_json + try: - # see rascaline-torch/calculators.py for the explanation of what's going on here + # see featomic-torch/calculators.py for the explanation of what's going on here _ = CalculatorBase except NameError: from .calculator_base import CalculatorBase @@ -18,20 +20,24 @@ class AtomicComposition(CalculatorBase): system is saved. The only sample left is named ``system``. """ - def __init__(self, per_system): - parameters = { - "per_system": per_system, - } + def __init__(self, *, per_system): + parameters = hypers_to_json( + { + "per_system": per_system, + } + ) super().__init__("atomic_composition", json.dumps(parameters)) class DummyCalculator(CalculatorBase): - def __init__(self, cutoff, delta, name): - parameters = { - "cutoff": cutoff, - "delta": delta, - "name": name, - } + def __init__(self, *, cutoff, delta, name): + parameters = hypers_to_json( + { + "cutoff": cutoff, + "delta": delta, + "name": name, + } + ) super().__init__("dummy_calculator", json.dumps(parameters)) @@ -57,17 +63,14 @@ class NeighborList(CalculatorBase): crossed to create this pair. """ - def __init__( - self, - cutoff: float, - full_neighbor_list: bool, - self_pairs: bool = False, - ): - parameters = { - "cutoff": cutoff, - "full_neighbor_list": full_neighbor_list, - "self_pairs": self_pairs, - } + def __init__(self, *, cutoff, full_neighbor_list, self_pairs=False): + parameters = hypers_to_json( + { + "cutoff": cutoff, + "full_neighbor_list": full_neighbor_list, + "self_pairs": self_pairs, + } + ) super().__init__("neighbor_list", json.dumps(parameters)) @@ -86,15 +89,38 @@ class SortedDistances(CalculatorBase): :ref:`documentation `. """ - def __init__(self, cutoff, max_neighbors, separate_neighbor_types): - parameters = { - "cutoff": cutoff, - "max_neighbors": max_neighbors, - "separate_neighbor_types": separate_neighbor_types, - } + def __init__(self, *, cutoff, max_neighbors, separate_neighbor_types): + parameters = hypers_to_json( + { + "cutoff": cutoff, + "max_neighbors": max_neighbors, + "separate_neighbor_types": separate_neighbor_types, + } + ) super().__init__("sorted_distances", json.dumps(parameters)) +def _check_for_old_hypers(calculator, hypers): + try: + new_hypers = convert_hypers( + origin="rascaline", + representation=calculator, + hypers=hypers, + ) + except BadHyperParameters as e: + print(e) + raise ValueError( + f"invalid hyper parameters to {calculator}, " + "expected `density` and `basis` to be present" + ) + + raise ValueError( + f"{calculator} hyper parameter changed recently, " + "please update your code. Here are the new equivalent parameters:\n" + + new_hypers + ) + + class SphericalExpansion(CalculatorBase): """Spherical expansion of Smooth Overlap of Atomic Positions (SOAP). @@ -108,35 +134,23 @@ class SphericalExpansion(CalculatorBase): See `this review article `_ for more information on the SOAP representation, and `this paper `_ for information on how it is - implemented in rascaline. + implemented in featomic. For a full description of the hyper-parameters, see the corresponding :ref:`documentation `. """ - def __init__( - self, - cutoff, - max_radial, - max_angular, - atomic_gaussian_width, - radial_basis, - center_atom_weight, - cutoff_function, - radial_scaling=None, - ): - parameters = { - "cutoff": cutoff, - "max_radial": max_radial, - "max_angular": max_angular, - "atomic_gaussian_width": atomic_gaussian_width, - "center_atom_weight": center_atom_weight, - "radial_basis": radial_basis, - "cutoff_function": cutoff_function, - } - - if radial_scaling is not None: - parameters["radial_scaling"] = radial_scaling + def __init__(self, *, cutoff=None, density=None, basis=None, **kwargs): + if len(kwargs) != 0 or density is None or basis is None: + _check_for_old_hypers("SphericalExpansion", {"cutoff": cutoff, **kwargs}) + + parameters = hypers_to_json( + { + "cutoff": cutoff, + "density": density, + "basis": basis, + } + ) super().__init__("spherical_expansion", json.dumps(parameters)) @@ -174,31 +188,68 @@ class SphericalExpansionByPair(CalculatorBase): :ref:`documentation `. """ + def __init__(self, *, cutoff=None, density=None, basis=None, **kwargs): + if len(kwargs) != 0 or density is None or basis is None: + _check_for_old_hypers( + "SphericalExpansionByPair", {"cutoff": cutoff, **kwargs} + ) + + parameters = hypers_to_json( + { + "cutoff": cutoff, + "density": density, + "basis": basis, + } + ) + + super().__init__("spherical_expansion_by_pair", json.dumps(parameters)) + + +class SphericalExpansionForBonds(CalculatorBase): + """A SOAP-like spherical expansion coefficients for bond-centered environments + In other words, the spherical expansion of the neighbor density function centered + on the center of a bond, + 'after' rotating the system so that the bond is aligned with the z axis. + + This is not rotationally invariant, and as such you should use some + not-implemented-here matheatical trick + similar to what SOAP (the :py:class:`SoapPowerSpectrum` class) uses. + + Most hyperparameters are identical to that of the regulat spherical expansion: + :ref:`documentation `. + + the few changes to this are: + + - "cutoff" renamed to "third_cutoff" + - "bond_cutoff" which expresses how the pairs of atoms used for the 'bonds' are + chosen. + - "center_atomS_weight" (caps only used for emphasis): the weight multiplier + for the coefficients of the self interactions + (where the neighboring atom is one of the pair's atoms). + """ + def __init__( - self, - cutoff, - max_radial, - max_angular, - atomic_gaussian_width, - radial_basis, - center_atom_weight, - cutoff_function, - radial_scaling=None, + self, *, + third_cutoff, + density, + basis, + bond_cutoff_radius, + center_atoms_weight=None ): - parameters = { - "cutoff": cutoff, - "max_radial": max_radial, - "max_angular": max_angular, - "atomic_gaussian_width": atomic_gaussian_width, - "center_atom_weight": center_atom_weight, - "radial_basis": radial_basis, - "cutoff_function": cutoff_function, - } - - if radial_scaling is not None: - parameters["radial_scaling"] = radial_scaling + if center_atoms_weight is None: + center_atoms_weight = 1 - super().__init__("spherical_expansion_by_pair", json.dumps(parameters)) + parameters = hypers_to_json({ + "bond_cutoff_radius": bond_cutoff_radius, + "center_atoms_weight": center_atoms_weight, + "raw_spherical_expansion": { + "cutoff": third_cutoff, + "density": density, + "basis": basis, + }, + }) + + super().__init__("spherical_expansion_for_bonds", json.dumps(parameters)) class SoapRadialSpectrum(CalculatorBase): @@ -212,33 +263,23 @@ class SoapRadialSpectrum(CalculatorBase): See `this review article `_ for more information on the SOAP representation, and `this paper `_ for information on how it is - implemented in rascaline. + implemented in featomic. For a full description of the hyper-parameters, see the corresponding :ref:`documentation `. """ - def __init__( - self, - cutoff, - max_radial, - atomic_gaussian_width, - center_atom_weight, - radial_basis, - cutoff_function, - radial_scaling=None, - ): - parameters = { - "cutoff": cutoff, - "max_radial": max_radial, - "atomic_gaussian_width": atomic_gaussian_width, - "center_atom_weight": center_atom_weight, - "radial_basis": radial_basis, - "cutoff_function": cutoff_function, - } - - if radial_scaling is not None: - parameters["radial_scaling"] = radial_scaling + def __init__(self, *, cutoff=None, density=None, basis=None, **kwargs): + if len(kwargs) != 0 or density is None or basis is None: + _check_for_old_hypers("SoapRadialSpectrum", {"cutoff": cutoff, **kwargs}) + + parameters = hypers_to_json( + { + "cutoff": cutoff, + "density": density, + "basis": basis, + } + ) super().__init__("soap_radial_spectrum", json.dumps(parameters)) @@ -254,39 +295,27 @@ class SoapPowerSpectrum(CalculatorBase): See `this review article `_ for more information on the SOAP representation, and `this paper `_ for information on how it is - implemented in rascaline. + implemented in featomic. For a full description of the hyper-parameters, see the corresponding :ref:`documentation `. .. seealso:: - :py:class:`rascaline.utils.PowerSpectrum` is an implementation that + :py:class:`featomic.utils.PowerSpectrum` is an implementation that allows to compute the power spectrum from different spherical expansions. """ - def __init__( - self, - cutoff, - max_radial, - max_angular, - atomic_gaussian_width, - center_atom_weight, - radial_basis, - cutoff_function, - radial_scaling=None, - ): - parameters = { - "cutoff": cutoff, - "max_radial": max_radial, - "max_angular": max_angular, - "atomic_gaussian_width": atomic_gaussian_width, - "center_atom_weight": center_atom_weight, - "radial_basis": radial_basis, - "cutoff_function": cutoff_function, - } - - if radial_scaling is not None: - parameters["radial_scaling"] = radial_scaling + def __init__(self, *, cutoff=None, density=None, basis=None, **kwargs): + if len(kwargs) != 0 or density is None or basis is None: + _check_for_old_hypers("SoapPowerSpectrum", {"cutoff": cutoff, **kwargs}) + + parameters = hypers_to_json( + { + "cutoff": cutoff, + "density": density, + "basis": basis, + } + ) super().__init__("soap_power_spectrum", json.dumps(parameters)) @@ -308,26 +337,18 @@ class LodeSphericalExpansion(CalculatorBase): :ref:`documentation `. """ - def __init__( - self, - cutoff, - max_radial, - max_angular, - atomic_gaussian_width, - center_atom_weight, - potential_exponent, - radial_basis, - k_cutoff=None, - ): - parameters = { - "cutoff": cutoff, - "k_cutoff": k_cutoff, - "max_radial": max_radial, - "max_angular": max_angular, - "atomic_gaussian_width": atomic_gaussian_width, - "center_atom_weight": center_atom_weight, - "potential_exponent": potential_exponent, - "radial_basis": radial_basis, - } + def __init__(self, *, density=None, basis=None, k_cutoff=None, **kwargs): + if len(kwargs) != 0 or density is None or basis is None: + _check_for_old_hypers( + "LodeSphericalExpansion", {"k_cutoff": k_cutoff, **kwargs} + ) + + parameters = hypers_to_json( + { + "k_cutoff": k_cutoff, + "density": density, + "basis": basis, + } + ) super().__init__("lode_spherical_expansion", json.dumps(parameters)) diff --git a/python/rascaline/rascaline/utils/clebsch_gordan/__init__.py b/python/featomic/featomic/clebsch_gordan/__init__.py similarity index 66% rename from python/rascaline/rascaline/utils/clebsch_gordan/__init__.py rename to python/featomic/featomic/clebsch_gordan/__init__.py index 6ba9fac07..d2f805482 100644 --- a/python/rascaline/rascaline/utils/clebsch_gordan/__init__.py +++ b/python/featomic/featomic/clebsch_gordan/__init__.py @@ -2,3 +2,5 @@ from ._cg_product import ClebschGordanProduct # noqa: F401 from ._coefficients import calculate_cg_coefficients # noqa: F401 from ._density_correlations import DensityCorrelations # noqa: F401 +from ._equivariant_power_spectrum import EquivariantPowerSpectrum # noqa: F401 +from ._power_spectrum import PowerSpectrum # noqa: F401 diff --git a/python/rascaline/rascaline/utils/_backend.py b/python/featomic/featomic/clebsch_gordan/_backend.py similarity index 99% rename from python/rascaline/rascaline/utils/_backend.py rename to python/featomic/featomic/clebsch_gordan/_backend.py index 979a9f998..f289aaf63 100644 --- a/python/rascaline/rascaline/utils/_backend.py +++ b/python/featomic/featomic/clebsch_gordan/_backend.py @@ -37,7 +37,6 @@ class TorchTensor: pass class TorchModule: - def __call__(self, *arg, **kwargs): return self.forward(*arg, **kwargs) diff --git a/python/rascaline/rascaline/utils/clebsch_gordan/_cartesian_spherical.py b/python/featomic/featomic/clebsch_gordan/_cartesian_spherical.py similarity index 99% rename from python/rascaline/rascaline/utils/clebsch_gordan/_cartesian_spherical.py rename to python/featomic/featomic/clebsch_gordan/_cartesian_spherical.py index e276775e2..32fc7eba2 100644 --- a/python/rascaline/rascaline/utils/clebsch_gordan/_cartesian_spherical.py +++ b/python/featomic/featomic/clebsch_gordan/_cartesian_spherical.py @@ -7,8 +7,8 @@ import numpy as np -from .. import _dispatch -from .._backend import ( +from . import _coefficients, _dispatch +from ._backend import ( Array, Labels, TensorBlock, @@ -16,7 +16,6 @@ TorchTensor, torch_jit_is_scripting, ) -from . import _coefficients def cartesian_to_spherical( diff --git a/python/rascaline/rascaline/utils/clebsch_gordan/_cg_product.py b/python/featomic/featomic/clebsch_gordan/_cg_product.py similarity index 98% rename from python/rascaline/rascaline/utils/clebsch_gordan/_cg_product.py rename to python/featomic/featomic/clebsch_gordan/_cg_product.py index 6e5268752..9d1ac4e0b 100644 --- a/python/rascaline/rascaline/utils/clebsch_gordan/_cg_product.py +++ b/python/featomic/featomic/clebsch_gordan/_cg_product.py @@ -7,8 +7,8 @@ import numpy as np -from .. import _dispatch -from .._backend import ( +from . import _coefficients, _dispatch, _utils +from ._backend import ( Device, DType, Labels, @@ -18,7 +18,6 @@ TorchScriptClass, torch_jit_is_scripting, ) -from . import _coefficients, _utils try: @@ -51,8 +50,8 @@ def __init__( coefficients for. :param cg_backend: :py:class:`str`, the backend to use for the CG tensor product. If ``None``, the backend is automatically selected based on the - arrays backend ("numpy" when importing this class from ``rascaline.utils``, - and "torch" when importing from ``rascaline.torch.utils``). + arrays backend ("numpy" when importing this class from ``featomic.utils``, + and "torch" when importing from ``featomic.torch.utils``). :param keys_filter: A function to remove more keys from the output. This is applied after any user-provided ``key_selection`` in :py:meth:`compute`. This function should take one argument ``keys: Labels``, and return the diff --git a/python/rascaline/rascaline/utils/clebsch_gordan/_coefficients.py b/python/featomic/featomic/clebsch_gordan/_coefficients.py similarity index 99% rename from python/rascaline/rascaline/utils/clebsch_gordan/_coefficients.py rename to python/featomic/featomic/clebsch_gordan/_coefficients.py index b34bf5eeb..e7d94f336 100644 --- a/python/rascaline/rascaline/utils/clebsch_gordan/_coefficients.py +++ b/python/featomic/featomic/clebsch_gordan/_coefficients.py @@ -9,8 +9,8 @@ import numpy as np import wigners -from .. import _dispatch -from .._backend import ( +from . import _dispatch +from ._backend import ( BACKEND_IS_METATENSOR_TORCH, Array, Device, @@ -191,7 +191,6 @@ def _build_dense_cg_coeff_dict( for o3_lambda in range( max(l1, l2) - min(l1, l2), min(lambda_max, (l1 + l2)) + 1 ): - complex_cg = _dispatch.to( wigners.clebsch_gordan_array(l1, l2, o3_lambda), backend=arrays_backend, @@ -354,9 +353,9 @@ def _real2complex(o3_lambda: int, like: Array) -> Array: for m in range(-o3_lambda, o3_lambda + 1): if m < 0: # Positive part - result[o3_lambda + m, o3_lambda + m] = i_sqrt_2 + result[o3_lambda + m, o3_lambda + m] = -i_sqrt_2 # Negative part - result[o3_lambda + m, o3_lambda - m] = -i_sqrt_2 * ((-1) ** m) + result[o3_lambda + m, o3_lambda - m] = i_sqrt_2 * ((-1) ** m) if m == 0: result[o3_lambda, o3_lambda] = 1.0 diff --git a/python/rascaline/rascaline/utils/clebsch_gordan/_density_correlations.py b/python/featomic/featomic/clebsch_gordan/_density_correlations.py similarity index 97% rename from python/rascaline/rascaline/utils/clebsch_gordan/_density_correlations.py rename to python/featomic/featomic/clebsch_gordan/_density_correlations.py index fbabeed6f..0fa7f0a9e 100644 --- a/python/rascaline/rascaline/utils/clebsch_gordan/_density_correlations.py +++ b/python/featomic/featomic/clebsch_gordan/_density_correlations.py @@ -1,15 +1,16 @@ """ -This module provides convenience calculators for preforming density correlations, i.e. -the (iterative) CG tensor products of density (body order 2) tensors. +This module provides a convenience calculator for performing density auto- +correlations, i.e. the (iterative) CG tensor products of density (body +order 2) tensors. -All of these calculators wrap the :py:class:`ClebschGordanProduct` class, handling the -higher-level metadata manipulation to produce the desired output tensors. +This wraps the :py:class:`ClebschGordanProduct` class, handling the higher-level +metadata manipulation to produce the desired output tensors. """ from typing import List, Optional -from .. import _dispatch -from .._backend import Device, DType, Labels, TensorMap, TorchModule, operations +from . import _dispatch +from ._backend import Device, DType, Labels, TensorMap, TorchModule, operations from ._cg_product import ClebschGordanProduct @@ -175,7 +176,6 @@ def _density_correlations( # Perform iterative CG tensor products new_lambda_names: List[str] = [] for i_correlation in range(self._n_correlations): - # Increment the density property dimension names density = _increment_property_names(density, 1) diff --git a/python/rascaline/rascaline/utils/_dispatch.py b/python/featomic/featomic/clebsch_gordan/_dispatch.py similarity index 100% rename from python/rascaline/rascaline/utils/_dispatch.py rename to python/featomic/featomic/clebsch_gordan/_dispatch.py diff --git a/python/featomic/featomic/clebsch_gordan/_equivariant_power_spectrum.py b/python/featomic/featomic/clebsch_gordan/_equivariant_power_spectrum.py new file mode 100644 index 000000000..6354f8356 --- /dev/null +++ b/python/featomic/featomic/clebsch_gordan/_equivariant_power_spectrum.py @@ -0,0 +1,365 @@ +""" +This module provides a convenience calculator for computing a single-center equivariant +power spectrum. +""" + +import json +from typing import List, Optional, Union + +from . import _dispatch +from ._backend import ( + CalculatorBase, + Device, + DType, + IntoSystem, + Labels, + TensorMap, + TorchModule, + operations, +) +from ._cg_product import ClebschGordanProduct +from ._density_correlations import _filter_redundant_keys + + +class EquivariantPowerSpectrum(TorchModule): + r""" + Computes a general equivariant power spectrum descriptor of two calculators. + + If only ``calculator_1`` is provided, the power spectrum is computed as the density + auto-correlation of the density produced by the first calculator. If + ``calculator_2`` is also provided, the power spectrum is computed as the density + cross-correlation of the densities produced by the two calculators. + + Example + ------- + As an example we calculate the equivariant power spectrum for a short range (sr) + spherical expansion and a long-range (lr) LODE spherical expansion for a NaCl + crystal. + + >>> import featomic + >>> import ase + + Construct the NaCl crystal + + >>> atoms = ase.Atoms( + ... symbols="NaCl", + ... positions=[[0, 0, 0], [0.5, 0.5, 0.5]], + ... pbc=True, + ... cell=[1, 1, 1], + ... ) + + Define the hyper parameters for the short-range spherical expansion + + >>> sr_hypers = { + ... "cutoff": { + ... "radius": 1.0, + ... "smoothing": {"type": "ShiftedCosine", "width": 0.5}, + ... }, + ... "density": { + ... "type": "Gaussian", + ... "width": 0.3, + ... }, + ... "basis": { + ... "type": "TensorProduct", + ... "max_angular": 2, + ... "radial": {"type": "Gto", "max_radial": 5}, + ... }, + ... } + + Define the hyper parameters for the long-range LODE spherical expansion from the + hyper parameters of the short-range spherical expansion + + >>> lr_hypers = { + ... "density": { + ... "type": "SmearedPowerLaw", + ... "smearing": 0.3, + ... "exponent": 1, + ... }, + ... "basis": { + ... "type": "TensorProduct", + ... "max_angular": 2, + ... "radial": {"type": "Gto", "max_radial": 3, "radius": 1.0}, + ... }, + ... } + + Construct the calculators + + >>> sr_calculator = featomic.SphericalExpansion(**sr_hypers) + >>> lr_calculator = featomic.LodeSphericalExpansion(**lr_hypers) + + Construct the power spectrum calculators and compute the spherical expansion + + >>> calculator = featomic.clebsch_gordan.EquivariantPowerSpectrum( + ... sr_calculator, lr_calculator + ... ) + >>> power_spectrum = calculator.compute(atoms, neighbors_to_properties=True) + + The resulting equivariants are stored as :py:class:`metatensor.TensorMap` as for any + other calculator. The keys contain the symmetry information: + + >>> power_spectrum.keys + Labels( + o3_lambda o3_sigma center_type + 0 1 11 + 1 1 11 + 2 1 11 + 1 -1 11 + 2 -1 11 + 3 1 11 + 3 -1 11 + 4 1 11 + 0 1 17 + 1 1 17 + 2 1 17 + 1 -1 17 + 2 -1 17 + 3 1 17 + 3 -1 17 + 4 1 17 + ) + + The block properties contain the angular order of the combined blocks ("l_1", + "l_2"), along with the neighbor types ("neighbor_1_type", "neighbor_2_type") and + radial channel indices. + + >>> power_spectrum[0].properties.names + ['l_1', 'l_2', 'neighbor_1_type', 'n_1', 'neighbor_2_type', 'n_2'] + + .. seealso:: + Faster power spectrum calculator specifically for invariant descriptors can + be found at :py:class:`featomic.SoapPowerSpectrum` and + :py:class:`featomic.clebsch_gordan.PowerSpectrum`. + """ + + def __init__( + self, + calculator_1: CalculatorBase, + calculator_2: Optional[CalculatorBase] = None, + neighbor_types: Optional[List[int]] = None, + *, + dtype: Optional[DType] = None, + device: Optional[Device] = None, + ): + """ + Constructs the equivariant power spectrum calculator. + + :param calculator_1: first calculator that computes a density descriptor, either + a :py:class:`featomic.SphericalExpansion` or + :py:class:`featomic.LodeSphericalExpansion`. + :param calculator_2: optional second calculator that computes a density + descriptor, either a :py:class:`featomic.SphericalExpansion` or + :py:class:`featomic.LodeSphericalExpansion`. If ``None``, the equivariant + power spectrum is computed as the auto-correlation of the first calculator. + Defaults to ``None``. + :param neighbor_types: List of ``"neighbor_type"`` to use in the properties of + the output. This option might be useful when running the calculation on + subset of a whole dataset and trying to join along the ``sample`` dimension + after the calculation. If ``None``, blocks are filled with + ``"neighbor_type"`` found in the systems. This parameter is only used if + ``neighbors_to_properties=True`` is passed to the :py:meth:`compute` method. + :param dtype: the scalar type to use to store coefficients + :param device: the computational device to use for calculations. + """ + + super().__init__() + self.calculator_1 = calculator_1 + self.calculator_2 = calculator_2 + self.neighbor_types = neighbor_types + self.dtype = dtype + self.device = device + + supported_calculators = ["lode_spherical_expansion", "spherical_expansion"] + + if self.calculator_1.c_name not in supported_calculators: + raise ValueError( + f"Only [{', '.join(supported_calculators)}] are supported for " + f"`calculator_1`, got '{self.calculator_1.c_name}'" + ) + + parameters_1 = json.loads(calculator_1.parameters) + + if self.calculator_2 is None: + parameters_2 = parameters_1 + else: + if self.calculator_2.c_name not in supported_calculators: + raise ValueError( + f"Only [{', '.join(supported_calculators)}] are supported for " + f"`calculator_2`, got '{self.calculator_2.c_name}'" + ) + + parameters_2 = json.loads(calculator_2.parameters) + if parameters_1["basis"]["type"] != "TensorProduct": + raise ValueError( + "only 'TensorProduct' basis is supported for calculator_1" + ) + + if parameters_2["basis"]["type"] != "TensorProduct": + raise ValueError( + "only 'TensorProduct' basis is supported for calculator_2" + ) + + self._cg_product = ClebschGordanProduct( + max_angular=parameters_1["basis"]["max_angular"] + + parameters_2["basis"]["max_angular"], + cg_backend=None, + keys_filter=_filter_redundant_keys, + arrays_backend=None, + dtype=dtype, + device=device, + ) + + @property + def name(self): + """Name of this calculator.""" + return "EquivariantPowerSpectrum" + + def compute( + self, + systems: Union[IntoSystem, List[IntoSystem]], + selected_keys: Optional[Labels] = None, + neighbors_to_properties: bool = False, + ) -> TensorMap: + """ + Computes an equivariant power spectrum, also called "Lambda-SOAP" when doing a + self-correlation of the SOAP density. + + First computes a :py:class:`SphericalExpansion` density descriptor of body order + 2. + + Before performing the Clebsch-Gordan tensor product, the spherical expansion + density can be densified by moving the key dimension "neighbor_type" to the + block properties. This is controlled by the ``neighbors_to_properties`` + parameter. Depending on the specific systems descriptors are being computed for, + the sparsity or density of the density can affect the computational cost of the + Clebsch-Gordan tensor product. + + If ``neighbors_to_properties=True`` and ``neighbor_types`` have been passed to + the constructor, property dimensions are created for all of these global atom + types when moving the key dimension to properties. This ensures that the output + properties dimension is of consistent size across all systems passed in + ``systems``. + + Finally a single Clebsch-Gordan tensor product is taken to produce a body order + 3 equivariant power spectrum. + + :param selected_keys: :py:class:`Labels`, the output keys to computed. If + ``None``, all keys are computed. Subsets of key dimensions can be passed to + compute output blocks that match in these dimensions. + :param neighbors_to_properties: :py:class:`bool`, if true, densifies the + spherical expansion by moving key dimension "neighbor_type" to properties + prior to performing the Clebsch Gordan product step. Defaults to false. + + :return: :py:class:`TensorMap`, the output equivariant power spectrum. + """ + return self._equivariant_power_spectrum( + systems=systems, + selected_keys=selected_keys, + neighbors_to_properties=neighbors_to_properties, + compute_metadata=False, + ) + + def forward( + self, + systems: Union[IntoSystem, List[IntoSystem]], + selected_keys: Optional[Labels] = None, + neighbors_to_properties: bool = False, + ) -> TensorMap: + """ + Calls the :py:meth:`compute` method. + + This is intended for :py:class:`torch.nn.Module` compatibility, and should be + ignored in pure Python mode. + + See :py:meth:`compute` for a full description of the parameters. + """ + return self.compute( + systems=systems, + selected_keys=selected_keys, + neighbors_to_properties=neighbors_to_properties, + ) + + def compute_metadata( + self, + systems: Union[IntoSystem, List[IntoSystem]], + selected_keys: Optional[Labels] = None, + neighbors_to_properties: bool = False, + ) -> TensorMap: + """ + Returns the metadata-only :py:class:`TensorMap` that would be output by the + function :py:meth:`compute` for the same calculator under the same settings, + without performing the actual Clebsch-Gordan tensor products in the second step. + + See :py:meth:`compute` for a full description of the parameters. + """ + return self._equivariant_power_spectrum( + systems=systems, + selected_keys=selected_keys, + neighbors_to_properties=neighbors_to_properties, + compute_metadata=True, + ) + + def _equivariant_power_spectrum( + self, + systems: Union[IntoSystem, List[IntoSystem]], + selected_keys: Optional[Labels], + neighbors_to_properties: bool, + compute_metadata: bool, + ) -> TensorMap: + """ + Computes the equivariant power spectrum, either fully or just metadata + """ + # Compute density + density_1 = self.calculator_1.compute(systems) + + if self.calculator_2 is None: + density_2 = density_1 + else: + density_2 = self.calculator_2.compute(systems) + + # Rename "neighbor_type" dimension so they are correlated + density_1 = operations.rename_dimension( + density_1, "keys", "neighbor_type", "neighbor_1_type" + ) + density_2 = operations.rename_dimension( + density_2, "keys", "neighbor_type", "neighbor_2_type" + ) + density_1 = operations.rename_dimension(density_1, "properties", "n", "n_1") + density_2 = operations.rename_dimension(density_2, "properties", "n", "n_2") + + if neighbors_to_properties: + if self.neighbor_types is None: # just move neighbor type + keys_to_move_1 = "neighbor_1_type" + keys_to_move_2 = "neighbor_2_type" + else: # use the user-specified types + values = _dispatch.list_to_array( + array=density_1.keys.values, + data=[[t] for t in self.neighbor_types], + ) + keys_to_move_1 = Labels(names="neighbor_1_type", values=values) + keys_to_move_2 = Labels(names="neighbor_2_type", values=values) + + density_1 = density_1.keys_to_properties(keys_to_move_1) + density_2 = density_2.keys_to_properties(keys_to_move_2) + + # Compute the power spectrum + if compute_metadata: + pow_spec = self._cg_product.compute_metadata( + tensor_1=density_1, + tensor_2=density_2, + o3_lambda_1_new_name="l_1", + o3_lambda_2_new_name="l_2", + selected_keys=selected_keys, + ) + else: + pow_spec = self._cg_product.compute( + tensor_1=density_1, + tensor_2=density_2, + o3_lambda_1_new_name="l_1", + o3_lambda_2_new_name="l_2", + selected_keys=selected_keys, + ) + + # Move the CG combination info keys to properties + pow_spec = pow_spec.keys_to_properties(["l_1", "l_2"]) + + return pow_spec diff --git a/python/rascaline/rascaline/utils/power_spectrum.py b/python/featomic/featomic/clebsch_gordan/_power_spectrum.py similarity index 76% rename from python/rascaline/rascaline/utils/power_spectrum.py rename to python/featomic/featomic/clebsch_gordan/_power_spectrum.py index 35c1c1a4a..6b809f626 100644 --- a/python/rascaline/rascaline/utils/power_spectrum.py +++ b/python/featomic/featomic/clebsch_gordan/_power_spectrum.py @@ -37,26 +37,15 @@ class PowerSpectrum(TorchModule): If ``calculator_2=None`` invariants are generated by combining the coefficients of the spherical expansion of ``calculator_1``. The spherical expansions given as input - can only be :py:class:`rascaline.SphericalExpansion` or - :py:class:`rascaline.LodeSphericalExpansion`. - - :param calculator_1: first calculator - :param calculator_1: second calculator - :param types: List of ``"neighbor_type"`` to use in the properties of the output. - This option might be useful when running the calculation on subset of a whole - dataset and trying to join along the ``sample`` dimension after the calculation. - If ``None``, blocks are filled with ``"neighbor_type"`` found in the systems. - :raises ValueError: If other calculators than - :py:class:`rascaline.SphericalExpansion` or - :py:class:`rascaline.LodeSphericalExpansion` are used. - :raises ValueError: If ``"max_angular"`` of both calculators is different. + can only be :py:class:`featomic.SphericalExpansion` or + :py:class:`featomic.LodeSphericalExpansion`. Example ------- As an example we calculate the power spectrum for a short range (sr) spherical expansion and a long-range (lr) LODE spherical expansion for a NaCl crystal. - >>> import rascaline + >>> import featomic >>> import ase Construct the NaCl crystal @@ -71,35 +60,45 @@ class PowerSpectrum(TorchModule): Define the hyper parameters for the short-range spherical expansion >>> sr_hypers = { - ... "cutoff": 1.0, - ... "max_radial": 6, - ... "max_angular": 2, - ... "atomic_gaussian_width": 0.3, - ... "center_atom_weight": 1.0, - ... "radial_basis": { - ... "Gto": {}, + ... "cutoff": { + ... "radius": 1.0, + ... "smoothing": {"type": "ShiftedCosine", "width": 0.5}, ... }, - ... "cutoff_function": { - ... "ShiftedCosine": {"width": 0.5}, + ... "density": { + ... "type": "Gaussian", + ... "width": 0.3, + ... }, + ... "basis": { + ... "type": "TensorProduct", + ... "max_angular": 2, + ... "radial": {"type": "Gto", "max_radial": 5}, ... }, ... } Define the hyper parameters for the long-range LODE spherical expansion from the hyper parameters of the short-range spherical expansion - >>> lr_hypers = sr_hypers.copy() - >>> lr_hypers.pop("cutoff_function") - {'ShiftedCosine': {'width': 0.5}} - >>> lr_hypers["potential_exponent"] = 1 + >>> lr_hypers = { + ... "density": { + ... "type": "SmearedPowerLaw", + ... "smearing": 0.3, + ... "exponent": 1, + ... }, + ... "basis": { + ... "type": "TensorProduct", + ... "max_angular": 2, + ... "radial": {"type": "Gto", "max_radial": 5, "radius": 1.0}, + ... }, + ... } Construct the calculators - >>> sr_calculator = rascaline.SphericalExpansion(**sr_hypers) - >>> lr_calculator = rascaline.LodeSphericalExpansion(**lr_hypers) + >>> sr_calculator = featomic.SphericalExpansion(**sr_hypers) + >>> lr_calculator = featomic.LodeSphericalExpansion(**lr_hypers) Construct the power spectrum calculators and compute the spherical expansion - >>> calculator = rascaline.utils.PowerSpectrum(sr_calculator, lr_calculator) + >>> calculator = featomic.clebsch_gordan.PowerSpectrum(sr_calculator, lr_calculator) >>> power_spectrum = calculator.compute(atoms) The resulting invariants are stored as :py:class:`metatensor.TensorMap` as for any @@ -121,39 +120,73 @@ class PowerSpectrum(TorchModule): .. seealso:: If you are interested in the SOAP power spectrum you can the use the - faster :py:class:`rascaline.SoapPowerSpectrum`. + faster :py:class:`featomic.SoapPowerSpectrum`. For an equivariant version of + this calculator, computing the power spectrum for covariant as well as + invariant blocks, see :py:class:`featomic.EquivariantPowerSpectrum`. """ def __init__( self, calculator_1: CalculatorBase, calculator_2: Optional[CalculatorBase] = None, - types: Optional[List[int]] = None, + neighbor_types: Optional[List[int]] = None, ): + """ + Constructs the power spectrum calculator. + + :param calculator_1: first calculator that computes a density descriptor, either + a :py:class:`featomic.SphericalExpansion` or + :py:class:`featomic.LodeSphericalExpansion`. + :param calculator_2: optional second calculator that computes a density + descriptor, either a :py:class:`featomic.SphericalExpansion` or + :py:class:`featomic.LodeSphericalExpansion`. If ``None``, the power spectrum + is computed as the auto-correlation of the first calculator. Defaults to + ``None``. + :param neighbor_types: List of ``"neighbor_type"`` to use in the properties of + the output. This option might be useful when running the calculation on + subset of a whole dataset and trying to join along the ``sample`` dimension + after the calculation. If ``None``, blocks are filled with + ``"neighbor_type"`` found in the systems. + """ super().__init__() self.calculator_1 = calculator_1 self.calculator_2 = calculator_2 - self.types = types + self.neighbor_types = neighbor_types supported_calculators = ["lode_spherical_expansion", "spherical_expansion"] if self.calculator_1.c_name not in supported_calculators: raise ValueError( - f"Only {','.join(supported_calculators)} are supported for " - "calculator_1!" + f"Only [{', '.join(supported_calculators)}] are supported for " + f"`calculator_1`, got '{self.calculator_1.c_name}'" ) if self.calculator_2 is not None: if self.calculator_2.c_name not in supported_calculators: raise ValueError( - f"Only {','.join(supported_calculators)} are supported for " - "calculator_2!" + f"Only [{', '.join(supported_calculators)}] are supported for " + f"`calculator_2`, got '{self.calculator_2.c_name}'" ) parameters_1 = json.loads(calculator_1.parameters) parameters_2 = json.loads(calculator_2.parameters) - if parameters_1["max_angular"] != parameters_2["max_angular"]: - raise ValueError("'max_angular' of both calculators must be the same!") + if parameters_1["basis"]["type"] != "TensorProduct": + raise ValueError( + "only 'TensorProduct' basis is supported for calculator_1" + ) + + if parameters_2["basis"]["type"] != "TensorProduct": + raise ValueError( + "only 'TensorProduct' basis is supported for calculator_2" + ) + + max_angular_1 = parameters_1["basis"]["max_angular"] + max_angular_2 = parameters_2["basis"]["max_angular"] + if max_angular_1 != max_angular_2: + raise ValueError( + "'basis.max_angular' must be the same in both calculators, " + f"got {max_angular_1} and {max_angular_2}" + ) @property def name(self): @@ -168,7 +201,7 @@ def compute( ) -> TensorMap: """Runs a calculation with this calculator on the given ``systems``. - See :py:func:`rascaline.calculators.CalculatorBase.compute()` for details on the + See :py:func:`featomic.calculators.CalculatorBase.compute()` for details on the parameters. :raises NotImplementedError: If a spherical expansions contains a gradient with @@ -196,7 +229,7 @@ def compute( assert spherical_expansion_1.keys.names == expected_key_names assert spherical_expansion_1.property_names == ["n"] - if self.types is None: + if self.neighbor_types is None: # Fill blocks with `neighbor_type` from ALL blocks. If we don't do this # merging blocks along the ``sample`` direction might be not possible. array = spherical_expansion_1.keys.column("neighbor_type") @@ -205,7 +238,7 @@ def compute( # Take user provided `neighbor_type` list. values = _dispatch.list_to_array( array=spherical_expansion_1.keys.values, - data=[[t] for t in self.types], + data=[[t] for t in self.neighbor_types], ) keys_to_move = Labels(names="neighbor_type", values=values) @@ -223,13 +256,13 @@ def compute( assert spherical_expansion_2.keys.names == expected_key_names assert spherical_expansion_2.property_names == ["n"] - if self.types is None: + if self.neighbor_types is None: array = spherical_expansion_2.keys.column("neighbor_type") values = _dispatch.unique(array).reshape(-1, 1) else: values = _dispatch.list_to_array( array=spherical_expansion_2.keys.values, - data=[[t] for t in self.types], + data=[[t] for t in self.neighbor_types], ) keys_to_move = Labels(names="neighbor_type", values=values) diff --git a/python/rascaline/rascaline/utils/clebsch_gordan/_utils.py b/python/featomic/featomic/clebsch_gordan/_utils.py similarity index 91% rename from python/rascaline/rascaline/utils/clebsch_gordan/_utils.py rename to python/featomic/featomic/clebsch_gordan/_utils.py index b02292d3d..10c23f29f 100644 --- a/python/rascaline/rascaline/utils/clebsch_gordan/_utils.py +++ b/python/featomic/featomic/clebsch_gordan/_utils.py @@ -6,9 +6,8 @@ from typing import List, Tuple -from .. import _dispatch -from .._backend import Labels, TensorBlock, TensorMap -from . import _coefficients +from . import _coefficients, _dispatch +from ._backend import Labels, TensorBlock, TensorMap # ======================================== # @@ -112,7 +111,6 @@ def _compute_output_keys( combinations: List[Tuple[int, int]] = [] for key_1_i in range(len(keys_1)): for key_2_i in range(len(keys_2)): - # Get the keys key_1 = keys_1.entry(key_1_i) key_2 = keys_2.entry(key_2_i) @@ -131,7 +129,6 @@ def _compute_output_keys( for o3_lambda in range( abs(o3_lambda_1 - o3_lambda_2), abs(o3_lambda_1 + o3_lambda_2) + 1 ): - # Calculate new sigma o3_sigma = int( o3_sigma_1 @@ -282,13 +279,35 @@ def _match_samples_of_blocks( Assumes that the samples dimensions of the block with fewer dimensions are a subset of the dimensions of the other. If the dimensions are not a subset, an error is raised. - - TODO: implement for samples dimensions that are not a subset of the other. """ - # If the number of dimensions are the same, check they are equivalent and return + # The number of dimensions are the same if len(block_1.samples.names) == len(block_2.samples.names): - if not block_1.samples == block_2.samples: - raise ValueError("Samples dimensions of the two blocks are not equivalent.") + if block_1.samples == block_2.samples: # nothing needs to be done + return block_1, block_2 + + # Find the union of samples and broadcast both blocks along samples axis + new_blocks: List[TensorBlock] = [] + union, mapping_1, mapping_2 = block_1.samples.union_and_mapping(block_2.samples) + for block, mapping in [(block_1, mapping_1), (block_2, mapping_2)]: + new_block_vals = _dispatch.zeros_like( + block.values, [len(union)] + [i for i in block.values.shape[1:]] + ) + new_block_vals[mapping] = block.values + new_block = TensorBlock( + values=new_block_vals, + samples=union, + components=block.components, + properties=block.properties, + ) + new_blocks.append(new_block) + + block_1, block_2 = new_blocks + + return block_1, block_2 + + # Otherwise, the samples dimensions of one block is assumed (and checked) to be a + # subset of the other. + assert len(block_1.samples.names) != len(block_2.samples.names) # First find the block with fewer dimensions. Reorder to have this block on the # 'left' for simplicity, but record the original order for the final output diff --git a/python/featomic/featomic/cutoff.py b/python/featomic/featomic/cutoff.py new file mode 100644 index 000000000..cc04ebdad --- /dev/null +++ b/python/featomic/featomic/cutoff.py @@ -0,0 +1,131 @@ +import abc +from typing import Optional + +import numpy as np + + +class SmoothingFunction(metaclass=abc.ABCMeta): + """ + Base class representing radial cutoff smoothing functions. + + You can inherit from this class to define new smoothing functions, implementing + :py:meth:`compute` accordingly. If the new smoothing function has corresponding + hyper parameters in the native calculators, you should also implement + :py:meth:`get_hypers`. + """ + + def _featomic_hypers(self): + return self.get_hypers() + + def get_hypers(self): + raise NotImplementedError( + f"this smoothing function ({self.__class__.__name__}) does not have " + "matching hyper parameters in the native calculators" + ) + + @abc.abstractmethod + def compute( + self, cutoff: float, positions: np.ndarray, *, derivative: bool + ) -> np.ndarray: + """ + Compute the smoothing function on grid points at the given ``positions``. + + :param cutoff: spherical cutoff radius + :param positions: positions of the grid points where the smoothing function + should be evaluated + :param derivative: should this function return the values of the smoothing + function or it's derivatives + """ + + +class ShiftedCosine(SmoothingFunction): + r""" + Shifted cosine smoothing function, with the following form: + + .. math:: + + f(r) = \begin{cases} + 1 & \text{for } r \le r_c - \sigma \\ + 1/2 \left(1 + \frac{\cos(\pi (r - r_c + \sigma)}{\sigma} \right) + & \text{for } r_c - \sigma \le r \le r_c \\ + 0 & \text{for } r \gt r_c \\ + \end{cases} + + with :math:`r_c` the cutoff radius and :math:`\sigma` is the width of the smoothing + (roughly how far from the cutoff smoothing should happen). + + :param width: width of the smoothing (:math:`\sigma` in the equation above) + """ + + def __init__(self, *, width: float): + self.width = float(width) + assert self.width >= 0 + + def get_hypers(self): + return {"type": "ShiftedCosine", "width": self.width} + + def compute( + self, cutoff: float, positions: np.ndarray, *, derivative: bool + ) -> np.ndarray: + assert cutoff > self.width + result = np.zeros_like(positions) + + mask = np.logical_and(positions >= cutoff - self.width, positions < cutoff) + s = np.pi * (positions[mask] - cutoff + self.width) / self.width + + if derivative: + result[mask] = -0.5 * np.pi * np.sin(s) / self.width + else: + result[positions < cutoff - self.width] = 1.0 + result[mask] = 0.5 * (1 + np.cos(s)) + + return result + + +class Step(SmoothingFunction): + """ + Step smoothing function, i.e. no smoothing. + + This function is equal to 1 inside the cutoff radius and to 0 outside of the cutoff + radius, with a discontinuity at the cutoff. + """ + + def get_hypers(self): + return {"type": "Step"} + + def compute( + self, cutoff: float, positions: np.ndarray, *, derivative: bool + ) -> np.ndarray: + if derivative: + return np.zeros_like(positions) + else: + return np.where(positions <= cutoff, 1.0, 0.0) + + +class Cutoff: + """ + The ``Cutoff`` class contains the definition of local environments, where an atom + environment is defined by all its neighbors inside a sphere centered on the atom + with the given spherical cutoff ``radius``. + + During an atomistic simulation, atoms entering and exiting the sphere will create + discontinuities. To prevent them, one can use a ``smoothing`` function, smoothing + introducing new atoms inside the neighborhood. + """ + + def __init__(self, radius: float, smoothing: Optional[SmoothingFunction]): + self.radius = float(radius) + assert self.radius >= 0.0 + + if smoothing is None: + self.smoothing = Step() + else: + self.smoothing = smoothing + + assert isinstance(self.smoothing, SmoothingFunction) + + def _featomic_hypers(self): + return { + "radius": self.radius, + "smoothing": self.smoothing, + } diff --git a/python/featomic/featomic/density.py b/python/featomic/featomic/density.py new file mode 100644 index 000000000..a1cfa4973 --- /dev/null +++ b/python/featomic/featomic/density.py @@ -0,0 +1,367 @@ +import abc +import warnings +from typing import Optional + +import numpy as np + + +try: + import scipy.special + + HAS_SCIPY = True +except ImportError: + HAS_SCIPY = False + + +class RadialScaling(metaclass=abc.ABCMeta): + """ + Base class representing radial scaling of atomic densities. + + You can inherit from this class to define new custom radial scaling, implementing + :py:meth:`compute` accordingly. If the new radial scaling has corresponding hyper + parameters in the native calculators, you should also implement + :py:meth:`get_hypers`. + """ + + def _featomic_hypers(self): + return self.get_hypers() + + def get_hypers(self): + """ + Return the native hyper parameters corresponding to this atomic density scaling + """ + raise NotImplementedError( + f"this density scaling ({self.__class__.__name__}) does not have matching " + "hyper parameters in the native calculators" + ) + + @abc.abstractmethod + def compute(self, positions: np.ndarray, *, derivative: bool) -> np.ndarray: + """ + Compute the scaling function (or it's derivative) on grid points at the given + ``positions``. + + :param positions: positions of grid point where to evaluate the radial scaling + :param derivative: should this function return the values or the derivatives of + the radial scaling? + :returns: evaluated radial scaling function + """ + + +class Willatt2018(RadialScaling): + r""" + Radial density scaling as proposed in https://doi.org/10.1039/C8CP05921G by Willatt + et. al. + + .. math:: + + \text{scaling}(r) = \frac{c}{c + \left(\frac{r}{r_0}\right) ^ m} + """ + + def __init__(self, *, exponent: int, rate: float, scale: float): + """ + :param exponent: :math:`m` in the formula above + :param rate: :math:`c` in the formula above + :param scale: :math:`r_0` in the formula above + """ + self.exponent = int(exponent) + self.rate = float(rate) + self.scale = float(scale) + + assert self.exponent >= 0 + assert self.scale > 0 + assert self.rate >= 0 + + def get_hypers(self): + return { + "type": "Willatt2018", + "exponent": self.exponent, + "rate": self.rate, + "scale": self.scale, + } + + def compute(self, positions: np.ndarray, *, derivative: bool) -> np.ndarray: + if self.rate == 0: + result = (self.scale / positions) ** self.exponent + if derivative: + result *= -self.exponent / positions + return result + elif self.exponent == 0: + if derivative: + return np.zeros_like(positions) + else: + return np.ones_like(positions) + else: + if derivative: + self.rate / (self.rate + (positions / self.scale) ** self.exponent) + else: + rs = positions / self.scale + denominator = (self.rate + rs**self.exponent) ** 2 + + factor = -self.rate * self.exponent / self.scale + + return factor * rs ** (self.exponent - 1) / denominator + + +class AtomicDensity(metaclass=abc.ABCMeta): + r""" + Base class representing atomic densities. + + You can inherit from this class to define new custom densities, implementing + :py:meth:`compute` accordingly. If the new density has corresponding hyper + parameters in the native calculators, you should also implement + :py:meth:`get_hypers`. + + All atomic densities are assumed to be invariant under rotation, and as such only + defined as a function of the distance to the origin. + + The overall density around a central atom is the sum of the central atom's neighbors + density, with some optional radial scaling. + + .. math:: + + \rho_i(r) = \sum_j \text{scaling}(r_{ij}) \; g_j(r - r_{ij}) + + where :math:`\text{scaling}(r)` is the scaling function, :math:`g_j` the density + coming from neighbor :math:`j`, and :math:`r_{ij}` the distance between the center + :math:`i` and neighbor :math:`j`. + """ + + def __init__( + self, + *, + center_atom_weight: float = 1.0, + scaling: Optional[RadialScaling] = None, + ): + r""" + :param center_atom_weight: in density expansion, the central atom sees its own + density, and is in this sense its own neighbor. Setting this weight to ``0`` + allows to disable this behavior and only expand the density of actual + neighbors. + :param scaling: optional radial scaling function. If this is left to ``None``, + no radial scaling is applied. + """ + self.center_atom_weight = float(center_atom_weight) + self.scaling = scaling + + assert isinstance(self.scaling, (type(None), RadialScaling)) + + @abc.abstractmethod + def compute(self, positions: np.ndarray, *, derivative: bool) -> np.ndarray: + """ + Compute the density (or it's derivative) around a single atom. + + The atom is located at ``position=0`` and the density is computed on multiple + grid points at the given ``positions``. The computed density does not include + any radial scaling or central atom weighting. + + :param positions: positions of grid point where to evaluate the atomic density + :param derivative: should this function return the values or the derivatives of + the density? + :returns: evaluated atomic density on the grid + """ + + def get_hypers(self): + """ + Return the native hyper parameters corresponding to this atomic density + """ + raise NotImplementedError( + f"This density ({self.__class__.__name__}) does not have matching " + "hyper parameters in the native calculators. It should be used " + "through one of the spliner class instead of directly." + ) + + def _featomic_hypers(self): + """ + Return the native hyper parameters corresponding to this atomic density. + """ + return { + **self.get_hypers(), + "center_atom_weight": self.center_atom_weight, + "scaling": self.scaling, + } + + +class DiracDelta(AtomicDensity): + r"""Delta atomic densities of the form :math:`g(r)=\delta(r)`.""" + + def __init__( + self, + *, + center_atom_weight: float = 1.0, + scaling: Optional[RadialScaling] = None, + ): + super().__init__(center_atom_weight=center_atom_weight, scaling=scaling) + + def get_hypers(self): + return {"type": "DiracDelta"} + + def compute(self, positions: np.ndarray, *, derivative: bool) -> np.ndarray: + if derivative: + return np.zeros_like(positions) + else: + result = np.zeros_like(positions) + result[result == 0.0] = 1.0 + return result + + +class Gaussian(AtomicDensity): + r"""Gaussian atomic density function. + + In featomic, we use the convention + + .. math:: + + g(r) = \frac{1}{(\pi \sigma^2)^{3/4}}e^{-\frac{r^2}{2\sigma^2}} \,. + + The prefactor was chosen such that the "L2-norm" of the Gaussian + + .. math:: + + \|g\|^2 = \int \mathrm{d}^3\boldsymbol{r} |g(r)|^2 = 1\,, + + The derivatives of the Gaussian atomic density with respect to the position is + + .. math:: + + g^\prime(r) = + \frac{\partial g(r)}{\partial r} = \frac{-r}{\sigma^2(\pi + \sigma^2)^{3/4}}e^{-\frac{r^2}{2\sigma^2}} \,. + + :param width: Width of the atom-centered gaussian used to create the atomic density + """ + + def __init__( + self, + *, + width: float, + center_atom_weight: float = 1.0, + scaling: Optional[RadialScaling] = None, + ): + super().__init__(center_atom_weight=center_atom_weight, scaling=scaling) + self.width = float(width) + + def get_hypers(self): + return {"type": "Gaussian", "width": self.width} + + def compute(self, positions: np.ndarray, *, derivative: bool) -> np.ndarray: + width_sq = self.width**2 + x = positions**2 / (2 * width_sq) + + density = np.exp(-x) / (np.pi * width_sq) ** (3 / 4) + + if derivative: + density *= -positions / width_sq + + return density + + +class SmearedPowerLaw(AtomicDensity): + r"""Smeared power law density, as used in LODE. + + This is a smooth, differentiable density that behaves like :math:`1 / r^p` as + :math:`r` goes to infinity. + + It is defined as + + .. math:: + + g(r) = \frac{1}{\Gamma\left(\frac{p}{2}\right)} + \frac{\gamma\left( \frac{p}{2}, \frac{r^2}{2\sigma^2} \right)} + {r^p}, + + where :math:`p` is the potential exponent, :math:`\Gamma(z)` is the Gamma function + and :math:`\gamma(a, x)` is the incomplete lower Gamma function. + + For more information about the derivation of this density, see + https://doi.org/10.1021/acs.jpclett.3c02375 and section D of the supplementary + information. + + :param smearing: Smearing used to remove the singularity at 0 (:math:`\sigma` above) + :param exponent: Potential exponent of the decorated atom density (:math:`p` above) + """ + + def __init__( + self, + *, + smearing: float, + exponent: int, + center_atom_weight: float = 1.0, + scaling: Optional[RadialScaling] = None, + ): + super().__init__(center_atom_weight=center_atom_weight, scaling=scaling) + self.smearing = float(smearing) + self.exponent = int(exponent) + + def get_hypers(self): + return { + "type": "SmearedPowerLaw", + "smearing": self.smearing, + "exponent": self.exponent, + } + + def compute(self, positions: np.ndarray, *, derivative: bool) -> np.ndarray: + if not HAS_SCIPY: + raise ValueError("SmearedPowerLaw requires scipy to be installed") + + if self.exponent == 0: + proxy = Gaussian(width=self.smearing) + return proxy.compute(positions=positions, derivative=derivative) + else: + smearing_sq = self.smearing**2 + a = self.exponent / 2 + x = positions**2 / (2 * smearing_sq) + + # Evaluating the formula above at :math:`r=0` is problematic because + # :math:`g(r)` is of the form :math:`0/0`. For practical implementations, it + # is thus more convenient to rewrite the density as + # + # .. math:: + # + # g(r) = \frac{1}{\Gamma(a)}\frac{1}{\left(2 \sigma^2\right)^a} + # \begin{cases} + # \frac{1}{a} - \frac{x}{a+1} + \frac{x^2}{2(a+2)}+\mathcal{O}(x^3) + # & x < 10^{-5} \\ + # \frac{\gamma(a,x)}{x^a} + # & x \geq 10^{-5} + # \end{cases} + # + # where :math:`a = p/2`. It is convenient to use the expression for + # sufficiently small :math:`x` since the relative weight of the first + # neglected term is on the order of :math:`1/6x^3`. Therefore, the threshold + # :math:`x = 10^{-5}` leads to relative errors on the order of the machine + # epsilon. + with warnings.catch_warnings(): + # Even though we use `np.where` to apply the `_compute_close_zero` + # method for small `x`, the `_compute_far_zero` method will also + # evaluated for small `x` and sending RuntimeWarnings. We filter these + # warnings to avoid that these are presented to the user. + warnings.filterwarnings("ignore", category=RuntimeWarning) + density = np.where( + x < 1e-5, + self._compute_close_zero(a, x, derivative=derivative), + self._compute_far_zero(a, x, derivative=derivative), + ) + + density *= 1 / scipy.special.gamma(a) / (2 * smearing_sq) ** a + + # add inner derivative: ∂x/∂r + if derivative: + density *= positions / smearing_sq + + return density + + def _compute_close_zero(self, a: float, x: np.ndarray, derivative: bool): + if derivative: + return -1 / (a + 1) + x / (a + 2) + else: + return 1 / a - x / (a + 1) + x**2 / (2 * (a + 2)) + + def _compute_far_zero(self, a: float, x: np.ndarray, derivative: bool): + if derivative: + return ( + np.exp(-x) + - a * scipy.special.gamma(a) * scipy.special.gammainc(a, x) / x**a + ) / x + else: + return scipy.special.gamma(a) * scipy.special.gammainc(a, x) / x**a diff --git a/python/rascaline/rascaline/log.py b/python/featomic/featomic/log.py similarity index 75% rename from python/rascaline/rascaline/log.py rename to python/featomic/featomic/log.py index d543c195a..079d97e8f 100644 --- a/python/rascaline/rascaline/log.py +++ b/python/featomic/featomic/log.py @@ -2,12 +2,12 @@ import warnings from ._c_api import ( - RASCAL_LOG_LEVEL_DEBUG, - RASCAL_LOG_LEVEL_ERROR, - RASCAL_LOG_LEVEL_INFO, - RASCAL_LOG_LEVEL_TRACE, - RASCAL_LOG_LEVEL_WARN, - rascal_logging_callback_t, + FEATOMIC_LOG_LEVEL_DEBUG, + FEATOMIC_LOG_LEVEL_ERROR, + FEATOMIC_LOG_LEVEL_INFO, + FEATOMIC_LOG_LEVEL_TRACE, + FEATOMIC_LOG_LEVEL_WARN, + featomic_logging_callback_t, ) @@ -16,15 +16,15 @@ def default_logging_callback(level, message): """Redirect message to the ``logging`` module.""" - if level == RASCAL_LOG_LEVEL_ERROR: + if level == FEATOMIC_LOG_LEVEL_ERROR: logging.error(message) - elif level == RASCAL_LOG_LEVEL_WARN: + elif level == FEATOMIC_LOG_LEVEL_WARN: logging.warning(message) - elif level == RASCAL_LOG_LEVEL_INFO: + elif level == FEATOMIC_LOG_LEVEL_INFO: logging.info(message) - elif level == RASCAL_LOG_LEVEL_DEBUG: + elif level == FEATOMIC_LOG_LEVEL_DEBUG: logging.debug(message) - elif level == RASCAL_LOG_LEVEL_TRACE: + elif level == FEATOMIC_LOG_LEVEL_TRACE: logging.debug(message) else: raise ValueError(f"Log level {level} is not supported.") @@ -46,7 +46,7 @@ def set_logging_callback(function): def _set_logging_callback_impl(library, function): """Implementation of :py:func:`set_logging_callback` - This function gets the :py:class:`ctypes.CDLL` instance for ``librascaline`` + This function gets the :py:class:`ctypes.CDLL` instance for ``libfeatomic`` as a parameter. This is used to be able to setup the default logging callback when loading @@ -67,6 +67,6 @@ def wrapper(log_level, message): # store the current callback in a global python variable to prevent it from # being garbage-collected. global _CURRENT_CALLBACK - _CURRENT_CALLBACK = rascal_logging_callback_t(wrapper) + _CURRENT_CALLBACK = featomic_logging_callback_t(wrapper) - library.rascal_set_logging_callback(_CURRENT_CALLBACK) + library.featomic_set_logging_callback(_CURRENT_CALLBACK) diff --git a/python/rascaline/rascaline/profiling.py b/python/featomic/featomic/profiling.py similarity index 67% rename from python/rascaline/rascaline/profiling.py rename to python/featomic/featomic/profiling.py index 4c7a003cb..7c4cfdcc3 100644 --- a/python/rascaline/rascaline/profiling.py +++ b/python/featomic/featomic/profiling.py @@ -3,9 +3,9 @@ class Profiler: - """Profiler recording execution time of rascaline functions. + """Profiler recording execution time of featomic functions. - Rascaline uses the `time_graph `_ to collect + Featomic uses the `time_graph `_ to collect timing information on the calculations. The ``Profiler`` class can be used as a context manager to access to this functionality. @@ -15,9 +15,9 @@ class Profiler: .. code-block:: python - import rascaline + import featomic - with rascaline.Profiler() as profiler: + with featomic.Profiler() as profiler: # run some calculations ... @@ -28,23 +28,23 @@ def __init__(self): self._lib = _get_library() def __enter__(self): - self._lib.rascal_profiling_enable(True) - self._lib.rascal_profiling_clear() + self._lib.featomic_profiling_enable(True) + self._lib.featomic_profiling_clear() return self def __exit__(self, exc_type, exc_val, exc_tb): - self._lib.rascal_profiling_enable(False) + self._lib.featomic_profiling_enable(False) def as_json(self): """Get current profiling data formatted as JSON.""" return _call_with_growing_buffer( - lambda b, s: self._lib.rascal_profiling_get("json".encode("utf8"), b, s) + lambda b, s: self._lib.featomic_profiling_get("json".encode("utf8"), b, s) ) def as_table(self): """Get current profiling data formatted as a table.""" return _call_with_growing_buffer( - lambda b, s: self._lib.rascal_profiling_get("table".encode("utf8"), b, s) + lambda b, s: self._lib.featomic_profiling_get("table".encode("utf8"), b, s) ) def as_short_table(self): @@ -52,7 +52,7 @@ def as_short_table(self): Get current profiling data formatted as a table, using short functions names. """ return _call_with_growing_buffer( - lambda b, s: self._lib.rascal_profiling_get( + lambda b, s: self._lib.featomic_profiling_get( "short_table".encode("utf8"), b, s ) ) diff --git a/python/featomic/featomic/splines.py b/python/featomic/featomic/splines.py new file mode 100644 index 000000000..ec4d7fbf8 --- /dev/null +++ b/python/featomic/featomic/splines.py @@ -0,0 +1,817 @@ +import functools +from typing import Callable, Optional + +import numpy as np + + +try: + import scipy.integrate + import scipy.special + + HAS_SCIPY = True +except ImportError: + HAS_SCIPY = False + +from .basis import ExpansionBasis, Explicit, RadialBasis +from .cutoff import Cutoff +from .density import AtomicDensity, DiracDelta, Gaussian, SmearedPowerLaw + + +class Spline: + """ + `Cubic Hermite splines`_ implementation + + .. _Cubic Hermit splines: https://en.wikipedia.org/wiki/Cubic_Hermite_spline + """ + + def __init__(self, start, stop): + self.start = float(start) + self.stop = float(stop) + + self.positions = None + self.values = None + self.derivatives = None + + def __len__(self): + if self.positions is None: + return 0 + else: + return len(self.positions) + + def compute( + self, + positions: np.ndarray, + n: Optional[int] = None, + *, + derivative=False, + ) -> np.ndarray: + """ + Evaluate the spline at the given ``positions``, optionally evaluating only + the value for the ``n``'th splined function. + """ + + if self.positions is None: + raise ValueError("you must add points to the spline before evaluating it") + + if n is None: + n = ... + + x = positions + delta_x = self.positions[1] - self.positions[0] + k = (np.floor(x / delta_x)).astype(np.int64) + + t = (x - k * delta_x) / delta_x + t_2 = t**2 + t_3 = t**3 + + h00 = 2.0 * t_3 - 3.0 * t_2 + 1.0 + h10 = t_3 - 2.0 * t_2 + t + h01 = -2.0 * t_3 + 3.0 * t_2 + h11 = t_3 - t_2 + + p_k = self.values[k, n] + p_k_1 = self.values[k + 1, n] + + m_k = self.derivatives[k, n] + m_k_1 = self.derivatives[k + 1, n] + + new_shape = (-1,) + (1,) * (len(p_k.shape) - 1) + h00 = h00.reshape(new_shape) + h10 = h10.reshape(new_shape) + h01 = h01.reshape(new_shape) + h11 = h11.reshape(new_shape) + + if derivative: + d_h00_dt = 6.0 * (t_2 - t) + d_h10_dt = 3.0 * t_2 - 4.0 * t + 1.0 + d_h01_dt = -d_h00_dt + d_h11_dt = 3.0 * t_2 - 2.0 * t + + dx_dt = 1.0 / delta_x + + return ( + d_h00_dt * p_k * dx_dt + + d_h10_dt * m_k + + d_h01_dt * p_k_1 * dx_dt + + d_h11_dt * m_k_1 + ) + + else: + return h00 * p_k + h10 * delta_x * m_k + h01 * p_k_1 + h11 * delta_x * m_k_1 + + def add_points(self, positions, values, derivatives): + """Add points to the spline + + :param positions: positions of all points + :param values: values of the splined functions at the points positions + :param derivatives: derivative of the splined functions at the points positions + """ + positions = np.asarray(positions) + values = np.asarray(values) + derivatives = np.asarray(derivatives) + + if not np.all(np.isfinite(positions)): + raise ValueError( + "new spline points `positions` contains NaN/infinity, " + "numerical integrals are not converging" + ) + + if not np.all(np.isfinite(values)): + raise ValueError( + "new spline points `values` contains NaN/infinity, " + "numerical integrals are not converging" + ) + + if not np.all(np.isfinite(derivatives)): + raise ValueError( + "new spline points `derivatives` contains NaN/infinity, " + "numerical integrals are not converging" + ) + + assert len(positions.shape) == 1 + assert np.min(positions) >= self.start + assert np.max(positions) <= self.stop + + assert values.shape == derivatives.shape + assert values.shape[0] == len(positions) + assert derivatives.shape[0] == len(positions) + + if self.values is not None: + assert values.shape[1:] == self.values.shape[1:] + assert derivatives.shape[1:] == self.derivatives.shape[1:] + + positions = np.concatenate([self.positions, positions], axis=0) + values = np.concatenate([self.values, values], axis=0) + derivatives = np.concatenate([self.derivatives, derivatives], axis=0) + + sort_indices = np.argsort(positions, axis=0) + self.positions = positions[sort_indices] + self.values = values[sort_indices] + self.derivatives = derivatives[sort_indices] + + assert len(self.values.shape) == 2 + + @staticmethod + def with_accuracy( + start: float, + stop: float, + values_fn: Callable[[np.ndarray], np.ndarray], + derivatives_fn: Callable[[np.ndarray], np.ndarray], + accuracy: float, + ) -> "Spline": + """ + Create a :py:class:`Spline` using any set of functions defined within the + ``start, stop`` interval. The same spline points will be used for all functions, + and more will be added until either the relative error or the absolute error + fall below the requested accuracy on average across all functions. + + The functions are specified via ``values_fn`` and ``derivatives_fn``. These must + take the positions of new spline points as their input, and they must output a + numpy array containing the full set of function evaluated at these points. + """ + # initialize spline with 11 points + spline = Spline(start=start, stop=stop) + positions = np.linspace(start, stop, 11) + spline.add_points(positions, values_fn(positions), derivatives_fn(positions)) + + while True: + n_intermediate_positions = len(spline) - 1 + + if n_intermediate_positions >= 50000: + raise ValueError( + "Maximum number of spline points reached. \ + There might be a problem with the functions to be splined" + ) + + half_step = (spline.positions[1] - spline.positions[0]) / 2 + intermediate_positions = np.linspace( + start + half_step, stop - half_step, n_intermediate_positions + ) + + estimated_values = spline.compute(intermediate_positions) + new_values = values_fn(intermediate_positions) + + mean_absolute_error = np.mean(np.abs(estimated_values - new_values)) + with np.errstate(divide="ignore"): # Ignore divide-by-zero warnings + relative_error = np.abs((estimated_values - new_values) / (new_values)) + mean_relative_error = np.mean( + # exclude points where the denominator is (almost) zero + relative_error[np.where(np.abs(new_values) > 1e-16)] + ) + + if mean_absolute_error < accuracy or mean_relative_error < accuracy: + break + + new_derivatives = derivatives_fn(intermediate_positions) + + spline.add_points(intermediate_positions, new_values, new_derivatives) + + return spline + + +class SplinedRadialBasis(RadialBasis): + """ + Radial basis based on a spline. This is mainly intended to be used to transfer hyper + parameters to the native code, but can also be used to check the exact shape of the + splined radial basis. + """ + + def __init__( + self, + *, + spline: Spline, + max_radial: int, + radius: float, + lode_center_contribution: Optional[np.ndarray] = None, + ): + super().__init__(max_radial=max_radial, radius=radius) + self.spline = spline + self.lode_center_contribution = lode_center_contribution + + def get_hypers(self): + hypers = { + "type": "Tabulated", + "points": [ + { + "position": float(p), + "values": v.tolist(), + "derivatives": d.tolist(), + } + for p, v, d in zip( + self.spline.positions, + self.spline.values, + self.spline.derivatives, + ) + ], + } + + if self.lode_center_contribution is not None: + hypers["center_contribution"] = self.lode_center_contribution.tolist() + + return hypers + + def compute_primitive( + self, positions: np.ndarray, n: int, *, derivative: bool + ) -> np.ndarray: + return self.spline.compute(positions, n, derivative=derivative) + + +def _spherical_bessel_scaled(ell, z): + """Compute ``exp(-z) * spherical_in(ell, z)``, avoiding overflow in + ``spherical_in`` for large values of ``z``""" + one_over_z = np.divide(1.0, z, out=np.zeros_like(z), where=z != 0) + + result = np.sqrt(0.5 * np.pi * one_over_z) * scipy.special.ive(ell + 0.5, z) + + if ell == 0: + return np.where(z == 0, 1.0, result) + else: + return result + + +class SoapSpliner: + """Compute an explicit spline of the radial integral for SOAP calculators. + + This allows a great deal of customization in the radial basis function used (any + child class of :py:class:`RadialBasis`) and atomic density (any child class of + :py:class:`AtomicDensity`). This way, users can define custom densities and/or + basis, and use them with any of the SOAP calculators. For more information about the + radial integral, you can refer to :ref:`this document `. + + This class should be used only in combination with SOAP calculators like + :class:`featomic.SphericalExpansion` or :class:`featomic.SoapPowerSpectrum`. For + k-space spherical expansions use :class:`LodeSpliner`. + + If ``density`` is either :class:`featomic.density.Delta` or + :class:`featomic.density.Gaussian` the radial integral will be partly solved + analytically, for faster and more stable evaluation. + + Example + ------- + + First let's define the hyper parameters for the spherical expansions. It is + important to note that only class-based hyper parameters are supported (in + opposition to ``dict`` based hyper parameters). + + >>> import featomic + + >>> cutoff = featomic.cutoff.Cutoff(radius=2, smoothing=None) + >>> density = featomic.density.Gaussian(width=1.0) + >>> basis = featomic.basis.TensorProduct( + ... max_angular=4, + ... radial=featomic.basis.Gto(max_radial=5, radius=2), + ... # use a reduced ``accuracy`` of ``1e-3`` (the default is ``1e-8``) + ... # to speed up calculations. + ... spline_accuracy=1e-3, + ... ) + + From here we can initialize the spliner instance + + >>> spliner = SoapSpliner(cutoff=cutoff, density=density, basis=basis) + + You can then give the result of :meth:`SoapSpliner.get_hypers()` directly the + calculator: + + >>> calculator = featomic.SphericalExpansion(**spliner.get_hypers()) + + .. seealso:: + :class:`LodeSpliner` for a spliner class that works with + :class:`featomic.LodeSphericalExpansion` + """ + + def __init__( + self, + *, + cutoff: Cutoff, + density: AtomicDensity, + basis: ExpansionBasis, + n_spline_points: Optional[int] = None, + ): + """ + :param cutoff: description of the local atomic environment, defined by a + spherical cutoff + :param density: atomic density that should be expanded for all neighbors in the + local atomic environment + :param basis: basis function to use to expand the neighbors' atomic density. + :param n_spline_points: number of spline points to use. If ``None``, points will + be added to the spline until the accuracy is at least the requested + ``basis.spline_accuracy``. + """ + if not HAS_SCIPY: + raise ImportError("SoapSpliner class requires scipy") + + if not isinstance(cutoff, Cutoff): + raise TypeError(f"`cutoff` should be a `Cutoff` object, got {type(cutoff)}") + + if not isinstance(density, AtomicDensity): + raise TypeError( + f"`density` should be an `AtomicDensity` object, got {type(density)}" + ) + + if not isinstance(basis, ExpansionBasis): + raise TypeError( + f"`basis` should be an `ExpansionBasis object, got {type(basis)}" + ) + + self.cutoff = cutoff + self.density = density + self.basis = basis + + self.n_spline_points = n_spline_points + + def get_hypers(self): + """Get the SOAP hyper-parameters for the splined basis and density.""" + # This class works by computing a spline for the radial integral, and then using + # this spline as a basis in combination with a DiracDelta density. + hypers = { + "cutoff": self.cutoff, + "density": DiracDelta( + center_atom_weight=self.density.center_atom_weight, + scaling=self.density.scaling, + ), + } + + def generic_values_fn(positions, radial_size, angular, orthonormalization): + integrals = np.vstack( + [ + self._radial_integral(radial, angular, positions, derivative=False) + for radial in range(radial_size) + ] + ) + return (orthonormalization @ integrals).T + + def generic_derivatives_fn(positions, radial_size, angular, orthonormalization): + integrals = np.vstack( + [ + self._radial_integral(radial, angular, positions, derivative=True) + for radial in range(radial_size) + ] + ) + return (orthonormalization @ integrals).T + + # We transform whatever basis the user provided to an "explicit" basis, since + # the radial integral will be different for different angular channels. + by_angular = {} + for angular in self.basis.angular_channels(): + radial_basis = self.basis.radial_basis(angular) + orthonormalization = radial_basis._get_orthonormalization_matrix() + + values_fn = functools.partial( + generic_values_fn, + radial_size=radial_basis.size, + angular=angular, + orthonormalization=orthonormalization, + ) + derivatives_fn = functools.partial( + generic_derivatives_fn, + radial_size=radial_basis.size, + angular=angular, + orthonormalization=orthonormalization, + ) + + if self.n_spline_points is not None: + positions = np.linspace(0, self.cutoff.radius, self.n_spline_points) + spline = Spline(0, self.cutoff.radius) + spline.add_points( + positions=positions, + values=values_fn(positions), + derivatives=derivatives_fn(positions), + ) + else: + spline = Spline.with_accuracy( + start=0, + stop=self.cutoff.radius, + values_fn=values_fn, + derivatives_fn=derivatives_fn, + accuracy=( + 1e-8 + if self.basis.spline_accuracy is None + else self.basis.spline_accuracy + ), + ) + + by_angular[angular] = SplinedRadialBasis( + spline=spline, + max_radial=radial_basis.max_radial, + radius=self.cutoff.radius, + ) + + hypers["basis"] = Explicit(by_angular=by_angular, spline_accuracy=None) + return hypers + + def _radial_integral( + self, + radial: int, + angular: int, + distances: np.ndarray, + derivative: bool, + ) -> np.ndarray: + """Compute the SOAP radial integral with a given density and radial basis""" + if isinstance(self.density, DiracDelta): + radial_basis = self.basis.radial_basis(angular) + return radial_basis.compute_primitive( + distances, radial, derivative=derivative + ) + elif isinstance(self.density, Gaussian): + if derivative: + return self._ri_gaussian_density_derivative(radial, angular, distances) + else: + return self._ri_gaussian_density(radial, angular, distances) + else: + return self._ri_custom_density( + radial, angular, distances, derivative=derivative + ) + + def _ri_gaussian_density( + self, + radial: int, + angular: int, + distances: np.ndarray, + ) -> np.ndarray: + # This code is derived in + # https://metatensor.github.io/featomic/latest/devdoc/explanations/radial-integral.html, + # and follows the same naming convention as the documentation. + sigma_sq = self.density.width**2 + + prefactor = ( + (4 * np.pi) + / (np.pi * sigma_sq) ** (3 / 4) + * np.exp(-0.5 * distances**2 / sigma_sq) + ) + + radial_basis = self.basis.radial_basis(angular) + + def integrand(r: float, n: int, ell: int, rij: np.array) -> np.ndarray: + Rnl = radial_basis.compute_primitive(r, n, derivative=False) + + z = r * rij / sigma_sq + bessel = _spherical_bessel_scaled(ell, z) + + return r**2 * Rnl * np.exp(z - 0.5 * r**2 / sigma_sq) * bessel + + integral = scipy.integrate.quad_vec( + f=integrand, + a=0, + b=radial_basis.integration_radius, + args=(radial, angular, distances), + )[0] + return prefactor * integral + + def _ri_gaussian_density_derivative( + self, + radial: int, + angular: int, + distances: np.ndarray, + ) -> np.ndarray: + # This code is derived in + # https://metatensor.github.io/featomic/latest/devdoc/explanations/radial-integral.html, + # and follows the same naming convention as the documentation. + sigma_sq = self.density.width**2 + + prefactor = ( + (4 * np.pi) + / (np.pi * sigma_sq) ** (3 / 4) + * np.exp(-0.5 * distances**2 / sigma_sq) + ) + + radial_basis = self.basis.radial_basis(angular) + + def integrand(r: float, n: int, ell: int, rij: np.array) -> np.ndarray: + Rnl = radial_basis.compute_primitive(r, n, derivative=False) + + z = r * rij / sigma_sq + one_over_z = np.divide(1.0, z, out=np.zeros_like(z), where=z != 0) + + # using recurrence relation from https://dlmf.nist.gov/10.51#E5 for the + # derivative + bessel_ell = _spherical_bessel_scaled(ell, z) + bessel_ell_p1 = _spherical_bessel_scaled(ell + 1, z) + bessel_grad = bessel_ell_p1 + ell * one_over_z * bessel_ell + + # The formula above is wrong for z=0, so let's replace it manually + if ell == 1: + bessel_grad = np.where(z == 0, 1.0 / 3.0, bessel_grad) + + return r**3 * Rnl * np.exp(z - 0.5 * r**2 / sigma_sq) * bessel_grad + + radial_integral = self._ri_gaussian_density(radial, angular, distances) + grad_integral = scipy.integrate.quad_vec( + f=integrand, + a=0, + b=radial_basis.integration_radius, + args=(radial, angular, distances), + )[0] + return (prefactor * grad_integral - distances * radial_integral) / sigma_sq + + def _ri_custom_density( + self, + radial: int, + angular: int, + distances: np.ndarray, + derivative: bool, + ) -> np.ndarray: + # This code is derived in + # https://metatensor.github.io/featomic/latest/devdoc/explanations/radial-integral.html, + # and follows the same naming convention as the documentation. + + P_ell = scipy.special.legendre(angular) + radial_basis = self.basis.radial_basis(angular) + + if derivative: + + def integrand(u: float, r: float, n: int, ell: int, rij: float) -> float: + arg = np.sqrt(r**2 + rij**2 - 2 * r * rij * u) + + Rnl = radial_basis.compute_primitive(r, n, derivative=False) + density_grad = self.density.compute(arg, derivative=True) + + return r**2 * Rnl * P_ell(u) * (rij - u * r) * density_grad / arg + + else: + + def integrand(u: float, r: float, n: int, ell: int, rij: float) -> float: + arg = np.sqrt(r**2 + rij**2 - 2 * r * rij * u) + + Rnl = radial_basis.compute_primitive(r, n, derivative=False) + density = self.density.compute(arg, derivative=False) + + return r**2 * Rnl * P_ell(u) * density + + radial_integral = np.zeros(len(distances)) + + for i, rij in enumerate(distances): + radial_integral[i], _ = scipy.integrate.dblquad( + func=integrand, + # integration bounds for `r`` + a=0, + b=radial_basis.integration_radius, + # integration bounds for `u` + gfun=-1, + hfun=1, + args=(radial, angular, rij), + ) + + return 2 * np.pi * radial_integral + + +class LodeSpliner: + r"""Compute an explicit spline of the radial integral for LODE/k-space calculators. + + This allows a great deal of customization in the radial basis function used (any + child class of :py:class:`RadialBasis`). This way, users can define custom basis, + and use them with the LODE calculators. For more information about the radial + integral, you can refer to :ref:`this document `. + + This class should be used only in combination with k-space/LODE calculators like + :class:`featomic.LodeSphericalExpansion`. For real space spherical expansions you + should use :class:`SoapSpliner`. + + Example + ------- + + First let's define the hyper parameters for the LODE spherical expansions. It is + important to note that only class-based hyper parameters are supported (in + opposition to ``dict`` based hyper parameters). + + >>> import featomic + + >>> density = featomic.density.SmearedPowerLaw(smearing=1.0, exponent=1) + >>> basis = featomic.basis.TensorProduct( + ... max_angular=4, + ... radial=featomic.basis.Gto(max_radial=5, radius=2), + ... ) + + From here we can initialize the spliner instance + + >>> spliner = LodeSpliner(density=density, basis=basis) + + You can then give the result of :meth:`LodeSpliner.get_hypers()` directly the + calculator: + + >>> calculator = featomic.LodeSphericalExpansion(**spliner.get_hypers()) + + .. seealso:: + :class:`SoapSpliner` for a spliner class that works with + :class:`featomic.SphericalExpansion` + """ + + def __init__( + self, + density: AtomicDensity, + basis: ExpansionBasis, + k_cutoff: Optional[float] = None, + n_spline_points: Optional[int] = None, + ): + """ + :param density: atomic density that should be expanded for all neighbors in the + local atomic environment + :param basis: basis function to use to expand the neighbors' atomic density. + Currently only :py:class:`TensorProduct` expansion basis are supported. + :param k_cutoff: Spherical cutoff in reciprocal space. + :param n_spline_points: number of spline points to use. If ``None``, points will + be added to the spline until the accuracy is at least the requested + ``basis.spline_accuracy``. + """ + if not HAS_SCIPY: + raise ImportError("LodeSpliner class requires scipy") + + if not isinstance(density, SmearedPowerLaw): + raise TypeError( + "only `SmearedPowerLaw` `density` is supported by LODE, " + f"got {type(density)}" + ) + + if not isinstance(basis, ExpansionBasis): + raise TypeError( + f"`basis` should be an `ExpansionBasis object, got {type(basis)}" + ) + + if k_cutoff is None: + self.k_cutoff = 1.2 * np.pi / density.smearing + else: + self.k_cutoff = float(k_cutoff) + + self.density = density + self.basis = basis + + self.n_spline_points = n_spline_points + + def get_hypers(self): + """Get the LODE hyper-parameters for the splined basis and density.""" + # Contrary to the SOAP spliner, we don't transform the density to a delta + # density, since the splines are applied directly to k-vectors (and not to the + # density). + hypers = { + "k_cutoff": self.k_cutoff, + "density": self.density, + } + + def generic_values_fn(positions, radial_size, angular, orthonormalization): + integrals = np.vstack( + [ + self._radial_integral(radial, angular, positions, derivative=False) + for radial in range(radial_size) + ] + ) + return (orthonormalization @ integrals).T + + def generic_derivatives_fn(positions, radial_size, angular, orthonormalization): + integrals = np.vstack( + [ + self._radial_integral(radial, angular, positions, derivative=True) + for radial in range(radial_size) + ] + ) + return (orthonormalization @ integrals).T + + # We still transform whatever basis the user provided to an "explicit" basis, + # since the radial integral will be different for different angular channels. + by_angular = {} + for angular in self.basis.angular_channels(): + radial_basis = self.basis.radial_basis(angular) + orthonormalization = radial_basis._get_orthonormalization_matrix() + + values_fn = functools.partial( + generic_values_fn, + radial_size=radial_basis.size, + angular=angular, + orthonormalization=orthonormalization, + ) + derivatives_fn = functools.partial( + generic_derivatives_fn, + radial_size=radial_basis.size, + angular=angular, + orthonormalization=orthonormalization, + ) + + if self.n_spline_points is not None: + positions = np.linspace(0, self.k_cutoff, self.n_spline_points) + spline = Spline(0, self.k_cutoff) + spline.add_points( + positions=positions, + values=values_fn(positions), + derivatives=derivatives_fn(positions), + ) + else: + spline = Spline.with_accuracy( + start=0, + stop=self.k_cutoff, + values_fn=values_fn, + derivatives_fn=derivatives_fn, + accuracy=( + 1e-8 + if self.basis.spline_accuracy is None + else self.basis.spline_accuracy + ), + ) + + if angular == 0: + center_contribution = self._center_contribution() + else: + center_contribution = None + + by_angular[angular] = SplinedRadialBasis( + spline=spline, + max_radial=radial_basis.max_radial, + radius=self.k_cutoff, + lode_center_contribution=center_contribution, + ) + + hypers["basis"] = Explicit(by_angular=by_angular, spline_accuracy=None) + return hypers + + def _radial_integral( + self, + radial: int, + angular: int, + distances: np.ndarray, + derivative: bool, + ) -> np.ndarray: + radial_basis = self.basis.radial_basis(angular) + if derivative: + + def integrand(r: float, n: int, ell: int, rij: np.ndarray) -> np.ndarray: + Rnl = radial_basis.compute_primitive(r, n, derivative=False) + spherical_jn = scipy.special.spherical_jn(ell, r * rij, derivative=True) + return r**3 * Rnl * spherical_jn + + else: + + def integrand(r: float, n: int, ell: int, rij: np.ndarray) -> np.ndarray: + Rnl = radial_basis.compute_primitive(r, n, derivative=False) + spherical_jn = scipy.special.spherical_jn( + ell, r * rij, derivative=False + ) + return r**2 * Rnl * spherical_jn + + return scipy.integrate.quad_vec( + f=integrand, + a=0, + b=radial_basis.integration_radius, + args=(radial, angular, distances), + )[0] + + def _center_contribution(self) -> np.ndarray: + assert isinstance(self.density, SmearedPowerLaw) + + radial_basis = self.basis.radial_basis(angular=0) + + def integrand(r: float, n: int) -> np.ndarray: + return ( + r**2 + * radial_basis.compute_primitive(r, n, derivative=False) + * self.density.compute(r, derivative=False) + ) + + integrals = [] + for radial in range(radial_basis.size): + integrals.append( + scipy.integrate.quad( + func=integrand, + a=0, + b=radial_basis.integration_radius, + args=(radial,), + )[0] + ) + + return np.sqrt(4 * np.pi) * np.array(integrals) diff --git a/python/rascaline/rascaline/status.py b/python/featomic/featomic/status.py similarity index 57% rename from python/rascaline/rascaline/status.py rename to python/featomic/featomic/status.py index c9f07c09a..7159f0994 100644 --- a/python/rascaline/rascaline/status.py +++ b/python/featomic/featomic/status.py @@ -1,9 +1,9 @@ -from ._c_api import RASCAL_SUCCESS +from ._c_api import FEATOMIC_SUCCESS from ._c_lib import _get_library -class RascalError(Exception): - """Exceptions thrown for all errors in rascaline.""" +class FeatomicError(Exception): + """Exceptions thrown for all errors in featomic.""" def __init__(self, message, status=None): super(Exception, self).__init__(message) @@ -23,25 +23,25 @@ def _save_exception(e): LAST_EXCEPTION = e -def _check_rascal_status_t(status): - if status == RASCAL_SUCCESS: +def _check_featomic_status_t(status): + if status == FEATOMIC_SUCCESS: return - elif status > RASCAL_SUCCESS: - raise RascalError(last_error(), status) - elif status < RASCAL_SUCCESS: + elif status > FEATOMIC_SUCCESS: + raise FeatomicError(last_error(), status) + elif status < FEATOMIC_SUCCESS: global LAST_EXCEPTION e = LAST_EXCEPTION LAST_EXCEPTION = None - raise RascalError(last_error(), status) from e + raise FeatomicError(last_error(), status) from e -def _check_rascal_pointer(pointer): +def _check_featomic_pointer(pointer): if not pointer: - raise RascalError(last_error()) + raise FeatomicError(last_error()) def last_error(): """Get the last error message on this thread.""" lib = _get_library() - message = lib.rascal_last_error() + message = lib.featomic_last_error() return message.decode("utf8") diff --git a/python/rascaline/rascaline/systems/__init__.py b/python/featomic/featomic/systems/__init__.py similarity index 83% rename from python/rascaline/rascaline/systems/__init__.py rename to python/featomic/featomic/systems/__init__.py index 6c3d08496..22daca725 100644 --- a/python/rascaline/rascaline/systems/__init__.py +++ b/python/featomic/featomic/systems/__init__.py @@ -6,7 +6,7 @@ class IntoSystem: """ - Possible types that can be used as a rascaline System. + Possible types that can be used as a featomic System. """ def __init__(self): @@ -48,16 +48,16 @@ def wrap_system(system: IntoSystem) -> SystemBase: """Wrap different systems implementation into the right class. This function is automatically called on all systems passed to - :py:func:`rascaline.calculators.CalculatorBase.compute`. This function makes - different systems compatible with rascaline. + :py:func:`featomic.calculators.CalculatorBase.compute`. This function makes + different systems compatible with featomic. The supported system types are documented in the - py:class:`rascaline.IntoSystem` class. If ``system`` is already a subclass - of :py:class:`rascaline.SystemBase`, it is returned as-is. + py:class:`featomic.IntoSystem` class. If ``system`` is already a subclass + of :py:class:`featomic.SystemBase`, it is returned as-is. :param system: external system to wrap - :returns: a specialized instance of :py:class:`rascaline.SystemBase` + :returns: a specialized instance of :py:class:`featomic.SystemBase` """ if isinstance(system, SystemBase): return system diff --git a/python/rascaline/rascaline/systems/ase.py b/python/featomic/featomic/systems/ase.py similarity index 98% rename from python/rascaline/rascaline/systems/ase.py rename to python/featomic/featomic/systems/ase.py index 7870d3c8f..6774e6d6b 100644 --- a/python/rascaline/rascaline/systems/ase.py +++ b/python/featomic/featomic/systems/ase.py @@ -16,7 +16,7 @@ class AseSystem(SystemBase): - """Implements :py:class:`rascaline.SystemBase` using `ase.Atoms`_. + """Implements :py:class:`featomic.SystemBase` using `ase.Atoms`_. Gets the data and `ase.neighborlist.neighbor_list`_ to compute the neighbor list. diff --git a/python/rascaline/rascaline/systems/base.py b/python/featomic/featomic/systems/base.py similarity index 80% rename from python/rascaline/rascaline/systems/base.py rename to python/featomic/featomic/systems/base.py index 377f8688d..73e555d8a 100644 --- a/python/rascaline/rascaline/systems/base.py +++ b/python/featomic/featomic/systems/base.py @@ -3,7 +3,7 @@ import numpy as np -from .._c_api import c_uintptr_t, rascal_pair_t, rascal_system_t +from .._c_api import c_uintptr_t, featomic_pair_t, featomic_system_t from ..status import _save_exception @@ -22,25 +22,25 @@ def inner(*args, **kwargs): class SystemBase: - """Base class implementing the ``System`` trait in rascaline. + """Base class implementing the ``System`` trait in featomic. Developers should implement this class to add new kinds of system that work with - rascaline. + featomic. Most users should use one of the already provided implementation, such as - :py:class:`rascaline.systems.AseSystem` or - :py:class:`rascaline.systems.ChemfilesSystem` instead of using this class directly. + :py:class:`featomic.systems.AseSystem` or + :py:class:`featomic.systems.ChemfilesSystem` instead of using this class directly. A very simple implementation of the interface is given below as starting point. The example does not implement a neighbor list, and it can only be used by setting ``use_native_system=True`` in - :py:meth:`rascaline.calculators.CalculatorBase.compute`, to transfer the data to the + :py:meth:`featomic.calculators.CalculatorBase.compute`, to transfer the data to the native code and compute the neighbors list there. - >>> import rascaline + >>> import featomic >>> import numpy as np >>> - >>> class SimpleSystem(rascaline.systems.SystemBase): + >>> class SimpleSystem(featomic.systems.SystemBase): ... def __init__(self, types, positions, cell): ... super().__init__() ... @@ -86,14 +86,13 @@ class SystemBase: ... ... def pairs_containing(self, atom): ... raise NotImplementedError("this system does not have a neighbors list") - ... >>> system = SimpleSystem( ... types=np.random.randint(2, size=25, dtype=np.int32), ... positions=6 * np.random.uniform(size=(25, 3)), ... cell=6 * np.eye(3), ... ) >>> - >>> calculator = rascaline.SortedDistances( + >>> calculator = featomic.SortedDistances( ... cutoff=3.3, ... max_neighbors=4, ... separate_neighbor_types=True, @@ -110,9 +109,8 @@ class SystemBase: >>> # this does not work, since the code is trying to get a neighbors list >>> try: ... calculator.compute(system, use_native_system=False) - ... except rascaline.RascalError as e: + ... except featomic.FeatomicError as e: ... raise e.__cause__ - ... Traceback (most recent call last): ... NotImplementedError: this system does not have a neighbors list @@ -123,12 +121,12 @@ def __init__(self): # might be using the data self._keepalive = {} - def _as_rascal_system_t(self): + def _as_featomic_system_t(self): """Convert a child instance of :py:class:`SystemBase`. - Instances are converted to a C compatible ``rascal_system_t``. + Instances are converted to a C compatible ``featomic_system_t``. """ - struct = rascal_system_t() + struct = featomic_system_t() self._keepalive["c_struct"] = struct # user_data is a pointer to the PyObject `self` @@ -141,20 +139,20 @@ def get_self(ptr): return self @catch_exceptions - def rascal_system_size(user_data, size): + def featomic_system_size(user_data, size): """ - Implementation of ``rascal_system_t::size`` using + Implementation of ``featomic_system_t::size`` using :py:func:`SystemBase.size`. """ size[0] = c_uintptr_t(get_self(user_data).size()) # use struct.XXX.__class__ to get the right type for all functions - struct.size = struct.size.__class__(rascal_system_size) + struct.size = struct.size.__class__(featomic_system_size) @catch_exceptions - def rascal_system_types(user_data, data): + def featomic_system_types(user_data, data): """ - Implementation of ``rascal_system_t::types`` using + Implementation of ``featomic_system_t::types`` using :py:func:`SystemBase.types`. """ self = get_self(user_data) @@ -163,12 +161,12 @@ def rascal_system_types(user_data, data): data[0] = types.ctypes.data self._keepalive["types"] = types - struct.types = struct.types.__class__(rascal_system_types) + struct.types = struct.types.__class__(featomic_system_types) @catch_exceptions - def rascal_system_positions(user_data, data): + def featomic_system_positions(user_data, data): """ - Implementation of ``rascal_system_t::positions`` using + Implementation of ``featomic_system_t::positions`` using :py:func:`SystemBase.positions`. """ self = get_self(user_data) @@ -180,12 +178,12 @@ def rascal_system_positions(user_data, data): data[0] = positions.ctypes.data self._keepalive["positions"] = positions - struct.positions = struct.positions.__class__(rascal_system_positions) + struct.positions = struct.positions.__class__(featomic_system_positions) @catch_exceptions - def rascal_system_cell(user_data, data): + def featomic_system_cell(user_data, data): """ - Implementation of ``rascal_system_t::cell`` using + Implementation of ``featomic_system_t::cell`` using :py:func:`SystemBase.cell`. """ self = get_self(user_data) @@ -202,25 +200,25 @@ def rascal_system_cell(user_data, data): data[7] = cell[2][1] data[8] = cell[2][2] - struct.cell = struct.cell.__class__(rascal_system_cell) + struct.cell = struct.cell.__class__(featomic_system_cell) @catch_exceptions - def rascal_system_compute_neighbors(user_data, cutoff): + def featomic_system_compute_neighbors(user_data, cutoff): """ - Implementation of ``rascal_system_t::compute_neighbors`` using + Implementation of ``featomic_system_t::compute_neighbors`` using :py:func:`SystemBase.compute_neighbors`. """ self = get_self(user_data) self.compute_neighbors(cutoff) struct.compute_neighbors = struct.compute_neighbors.__class__( - rascal_system_compute_neighbors + featomic_system_compute_neighbors ) @catch_exceptions - def rascal_system_pairs(user_data, data, count): + def featomic_system_pairs(user_data, data, count): """ - Implementation of ``rascal_system_t::pairs`` using + Implementation of ``featomic_system_t::pairs`` using :py:func:`SystemBase.pairs`. """ self = get_self(user_data) @@ -228,19 +226,19 @@ def rascal_system_pairs(user_data, data, count): pairs = np.asarray( self.pairs(), order="C", - dtype=rascal_pair_t, + dtype=featomic_pair_t, ) count[0] = c_uintptr_t(len(pairs)) data[0] = pairs.ctypes.data self._keepalive["pairs"] = pairs - struct.pairs = struct.pairs.__class__(rascal_system_pairs) + struct.pairs = struct.pairs.__class__(featomic_system_pairs) @catch_exceptions - def rascal_system_pairs_containing(user_data, atom, data, count): + def featomic_system_pairs_containing(user_data, atom, data, count): """ - Implementation of ``rascal_system_t::pairs_containing`` using + Implementation of ``featomic_system_t::pairs_containing`` using :py:func:`SystemBase.pairs_containing`. """ self = get_self(user_data) @@ -248,7 +246,7 @@ def rascal_system_pairs_containing(user_data, atom, data, count): pairs = np.asarray( self.pairs_containing(atom), order="C", - dtype=rascal_pair_t, + dtype=featomic_pair_t, ) count[0] = c_uintptr_t(len(pairs)) @@ -256,7 +254,7 @@ def rascal_system_pairs_containing(user_data, atom, data, count): self._keepalive["pairs_containing"] = pairs struct.pairs_containing = struct.pairs_containing.__class__( - rascal_system_pairs_containing + featomic_system_pairs_containing ) return struct @@ -302,8 +300,8 @@ def cell(self): def compute_neighbors(self, cutoff): """Compute the neighbor list with the given ``cutoff``. - Store it for later access using :py:func:`rascaline.SystemBase.pairs` or - :py:func:`rascaline.SystemBase.pairs_containing`. + Store it for later access using :py:func:`featomic.SystemBase.pairs` or + :py:func:`featomic.SystemBase.pairs_containing`. """ raise NotImplementedError("System.compute_neighbors method is not implemented") @@ -320,15 +318,15 @@ def pairs(self): them, and the cell shift. The vector should be ``position[first] - position[second] * + H * cell_shift`` where ``H`` is the cell matrix. Alternatively, this function can return a 1D numpy array with - ``dtype=rascal_pair_t``. + ``dtype=featomic_pair_t``. The list of pair should only contain each pair once (and not twice as ``i-j`` and ``j-i``), should not contain self pairs (``i-i``); and should only contains pairs where the distance between atoms is actually bellow the cutoff passed in - the last call to :py:func:`rascaline.SystemBase.compute_neighbors`. + the last call to :py:func:`featomic.SystemBase.compute_neighbors`. This function is only valid to call after a call to - :py:func:`rascaline.SystemBase.compute_neighbors` to set the cutoff. + :py:func:`featomic.SystemBase.compute_neighbors` to set the cutoff. """ raise NotImplementedError("System.pairs method is not implemented") @@ -339,7 +337,7 @@ def pairs_containing(self, atom): ``atom``. The return type of this function should be the same as - :py:func:`rascaline.SystemBase.pairs`. The same restrictions on the list + :py:func:`featomic.SystemBase.pairs`. The same restrictions on the list of pairs also applies, with the additional condition that the pair ``i-j`` should be included both in the list returned by ``pairs_containing(i)`` and ``pairs_containing(j)``. diff --git a/python/rascaline/rascaline/systems/chemfiles.py b/python/featomic/featomic/systems/chemfiles.py similarity index 95% rename from python/rascaline/rascaline/systems/chemfiles.py rename to python/featomic/featomic/systems/chemfiles.py index fe918ab3e..54ef366a4 100644 --- a/python/rascaline/rascaline/systems/chemfiles.py +++ b/python/featomic/featomic/systems/chemfiles.py @@ -39,11 +39,11 @@ def get_type_for_non_element(name): class ChemfilesSystem(SystemBase): - """Implements :py:class:`rascaline.SystemBase` wrapping a `chemfiles.Frame`_. + """Implements :py:class:`featomic.SystemBase` wrapping a `chemfiles.Frame`_. Since chemfiles does not offer a neighbors list, this implementation of system can only be used with ``use_native_system=True`` in - :py:func:`rascaline.calculators.CalculatorBase.compute`. + :py:func:`featomic.calculators.CalculatorBase.compute`. Atomic type are assigned as the atomic number if the atom ``type`` is one of the periodic table elements; or as a value above 120 if the atom type is not in the diff --git a/python/rascaline/rascaline/systems/pyscf.py b/python/featomic/featomic/systems/pyscf.py similarity index 81% rename from python/rascaline/rascaline/systems/pyscf.py rename to python/featomic/featomic/systems/pyscf.py index 22747e2b9..5f3309b23 100644 --- a/python/rascaline/rascaline/systems/pyscf.py +++ b/python/featomic/featomic/systems/pyscf.py @@ -34,19 +34,19 @@ def _std_symbol_without_ghost(symb_or_chg): class PyscfSystem(SystemBase): - """Implements :py:class:`rascaline.SystemBase` wrapping a + """Implements :py:class:`featomic.SystemBase` wrapping a `pyscf.gto.mole.Mole`_ or `pyscf.pbc.gto.cell.Cell`_. Since pyscf does not offer a neighbors list, this implementation of system can only be used with ``use_native_system=True`` in - :py:func:`rascaline.calculators.CalculatorBase.compute`. + :py:func:`featomic.calculators.CalculatorBase.compute`. Atomic type are assigned as the atomic number if the atom ``type`` is one of the periodic table elements; or their opposite if they are ghost atoms. (Pyscf does not seem to support anything else) Please note that while pyscf uses Bohrs as length units internally, we convert those - back into Angströms for rascaline. A pyscf object's "unit" attribute determines the + back into Angströms for featomic. A pyscf object's "unit" attribute determines the units of the coordinates given *to pyscf*, which are by default angströms. .. _pyscf.gto.mole.Mole: https://pyscf.org/user/gto.html @@ -61,43 +61,42 @@ def can_wrap(o): else: return isinstance(o, pyscf.gto.mole.Mole) - def __init__(self, frame): + def __init__(self, system): """ - :param frame : `chemfiles.Frame`_ object object to be wrapped - in this ``ChemfilesSystem`` + :param system : PySCF object object to be wrapped in this ``PyscfSystem`` """ super().__init__() - if not self.can_wrap(frame): + if not self.can_wrap(system): raise Exception( "this class expects pyscf.gto.mole.Mole" + "or pyscf.pbc.gto.cell.Cell objects" ) - self._frame = frame - self._types = self._frame.atom_charges().copy() # dtype=int32 + self._system = system + self._types = self._system.atom_charges().copy() # dtype=int32 for atm_i, atomic_type in enumerate(self._types): if atomic_type == 0: - symb = self._frame.atom_symbol(atm_i) + symb = self._system.atom_symbol(atm_i) chg = pyscf.data.elements.index(symb) self._types[atm_i] = -chg if hasattr(pyscf, "pbc"): - self.is_periodic = isinstance(self._frame, pyscf.pbc.gto.cell.Cell) + self.is_periodic = isinstance(self._system, pyscf.pbc.gto.cell.Cell) else: self.is_periodic = False def size(self): - return self._frame.natm + return self._system.natm def types(self): return self._types def positions(self): - return pyscf.data.nist.BOHR * self._frame.atom_coords() + return self._system.atom_coords("angstrom") def cell(self): if self.is_periodic: - cell = self._frame.a - if self._frame.unit[0].lower() == "a": + cell = self._system.a + if self._system.unit[0].lower() == "a": # assume angströms, we are good to go return cell else: diff --git a/python/featomic/featomic/utils.py b/python/featomic/featomic/utils.py new file mode 100644 index 000000000..0d5fc8aa3 --- /dev/null +++ b/python/featomic/featomic/utils.py @@ -0,0 +1,9 @@ +import os + + +_HERE = os.path.dirname(__file__) + +cmake_prefix_path = os.path.realpath(os.path.join(_HERE, "lib", "cmake")) +""" +Path containing the CMake configuration files for the underlying C library +""" diff --git a/python/featomic/featomic/version.py b/python/featomic/featomic/version.py new file mode 100644 index 000000000..2640726bb --- /dev/null +++ b/python/featomic/featomic/version.py @@ -0,0 +1,4 @@ +import importlib.metadata + + +__version__ = importlib.metadata.version("featomic") diff --git a/python/featomic/pyproject.toml b/python/featomic/pyproject.toml new file mode 100644 index 000000000..afc6c71b5 --- /dev/null +++ b/python/featomic/pyproject.toml @@ -0,0 +1,65 @@ +[project] +name = "featomic" +dynamic = ["version", "authors", "optional-dependencies"] +requires-python = ">=3.9" + +readme = "README.rst" +license = {text = "BSD-3-Clause"} +description = "Computing representations for atomistic machine learning" + +keywords = ["computational science", "machine learning", "molecular modeling", "atomistic representations"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Operating System :: POSIX", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Bio-Informatics", + "Topic :: Scientific/Engineering :: Chemistry", + "Topic :: Scientific/Engineering :: Physics", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", +] + +dependencies = [ + "metatensor-core >=0.1.0,<0.2.0", + "metatensor-operations >=0.3.0,<0.4.0", + "wigners", +] + +[project.urls] +homepage = "https://metatensor.github.io/featomic/latest/" +documentation = "https://metatensor.github.io/featomic/latest/" +repository = "https://github.com/metatensor/featomic" +# changelog = "TODO" + +### ======================================================================== ### + +[build-system] +requires = [ + "setuptools", + "wheel", + "packaging", +] + +# use a custom build backend to add a dependency on metatensor/cmake only when +# building the wheels +build-backend = "backend" +backend-path = ["build-backend"] + +[tool.setuptools] +zip-safe = true + +[tool.setuptools.packages.find] +include = ["featomic*"] +namespaces = false + +### ======================================================================== ### + +[tool.pytest.ini_options] +python_files = ["*.py"] +testpaths = ["tests"] diff --git a/python/featomic/setup.py b/python/featomic/setup.py new file mode 100644 index 000000000..617cfa7f0 --- /dev/null +++ b/python/featomic/setup.py @@ -0,0 +1,328 @@ +import glob +import os +import subprocess +import sys + +import packaging +from setuptools import Extension, setup +from setuptools.command.bdist_egg import bdist_egg +from setuptools.command.build_ext import build_ext +from setuptools.command.sdist import sdist +from wheel.bdist_wheel import bdist_wheel + + +ROOT = os.path.realpath(os.path.dirname(__file__)) +FEATOMIC_SRC = os.path.realpath(os.path.join(ROOT, "..", "..", "featomic")) + +FEATOMIC_BUILD_TYPE = os.environ.get("FEATOMIC_BUILD_TYPE", "release") +if FEATOMIC_BUILD_TYPE not in ["debug", "release"]: + raise Exception( + f"invalid build type passed: '{FEATOMIC_BUILD_TYPE}'," + "expected 'debug' or 'release'" + ) + +# The code for featomic-torch needs to live in `featomic_torch` directory until +# https://github.com/pypa/pip/issues/13093 is fixed. +FEATOMIC_TORCH_SRC = os.path.realpath(os.path.join(ROOT, "..", "featomic_torch")) + + +class universal_wheel(bdist_wheel): + # When building the wheel, the `wheel` package assumes that if we have a + # binary extension then we are linking to `libpython.so`; and thus the wheel + # is only usable with a single python version. This is not the case for + # here, and the wheel will be compatible with any Python >=3. This is + # tracked in https://github.com/pypa/wheel/issues/185, but until then we + # manually override the wheel tag. + def get_tag(self): + tag = bdist_wheel.get_tag(self) + # tag[2:] contains the os/arch tags, we want to keep them + return ("py3", "none") + tag[2:] + + +class cmake_ext(build_ext): + """Build the native library using cmake.""" + + def run(self): + """Run cmake build and install the resulting library.""" + import metatensor + + source_dir = FEATOMIC_SRC + build_dir = os.path.join(ROOT, "build", "cmake-build") + install_dir = os.path.join(os.path.realpath(self.build_lib), "featomic") + + try: + os.mkdir(build_dir) + except OSError: + pass + + cmake_options = [ + f"-DCMAKE_INSTALL_PREFIX={install_dir}", + "-DCMAKE_INSTALL_LIBDIR=lib", + f"-DCMAKE_BUILD_TYPE={FEATOMIC_BUILD_TYPE}", + f"-DCMAKE_PREFIX_PATH={metatensor.utils.cmake_prefix_path}", + "-DFEATOMIC_INSTALL_BOTH_STATIC_SHARED=OFF", + "-DBUILD_SHARED_LIBS=ON", + "-DEXTRA_RUST_FLAGS=-Cstrip=symbols", + ] + + if "CARGO" in os.environ: + cmake_options.append(f"-DCARGO_EXE={os.environ['CARGO']}") + + # Handle cross-compilation by detecting cibuildwheels environnement + # variables + if sys.platform.startswith("darwin"): + # ARCHFLAGS is set by cibuildwheels + ARCHFLAGS = os.environ.get("ARCHFLAGS") + if ARCHFLAGS is not None: + archs = filter( + lambda u: bool(u), + ARCHFLAGS.strip().split("-arch "), + ) + archs = list(archs) + assert len(archs) == 1 + arch = archs[0].strip() + + if arch == "x86_64": + cmake_options.append("-DRUST_BUILD_TARGET=x86_64-apple-darwin") + elif arch == "arm64": + cmake_options.append("-DRUST_BUILD_TARGET=aarch64-apple-darwin") + else: + raise ValueError(f"unknown arch: {arch}") + + elif sys.platform.startswith("linux"): + # we set RUST_BUILD_TARGET in our custom docker image + RUST_BUILD_TARGET = os.environ.get("RUST_BUILD_TARGET") + if RUST_BUILD_TARGET is not None: + cmake_options.append(f"-DRUST_BUILD_TARGET={RUST_BUILD_TARGET}") + + elif sys.platform.startswith("win32"): + # CARGO_BUILD_TARGET is set by cibuildwheels + CARGO_BUILD_TARGET = os.environ.get("CARGO_BUILD_TARGET") + if CARGO_BUILD_TARGET is not None: + cmake_options.append(f"-DRUST_BUILD_TARGET={CARGO_BUILD_TARGET}") + + else: + raise ValueError(f"unknown platform: {sys.platform}") + + subprocess.run( + ["cmake", source_dir, *cmake_options], + cwd=build_dir, + check=True, + ) + subprocess.run( + ["cmake", "--build", build_dir, "--parallel", "--target", "install"], + check=True, + ) + + +class bdist_egg_disabled(bdist_egg): + """Disabled version of bdist_egg + + Prevents setup.py install performing setuptools' default easy_install, + which it should never ever do. + """ + + def run(self): + sys.exit( + "Aborting implicit building of eggs. " + + "Use `pip install .` or `python setup.py bdist_wheel && pip " + + "uninstall featomic -y && pip install dist/featomic-*.whl` " + + "to install from source." + ) + + +class sdist_generate_data(sdist): + """ + Create a sdist with an additional generated files: + - `git_version_info` + - `featomic-cxx-*.tar.gz` + """ + + def run(self): + n_commits, git_hash = git_version_info() + with open("git_version_info", "w") as fd: + fd.write(f"{n_commits}\n{git_hash}\n") + + generate_cxx_tar() + + # run original sdist + super().run() + + os.unlink("git_version_info") + for path in glob.glob("featomic-cxx-*.tar.gz"): + os.unlink(path) + + +def generate_cxx_tar(): + script = os.path.join(ROOT, "..", "..", "scripts", "package-featomic.sh") + assert os.path.exists(script) + + try: + output = subprocess.run( + ["bash", "--version"], + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + encoding="utf8", + ) + except Exception as e: + raise RuntimeError("could not run `bash`, is it installed?") from e + + output = subprocess.run( + ["bash", script, os.getcwd()], + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + encoding="utf8", + ) + if output.returncode != 0: + stderr = output.stderr + stdout = output.stdout + raise RuntimeError( + "failed to collect C++ sources for Python sdist\n" + f"stdout:\n {stdout}\n\nstderr:\n {stderr}" + ) + + +def get_rust_version(): + # read version from Cargo.toml + with open(os.path.join(FEATOMIC_SRC, "Cargo.toml")) as fd: + for line in fd: + if line.startswith("version"): + _, version = line.split(" = ") + # remove quotes + version = version[1:-2] + # take the first version in the file, this should be the right + # version + break + + return version + + +def git_version_info(): + """ + If git is available and we are building from a checkout, get the number of commits + since the last tag & full hash of the code. Otherwise, this always returns (0, ""). + """ + TAG_PREFIX = "featomic-v" + + if os.path.exists("git_version_info"): + # we are building from a sdist, without git available, but the git + # version was recorded in the `git_version_info` file + with open("git_version_info") as fd: + n_commits = int(fd.readline().strip()) + git_hash = fd.readline().strip() + else: + script = os.path.join(ROOT, "..", "..", "scripts", "git-version-info.py") + assert os.path.exists(script) + + output = subprocess.run( + [sys.executable, script, TAG_PREFIX], + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + encoding="utf8", + ) + + if output.returncode != 0: + raise Exception( + "failed to get git version info.\n" + f"stdout: {output.stdout}\n" + f"stderr: {output.stderr}\n" + ) + elif output.stderr: + print(output.stderr, file=sys.stderr) + n_commits = 0 + git_hash = "" + else: + lines = output.stdout.splitlines() + n_commits = int(lines[0].strip()) + git_hash = lines[1].strip() + + return n_commits, git_hash + + +def create_version_number(version): + version = packaging.version.parse(version) + + n_commits, git_hash = git_version_info() + if n_commits != 0: + # `n_commits` will be non zero only if we have commits since the last tag. This + # mean we are in a pre-release of the next version. So we increase either the + # minor version number or the release candidate number (if we are closing up on + # a release) + if version.pre is not None: + assert version.pre[0] == "rc" + pre = ("rc", version.pre[1] + 1) + release = version.release + else: + major, minor, patch = version.release + release = (major, minor + 1, 0) + pre = None + + # this is using a private API which is intended to become public soon: + # https://github.com/pypa/packaging/pull/698. In the mean time we'll + # use this + version._version = version._version._replace(release=release) + version._version = version._version._replace(pre=pre) + version._version = version._version._replace(dev=("dev", n_commits)) + version._version = version._version._replace(local=(git_hash,)) + + return str(version) + + +if __name__ == "__main__": + if not os.path.exists(FEATOMIC_SRC): + # we are building from a sdist, which should include featomic Rust + # sources as a tarball + tarballs = glob.glob(os.path.join(ROOT, "featomic-*.tar.gz")) + + if not len(tarballs) == 1: + raise RuntimeError( + "expected a single 'featomic-*.tar.gz' file containing " + "featomic Rust sources. remove all files and re-run " + "scripts/package-featomic.sh" + ) + + FEATOMIC_SRC = os.path.realpath(tarballs[0]) + subprocess.run( + ["cmake", "-E", "tar", "xf", FEATOMIC_SRC], + cwd=ROOT, + check=True, + ) + + FEATOMIC_SRC = ".".join(FEATOMIC_SRC.split(".")[:-2]) + + with open(os.path.join(ROOT, "AUTHORS")) as fd: + authors = fd.read().splitlines() + + extras_require = {} + + # when packaging a sdist for release, we should never use local dependencies + FEATOMIC_NO_LOCAL_DEPS = os.environ.get("FEATOMIC_NO_LOCAL_DEPS", "0") == "1" + if not FEATOMIC_NO_LOCAL_DEPS and os.path.exists(FEATOMIC_TORCH_SRC): + # we are building from a git checkout + extras_require["torch"] = f"featomic-torch @ file://{FEATOMIC_TORCH_SRC}" + else: + # we are building from a sdist/installing from a wheel + extras_require["torch"] = "featomic-torch" + + setup( + version=create_version_number(get_rust_version()), + author=", ".join(authors), + extras_require=extras_require, + ext_modules=[ + # only declare the extension, it is built & copied as required by cmake + # in the build_ext command + Extension(name="featomic", sources=[]), + ], + cmdclass={ + "build_ext": cmake_ext, + "bdist_egg": bdist_egg if "bdist_egg" in sys.argv else bdist_egg_disabled, + "bdist_wheel": universal_wheel, + "sdist": sdist_generate_data, + }, + package_data={ + "featomic": [ + "featomic/lib/*", + "featomic/include/*", + ] + }, + ) diff --git a/python/rascaline/tests/__init__.py b/python/featomic/tests/__init__.py similarity index 100% rename from python/rascaline/tests/__init__.py rename to python/featomic/tests/__init__.py diff --git a/python/featomic/tests/basis.py b/python/featomic/tests/basis.py new file mode 100644 index 000000000..ef113f015 --- /dev/null +++ b/python/featomic/tests/basis.py @@ -0,0 +1,117 @@ +import numpy as np +import pytest + +from featomic import hypers_to_json +from featomic.basis import ( + Gto, + LaplacianEigenstate, + Monomials, + RadialBasis, + SphericalBessel, +) + + +pytest.importorskip("scipy") + + +class RtoNRadialBasis(RadialBasis): + def __init__(self, max_radial, radius): + super().__init__(max_radial=max_radial, radius=radius) + + def compute_primitive( + self, positions: np.ndarray, n: int, *, derivative=False + ) -> np.ndarray: + assert not derivative + return positions**n + + +def test_radial_basis_gram(): + """Test that quad integration of the gram matrix is the same as an analytical.""" + + radius = 1.5 + max_radial = 4 + + test_basis = RtoNRadialBasis(max_radial=max_radial, radius=radius) + numerical_gram = test_basis._get_gram_matrix() + + analytical_gram = np.zeros_like(numerical_gram) + for n1 in range(max_radial + 1): + for n2 in range(max_radial + 1): + exponent = 3 + n1 + n2 + analytical_gram[n1, n2] = radius**exponent / exponent + + np.testing.assert_allclose(numerical_gram, analytical_gram, atol=1e-12) + + +def test_radial_basis_orthornormalization(): + radius = 1.5 + max_radial = 4 + + test_basis = RtoNRadialBasis(max_radial=max_radial, radius=radius) + + gram = test_basis._get_gram_matrix() + ortho = test_basis._get_orthonormalization_matrix() + + np.testing.assert_allclose( + ortho @ gram @ ortho.T, np.eye(max_radial + 1), atol=1e-9 + ) + + +# Define a helper class that used the numerical derivatives from `RadialBasis` +# instead of the explicitly implemented analytical ones in the child classes. +class NumericalRadialBasis(RadialBasis): + def __init__(self, basis): + super().__init__(max_radial=basis.max_radial, radius=basis.radius) + self.basis = basis + + def compute_primitive( + self, n: int, positions: np.ndarray, *, derivative: bool + ) -> np.ndarray: + if derivative: + return self.basis.finite_differences_derivative(n, positions) + else: + return self.basis.compute_primitive(n, positions, derivative=False) + + +@pytest.mark.parametrize( + "analytical_basis", + [ + Gto(radius=4, max_radial=6), + Monomials(radius=4, max_radial=6, angular_channel=3), + SphericalBessel(radius=4, max_radial=6, angular_channel=3), + ], +) +def test_derivative(analytical_basis): + """Finite difference test for testing the derivative of a radial basis""" + + numerical_basis = NumericalRadialBasis(analytical_basis) + + positions = np.linspace(0.5, analytical_basis.radius) + for n in range(analytical_basis.size): + np.testing.assert_allclose( + numerical_basis.compute_primitive(positions, n, derivative=True), + analytical_basis.compute_primitive(positions, n, derivative=True), + atol=1e-6, + ) + + +def test_le_basis(): + basis = LaplacianEigenstate(max_radial=4, radius=3.4) + assert basis.max_angular == 10 + assert basis.angular_channels() == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + for angular in basis.angular_channels(): + radial = basis.radial_basis(angular) + assert radial.max_radial <= 4 + + basis = LaplacianEigenstate(max_radial=4, max_angular=4, radius=3.4) + assert basis.max_angular == 4 + assert basis.angular_channels() == [0, 1, 2, 3, 4] + + message = ( + "This radial basis function \\(SphericalBessel\\) does not have matching " + "hyper parameters in the native calculators. It should be used through one of " + "the spliner class instead of directly" + ) + with pytest.raises(NotImplementedError, match=message): + hypers_to_json(basis.get_hypers()) diff --git a/python/rascaline/tests/calculators/__init__.py b/python/featomic/tests/calculators/__init__.py similarity index 100% rename from python/rascaline/tests/calculators/__init__.py rename to python/featomic/tests/calculators/__init__.py diff --git a/python/rascaline/tests/calculators/dummy_calculator.py b/python/featomic/tests/calculators/dummy_calculator.py similarity index 96% rename from python/rascaline/tests/calculators/dummy_calculator.py rename to python/featomic/tests/calculators/dummy_calculator.py index 5d8fcbebb..da4921c9f 100644 --- a/python/rascaline/tests/calculators/dummy_calculator.py +++ b/python/featomic/tests/calculators/dummy_calculator.py @@ -1,8 +1,8 @@ import numpy as np import pytest -from rascaline import RascalError -from rascaline.calculators import DummyCalculator +from featomic import FeatomicError +from featomic.calculators import DummyCalculator from ..test_systems import SystemForTests @@ -33,7 +33,7 @@ def test_bad_parameters(): 'json error: invalid type: string "12", expected isize at line 1 column 29' ) - with pytest.raises(RascalError, match=message): + with pytest.raises(FeatomicError, match=message): _ = DummyCalculator(cutoff=3.2, delta="12", name="foo") diff --git a/python/featomic/tests/calculators/hypers.py b/python/featomic/tests/calculators/hypers.py new file mode 100644 index 000000000..1dfeed098 --- /dev/null +++ b/python/featomic/tests/calculators/hypers.py @@ -0,0 +1,231 @@ +from typing import List + +import metatensor +import pytest + +import featomic +from featomic.calculators import ( + LodeSphericalExpansion, + SoapPowerSpectrum, + SoapRadialSpectrum, + SphericalExpansion, + SphericalExpansionByPair, +) + +from ..test_systems import SystemForTests + + +@pytest.mark.parametrize( + "CalculatorClass", + [ + SphericalExpansion, + SphericalExpansionByPair, + SoapPowerSpectrum, + SoapRadialSpectrum, + ], +) +def test_soap_hypers(CalculatorClass): + message = ( + "hyper parameter changed recently, please update your code. " + "Here are the new equivalent parameters" + ) + with pytest.raises(ValueError, match=message) as err: + CalculatorClass( + atomic_gaussian_width=0.3, + center_atom_weight=1.0, + cutoff=3.4, + cutoff_function={"ShiftedCosine": {"width": 0.5}}, + max_angular=5, + max_radial=3, + radial_basis={"Gto": {"spline_accuracy": 1e-3}}, + radial_scaling={"Willatt2018": {"exponent": 3, "rate": 2.2, "scale": 1.1}}, + ) + + error_message = str(err.value.args[0]) + first_line = error_message.find("\n") + code = error_message[first_line:] + + # max radial meaning changed + assert '"max_radial": 2' in code + + # check that the error message contains valid code that can be copy/pasted + eval(code) + + +def test_lode_hypers(): + message = ( + "hyper parameter changed recently, please update your code. " + "Here are the new equivalent parameters" + ) + with pytest.raises(ValueError, match=message) as err: + LodeSphericalExpansion( + atomic_gaussian_width=0.3, + center_atom_weight=0.5, + cutoff=3.4, + cutoff_function={"Step": {}}, + max_angular=5, + max_radial=3, + radial_basis={"Gto": {"splined_radial_integral": False}}, + potential_exponent=3, + k_cutoff=26.2, + ) + + error_message = str(err.value.args[0]) + first_line = error_message.find("\n") + code = error_message[first_line:] + + # max radial meaning changed + assert '"max_radial": 2' in code + + # check that the error message contains valid code that can be copy/pasted + eval(code) + + +def test_hypers_classes(): + hypers = { + "cutoff": { + "radius": 3.4, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, + }, + "density": { + "type": "Gaussian", + "width": 0.3, + "center_atom_weight": 0.3, + "scaling": { + "type": "Willatt2018", + "exponent": 3, + "rate": 2.2, + "scale": 1.1, + }, + }, + "basis": { + "type": "Explicit", + "by_angular": { + 3: {"type": "Gto", "max_radial": 5}, + 5: {"type": "Gto", "max_radial": 2}, + }, + }, + } + + with_dict = SphericalExpansion(**hypers) + + with_classes = SphericalExpansion( + cutoff=featomic.cutoff.Cutoff( + radius=3.4, + smoothing=featomic.cutoff.ShiftedCosine(width=0.5), + ), + density=featomic.density.Gaussian( + width=0.3, + scaling=featomic.density.Willatt2018(exponent=3, rate=2.2, scale=1.1), + center_atom_weight=0.3, + ), + basis=featomic.basis.Explicit( + by_angular={ + 3: featomic.basis.Gto(max_radial=5), + 5: featomic.basis.Gto(max_radial=2), + } + ), + ) + + system = SystemForTests() + metatensor.equal_raise(with_dict.compute(system), with_classes.compute(system)) + + +def test_hypers_custom_classes_errors(): + class MyCustomSmoothing(featomic.cutoff.SmoothingFunction): + def compute(self, cutoff, positions, derivative): + pass + + message = ( + "this smoothing function \\(MyCustomSmoothing\\) does not have " + "matching hyper parameters in the native calculators" + ) + with pytest.raises(NotImplementedError, match=message): + SphericalExpansion( + cutoff=featomic.cutoff.Cutoff(radius=3.4, smoothing=MyCustomSmoothing()), + density=featomic.density.Gaussian(width=0.3), + basis=featomic.basis.TensorProduct( + max_angular=5, + radial=featomic.basis.Gto(max_radial=5), + ), + ) + + class MyCustomScaling(featomic.density.RadialScaling): + def compute(self, positions, derivative): + pass + + message = ( + "this density scaling \\(MyCustomScaling\\) does not have matching hyper " + "parameters in the native calculators" + ) + with pytest.raises(NotImplementedError, match=message): + SphericalExpansion( + cutoff=featomic.cutoff.Cutoff(radius=3.4, smoothing=None), + density=featomic.density.Gaussian( + width=0.3, + scaling=MyCustomScaling(), + ), + basis=featomic.basis.TensorProduct( + max_angular=5, + radial=featomic.basis.Gto(max_radial=5), + ), + ) + + class MyCustomDensity(featomic.density.AtomicDensity): + def compute(self, positions, derivative): + pass + + message = ( + "This density \\(MyCustomDensity\\) does not have matching hyper " + "parameters in the native calculators. It should be used " + "through one of the spliner class instead of directly" + ) + with pytest.raises(NotImplementedError, match=message): + SphericalExpansion( + cutoff=featomic.cutoff.Cutoff(radius=3.4, smoothing=None), + density=MyCustomDensity(), + basis=featomic.basis.TensorProduct( + max_angular=5, + radial=featomic.basis.Gto(max_radial=5), + ), + ) + + class MyCustomExpansionBasis(featomic.basis.ExpansionBasis): + def angular_channels(self) -> List[int]: + return [0] + + def radial_basis(self, angular) -> featomic.basis.RadialBasis: + return featomic.basis.Gto(width=0.3) + + message = ( + "This basis functions set \\(MyCustomExpansionBasis\\) does not have " + "matching hyper parameters in the native calculators. It should be used " + "through one of the spliner class instead of directly" + ) + with pytest.raises(NotImplementedError, match=message): + SphericalExpansion( + cutoff=featomic.cutoff.Cutoff(radius=3.4, smoothing=None), + density=featomic.density.Gaussian(width=0.3), + basis=MyCustomExpansionBasis(), + ) + + class MyCustomRadialBasis(featomic.basis.RadialBasis): + def __init__(self): + super().__init__(max_radial=3, radius=2) + + def compute_primitive(self, positions, n, derivative): + pass + + message = ( + "This radial basis function \\(MyCustomRadialBasis\\) does not have " + "matching hyper parameters in the native calculators. It should be used " + "through one of the spliner class instead of directly" + ) + with pytest.raises(NotImplementedError, match=message): + SphericalExpansion( + cutoff=featomic.cutoff.Cutoff(radius=3.4, smoothing=None), + density=featomic.density.Gaussian(width=0.3), + basis=featomic.basis.TensorProduct( + max_angular=5, radial=MyCustomRadialBasis() + ), + ) diff --git a/python/rascaline/tests/calculators/keys_selection.py b/python/featomic/tests/calculators/keys_selection.py similarity index 89% rename from python/rascaline/tests/calculators/keys_selection.py rename to python/featomic/tests/calculators/keys_selection.py index dd7d2c90a..7499926a1 100644 --- a/python/rascaline/tests/calculators/keys_selection.py +++ b/python/featomic/tests/calculators/keys_selection.py @@ -2,8 +2,8 @@ import pytest from metatensor import Labels, TensorBlock, TensorMap -from rascaline import RascalError -from rascaline.calculators import DummyCalculator, SphericalExpansion +from featomic import FeatomicError +from featomic.calculators import DummyCalculator, SphericalExpansion from ..test_systems import SystemForTests @@ -57,13 +57,19 @@ def test_selection_existing(): def test_selection_partial(): system = SystemForTests() calculator = SphericalExpansion( - cutoff=2.5, - max_radial=1, - max_angular=1, - atomic_gaussian_width=0.2, - radial_basis={"Gto": {}}, - cutoff_function={"ShiftedCosine": {"width": 0.5}}, - center_atom_weight=1.0, + cutoff={ + "radius": 2.5, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, + }, + density={ + "type": "Gaussian", + "width": 0.2, + }, + basis={ + "type": "TensorProduct", + "max_angular": 1, + "radial": {"type": "Gto", "max_radial": 1}, + }, ) # Manually select the keys @@ -156,7 +162,7 @@ def test_name_errors(): "invalid parameter: 'bad_name' in keys selection is not " "part of the keys of this calculator" ) - with pytest.raises(RascalError, match=message): + with pytest.raises(FeatomicError, match=message): calculator.compute(system, selected_keys=selected_keys) @@ -170,7 +176,7 @@ def test_key_errors(): ) message = "invalid parameter: selected keys can not be empty" - with pytest.raises(RascalError, match=message): + with pytest.raises(FeatomicError, match=message): calculator.compute(system, selected_keys=selected_keys) # in the case where both selected_properties/selected_samples and @@ -202,7 +208,7 @@ def test_key_errors(): "invalid parameter: expected a block for \\(center_type=4\\) in " "predefined properties selection" ) - with pytest.raises(RascalError, match=message): + with pytest.raises(FeatomicError, match=message): calculator.compute( system, selected_properties=selected_properties, diff --git a/python/rascaline/tests/calculators/properties_selection.py b/python/featomic/tests/calculators/properties_selection.py similarity index 97% rename from python/rascaline/tests/calculators/properties_selection.py rename to python/featomic/tests/calculators/properties_selection.py index 8c25f0b1c..61bf8a216 100644 --- a/python/rascaline/tests/calculators/properties_selection.py +++ b/python/featomic/tests/calculators/properties_selection.py @@ -2,8 +2,8 @@ import pytest from metatensor import Labels, TensorBlock, TensorMap -from rascaline import RascalError -from rascaline.calculators import DummyCalculator +from featomic import FeatomicError +from featomic.calculators import DummyCalculator from ..test_systems import SystemForTests @@ -152,7 +152,7 @@ def test_errors(): "invalid parameter: 'bad_name' in properties selection is not " "part of the properties of this calculator" ) - with pytest.raises(RascalError, match=message): + with pytest.raises(FeatomicError, match=message): calculator.compute( system, use_native_system=False, diff --git a/python/rascaline/tests/calculators/sample_selection.py b/python/featomic/tests/calculators/sample_selection.py similarity index 96% rename from python/rascaline/tests/calculators/sample_selection.py rename to python/featomic/tests/calculators/sample_selection.py index cc8b0cb02..3bee6fce1 100644 --- a/python/rascaline/tests/calculators/sample_selection.py +++ b/python/featomic/tests/calculators/sample_selection.py @@ -2,8 +2,8 @@ import pytest from metatensor import Labels, TensorBlock, TensorMap -from rascaline import RascalError -from rascaline.calculators import DummyCalculator +from featomic import FeatomicError +from featomic.calculators import DummyCalculator from ..test_systems import SystemForTests @@ -148,5 +148,5 @@ def test_errors(): "invalid parameter: 'bad_name' in samples selection is not part " "of the samples of this calculator" ) - with pytest.raises(RascalError, match=message): + with pytest.raises(FeatomicError, match=message): calculator.compute(system, use_native_system=False, selected_samples=samples) diff --git a/python/rascaline/tests/systems/__init__.py b/python/featomic/tests/clebsch_gordan/__init__.py similarity index 100% rename from python/rascaline/tests/systems/__init__.py rename to python/featomic/tests/clebsch_gordan/__init__.py diff --git a/python/rascaline/tests/utils/cartesian_spherical.py b/python/featomic/tests/clebsch_gordan/cartesian_spherical.py similarity index 99% rename from python/rascaline/tests/utils/cartesian_spherical.py rename to python/featomic/tests/clebsch_gordan/cartesian_spherical.py index 0a8745ae7..cffdfc685 100644 --- a/python/rascaline/tests/utils/cartesian_spherical.py +++ b/python/featomic/tests/clebsch_gordan/cartesian_spherical.py @@ -2,7 +2,7 @@ import pytest from metatensor import Labels, TensorBlock, TensorMap -from rascaline.utils.clebsch_gordan import cartesian_to_spherical +from featomic.clebsch_gordan import cartesian_to_spherical @pytest.fixture diff --git a/python/rascaline/tests/utils/cg_product.py b/python/featomic/tests/clebsch_gordan/cg_product.py similarity index 95% rename from python/rascaline/tests/utils/cg_product.py rename to python/featomic/tests/clebsch_gordan/cg_product.py index 341fb50a4..59743c7f9 100644 --- a/python/rascaline/tests/utils/cg_product.py +++ b/python/featomic/tests/clebsch_gordan/cg_product.py @@ -7,13 +7,13 @@ import pytest from metatensor import Labels, TensorBlock, TensorMap -import rascaline -from rascaline.utils.clebsch_gordan import ClebschGordanProduct +import featomic +from featomic.clebsch_gordan import ClebschGordanProduct # Try to import some modules ase = pytest.importorskip("ase") -import ase.io # noqa: E402, F811 +from ase import io # noqa: E402, F401 try: @@ -45,13 +45,19 @@ MAX_ANGULAR = 3 SPHEX_HYPERS = { - "cutoff": 3.0, # Angstrom - "max_radial": 3, # Exclusive - "max_angular": MAX_ANGULAR, # Inclusive - "atomic_gaussian_width": 0.3, - "radial_basis": {"Gto": {}}, - "cutoff_function": {"ShiftedCosine": {"width": 0.5}}, - "center_atom_weight": 1.0, + "cutoff": { + "radius": 3.0, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, + }, + "basis": { + "type": "TensorProduct", + "max_angular": MAX_ANGULAR, + "radial": {"type": "Gto", "max_radial": 3}, + }, + "density": { + "type": "Gaussian", + "width": 0.3, + }, } @@ -69,14 +75,14 @@ def h2o_isolated(): def spherical_expansion(frames: List[ase.Atoms]): - """Returns a rascaline SphericalExpansion""" - calculator = rascaline.SphericalExpansion(**SPHEX_HYPERS) + """Returns a featomic SphericalExpansion""" + calculator = featomic.SphericalExpansion(**SPHEX_HYPERS) return calculator.compute(frames) def spherical_expansion_by_pair(frames: List[ase.Atoms]): - """Returns a rascaline SphericalExpansionByPair""" - calculator = rascaline.SphericalExpansionByPair(**SPHEX_HYPERS) + """Returns a featomic SphericalExpansionByPair""" + calculator = featomic.SphericalExpansionByPair(**SPHEX_HYPERS) return calculator.compute(frames) diff --git a/python/featomic/tests/clebsch_gordan/coefficients.py b/python/featomic/tests/clebsch_gordan/coefficients.py new file mode 100644 index 000000000..3c36a6d19 --- /dev/null +++ b/python/featomic/tests/clebsch_gordan/coefficients.py @@ -0,0 +1,50 @@ +import numpy as np +import pytest + +from featomic.clebsch_gordan._coefficients import _complex2real + + +scipy = pytest.importorskip("scipy") + + +def complex_to_real_manual(sph): + # following https://en.wikipedia.org/wiki/Spherical_harmonics#Real_form + ell = (sph.shape[1] - 1) // 2 + + real = np.zeros(sph.shape) + for m in range(-ell, ell + 1): + if m < 0: + real[:, ell + m] = np.sqrt(2) * (-1) ** m * np.imag(sph[:, ell + abs(m)]) + elif m == 0: + assert np.all(np.imag(sph[:, ell + m]) == 0) + real[:, ell + m] = np.real(sph[:, ell + m]) + else: + real[:, ell + m] = np.sqrt(2) * (-1) ** m * np.real(sph[:, ell + m]) + + return real + + +def complex_to_real_matrix(sph): + ell = (sph.shape[1] - 1) // 2 + + matrix = _complex2real(ell, sph) + + real = sph @ matrix + + assert np.linalg.norm(np.imag(real)) < 1e-15 + return np.real(real) + + +def test_complex_to_real(): + theta = 2 * np.pi * np.random.rand(10) + phi = np.pi * np.random.rand(10) + + for ell in range(4): + values = np.zeros((10, 2 * ell + 1), dtype=np.complex128) + for m in range(-ell, ell + 1): + values[:, ell + m] = scipy.special.sph_harm(m, ell, theta, phi) + + real_manual = complex_to_real_manual(values) + real_matrix = complex_to_real_matrix(values) + + assert np.allclose(real_manual, real_matrix) diff --git a/python/rascaline/tests/utils/density_correlations.py b/python/featomic/tests/clebsch_gordan/density_correlations.py similarity index 82% rename from python/rascaline/tests/utils/density_correlations.py rename to python/featomic/tests/clebsch_gordan/density_correlations.py index 879b6fda3..99b9c9dc8 100644 --- a/python/rascaline/tests/utils/density_correlations.py +++ b/python/featomic/tests/clebsch_gordan/density_correlations.py @@ -5,15 +5,19 @@ import pytest from metatensor import Labels, TensorBlock, TensorMap -import rascaline -from rascaline.utils import PowerSpectrum, _dispatch -from rascaline.utils.clebsch_gordan import ClebschGordanProduct, DensityCorrelations -from rascaline.utils.clebsch_gordan._coefficients import calculate_cg_coefficients +import featomic +from featomic.clebsch_gordan import ( + ClebschGordanProduct, + DensityCorrelations, + PowerSpectrum, + _dispatch, +) +from featomic.clebsch_gordan._coefficients import calculate_cg_coefficients # Try to import some modules ase = pytest.importorskip("ase") -import ase.io # noqa: E402, F811 +from ase import io # noqa: E402, F401 try: @@ -47,24 +51,21 @@ else: ARRAYS_BACKEND = ["numpy"] +MAX_ANGULAR = 2 SPHEX_HYPERS = { - "cutoff": 2.5, # Angstrom - "max_radial": 3, # Exclusive - "max_angular": 3, # Inclusive - "atomic_gaussian_width": 0.2, - "radial_basis": {"Gto": {}}, - "cutoff_function": {"ShiftedCosine": {"width": 0.5}}, - "center_atom_weight": 1.0, -} - -SPHEX_HYPERS_SMALL = { - "cutoff": 2.5, # Angstrom - "max_radial": 1, # Exclusive - "max_angular": 2, # Inclusive - "atomic_gaussian_width": 0.2, - "radial_basis": {"Gto": {}}, - "cutoff_function": {"ShiftedCosine": {"width": 0.5}}, - "center_atom_weight": 1.0, + "cutoff": { + "radius": 2.5, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, + }, + "density": { + "type": "Gaussian", + "width": 0.2, + }, + "basis": { + "type": "TensorProduct", + "max_angular": MAX_ANGULAR, + "radial": {"type": "Gto", "max_radial": 1}, + }, } @@ -111,46 +112,26 @@ def h2o_periodic(): ] -def wigner_d_matrices(lmax: int): - return WignerDReal(lmax=lmax) +def wigner_d_matrices(max_angular: int): + return WignerDReal(max_angular=max_angular) def spherical_expansion(frames: List[ase.Atoms]): - """Returns a rascaline SphericalExpansion""" - calculator = rascaline.SphericalExpansion(**SPHEX_HYPERS) - return calculator.compute(frames) - - -def spherical_expansion_small(frames: List[ase.Atoms]): - """Returns a rascaline SphericalExpansion with smaller hypers""" - calculator = rascaline.SphericalExpansion(**SPHEX_HYPERS_SMALL) + """Returns a featomic SphericalExpansion""" + calculator = featomic.SphericalExpansion(**SPHEX_HYPERS) return calculator.compute(frames) def spherical_expansion_by_pair(frames: List[ase.Atoms]): - """Returns a rascaline SphericalExpansionByPair""" - calculator = rascaline.SphericalExpansionByPair(**SPHEX_HYPERS) - return calculator.compute(frames) - - -def spherical_expansion_by_pair_small(frames: List[ase.Atoms]): - """Returns a rascaline SphericalExpansionByPair with smaller hypers""" - calculator = rascaline.SphericalExpansionByPair(**SPHEX_HYPERS_SMALL) + """Returns a featomic SphericalExpansionByPair""" + calculator = featomic.SphericalExpansionByPair(**SPHEX_HYPERS) return calculator.compute(frames) def power_spectrum(frames: List[ase.Atoms]): - """Returns a rascaline PowerSpectrum constructed from a + """Returns a featomic PowerSpectrum constructed from a SphericalExpansion""" - return PowerSpectrum(rascaline.SphericalExpansion(**SPHEX_HYPERS)).compute(frames) - - -def power_spectrum_small(frames: List[ase.Atoms]): - """Returns a rascaline PowerSpectrum constructed from a - SphericalExpansion""" - return PowerSpectrum(rascaline.SphericalExpansion(**SPHEX_HYPERS_SMALL)).compute( - frames - ) + return PowerSpectrum(featomic.SphericalExpansion(**SPHEX_HYPERS)).compute(frames) def get_norm(tensor: TensorMap): @@ -195,7 +176,7 @@ def test_so3_equivariance(): frames = h2o_periodic() n_correlations = 1 - wig = wigner_d_matrices((n_correlations + 1) * SPHEX_HYPERS["max_angular"]) + wig = wigner_d_matrices((n_correlations + 1) * MAX_ANGULAR) rotated_frames = [transform_frame_so3(frame, wig.angles) for frame in frames] # Generate density @@ -208,7 +189,7 @@ def test_so3_equivariance(): calculator = DensityCorrelations( n_correlations=n_correlations, - max_angular=SPHEX_HYPERS["max_angular"] * (n_correlations + 1), + max_angular=MAX_ANGULAR * (n_correlations + 1), ) nu_3 = calculator.compute(density) nu_3_so3 = calculator.compute(density_so3) @@ -229,7 +210,7 @@ def test_o3_equivariance(): frames = h2_isolated() n_correlations = 1 selected_keys = None - wig = wigner_d_matrices((n_correlations + 1) * SPHEX_HYPERS["max_angular"]) + wig = wigner_d_matrices((n_correlations + 1) * MAX_ANGULAR) frames_o3 = [transform_frame_o3(frame, wig.angles) for frame in frames] # Generate density @@ -242,7 +223,7 @@ def test_o3_equivariance(): calculator = DensityCorrelations( n_correlations=n_correlations, - max_angular=SPHEX_HYPERS["max_angular"] * (n_correlations + 1), + max_angular=MAX_ANGULAR * (n_correlations + 1), ) nu_3 = calculator.compute(density, selected_keys=selected_keys) nu_3_o3 = calculator.compute(density_o3, selected_keys=selected_keys) @@ -261,7 +242,7 @@ def test_lambda_soap_vs_powerspectrum(): """ Tests for exact equivalence between the invariant block of a generated lambda-SOAP equivariant and the Python implementation of PowerSpectrum in - rascaline utils. + featomic utils. """ frames = h2_isolated() @@ -275,7 +256,7 @@ def test_lambda_soap_vs_powerspectrum(): n_correlations = 1 calculator = DensityCorrelations( n_correlations=n_correlations, - max_angular=SPHEX_HYPERS["max_angular"] * (n_correlations + 1), + max_angular=MAX_ANGULAR * (n_correlations + 1), ) lambda_soap = calculator.compute( density, @@ -315,7 +296,7 @@ def test_lambda_soap_vs_powerspectrum(): ) def test_correlate_density_norm(): """ - Checks \\|\\rho^\\nu\\| = \\|\\rho\\|^\\nu in the case where l lists are not + Checks \\|\\rho^\\nu\\| = \\|\\rho\\|^\\nu in the case where l lists are not sorted. If l lists are sorted, thus saving computation of redundant block combinations, the norm check will not hold for target body order greater than 2. """ @@ -323,13 +304,13 @@ def test_correlate_density_norm(): n_correlations = 1 # Build nu=1 SphericalExpansion - density = spherical_expansion_small(frames) + density = spherical_expansion(frames) density = density.keys_to_properties("neighbor_type") # Build higher body order tensor without sorting the l lists calculator = DensityCorrelations( n_correlations=n_correlations, - max_angular=SPHEX_HYPERS_SMALL["max_angular"] * (n_correlations + 1), + max_angular=MAX_ANGULAR * (n_correlations + 1), skip_redundant=False, ) ps = calculator.compute(density) @@ -337,7 +318,7 @@ def test_correlate_density_norm(): # Build higher body order tensor *with* sorting the l lists calculator = DensityCorrelations( n_correlations=n_correlations, - max_angular=SPHEX_HYPERS_SMALL["max_angular"] * (n_correlations + 1), + max_angular=MAX_ANGULAR * (n_correlations + 1), skip_redundant=True, ) ps_sorted = calculator.compute(density) @@ -480,17 +461,17 @@ def test_correlate_density_dense_sparse_agree(): CG coefficient caches. """ frames = h2o_periodic() - density = spherical_expansion_small(frames) + density = spherical_expansion(frames) density = density.keys_to_properties("neighbor_type") n_correlations = 1 calculator_sparse = DensityCorrelations( n_correlations=n_correlations, - max_angular=SPHEX_HYPERS_SMALL["max_angular"] * (n_correlations + 1), + max_angular=MAX_ANGULAR * (n_correlations + 1), cg_backend="python-sparse", ) calculator_dense = DensityCorrelations( - max_angular=SPHEX_HYPERS_SMALL["max_angular"] * (n_correlations + 1), + max_angular=MAX_ANGULAR * (n_correlations + 1), n_correlations=n_correlations, cg_backend="python-dense", ) @@ -515,7 +496,7 @@ def test_correlate_density_metadata_agree(): frames = h2o_isolated() for max_angular, nu_1 in [ - (4, spherical_expansion_small(frames)), + (4, spherical_expansion(frames)), (6, spherical_expansion(frames)), ]: nu_1 = nu_1.keys_to_properties("neighbor_type") @@ -553,7 +534,7 @@ def test_correlate_density_angular_selection( calculator = DensityCorrelations( n_correlations=n_correlations, skip_redundant=skip_redundant, - max_angular=SPHEX_HYPERS["max_angular"] * (n_correlations + 1), + max_angular=MAX_ANGULAR * (n_correlations + 1), arrays_backend=arrays_backend, dtype=torch.float64 if arrays_backend == "torch" else None, ) @@ -566,7 +547,7 @@ def test_correlate_density_angular_selection( if selected_keys is None: assert np.all( [ - angular in np.arange(SPHEX_HYPERS["max_angular"] * 2 + 1) + angular in np.arange(MAX_ANGULAR * 2 + 1) for angular in np.unique(nu_2.keys.column("o3_lambda")) ] ) @@ -593,15 +574,15 @@ def test_summed_powerspectrum_by_pair_equals_powerspectrum(): frames = h2o_isolated() density_correlations = DensityCorrelations( n_correlations=1, - max_angular=SPHEX_HYPERS["max_angular"] * 2, + max_angular=MAX_ANGULAR * 2, skip_redundant=False, ) cg_product = ClebschGordanProduct( - max_angular=SPHEX_HYPERS["max_angular"] * 2, + max_angular=MAX_ANGULAR * 2, ) # Generate density and rename dimensions ready for correlation - density = spherical_expansion_small(frames) + density = spherical_expansion(frames) density = metatensor.rename_dimension( density, "keys", "center_type", "first_atom_type" ) @@ -621,7 +602,7 @@ def test_summed_powerspectrum_by_pair_equals_powerspectrum(): ) # Generate pair density - pair_density = spherical_expansion_by_pair_small(frames) + pair_density = spherical_expansion_by_pair(frames) pair_density = pair_density.keys_to_properties("second_atom_type") pair_density = metatensor.rename_dimension(pair_density, "properties", "n", "n_2") pair_density = metatensor.rename_dimension( @@ -657,28 +638,27 @@ def test_angular_cutoff(): """ frames = h2o_isolated() - # Initialize the calculator with only max_angular = SPHEX_HYPERS["max_angular"] * 2. + # Initialize the calculator with only max_angular = MAX_ANGULAR * 2. # We will cutoff off the angular channels at 3 for all intermediate iterations, and # only on the final iteration do the full product, doubling the max angular order. n_correlations = 2 calculator = DensityCorrelations( n_correlations=n_correlations, - max_angular=SPHEX_HYPERS_SMALL["max_angular"] * 2, + max_angular=MAX_ANGULAR * 2, ) # Generate density - density = spherical_expansion_small(frames) + density = spherical_expansion(frames) # Perform 3 iterations of DensityCorrelations with `angular_cutoff` nu_4 = calculator.compute( density, - angular_cutoff=SPHEX_HYPERS_SMALL["max_angular"], + angular_cutoff=MAX_ANGULAR, selected_keys=None, ) assert np.all( - np.sort(np.unique(nu_4.keys.column("o3_lambda"))) - == np.arange(SPHEX_HYPERS_SMALL["max_angular"] + 1) + np.sort(np.unique(nu_4.keys.column("o3_lambda"))) == np.arange(MAX_ANGULAR + 1) ) @@ -690,23 +670,22 @@ def test_angular_cutoff_with_selected_keys(): """ frames = h2o_isolated() - # Initialize the calculator with only max_angular = SPHEX_HYPERS["max_angular"] * 2. + # Initialize the calculator with only max_angular = MAX_ANGULAR * 2. # We will cutoff off the angular channels at 3 for all intermediate iterations, and # only on the final iteration do the full product, doubling the max angular order. calculator = DensityCorrelations( n_correlations=2, - max_angular=SPHEX_HYPERS_SMALL["max_angular"] * 2, + max_angular=MAX_ANGULAR * 2, ) # Generate density - density = spherical_expansion_small(frames) + density = spherical_expansion(frames) # Perform 3 iterations of DensityCorrelations with `angular_cutoff` nu_4 = calculator.compute( density, - angular_cutoff=SPHEX_HYPERS_SMALL[ - "max_angular" - ], # applies to all intermediate steps as selected_keys is specified + # `angular_cutoff` applies to all iterations as `selected_keys` is specified + angular_cutoff=MAX_ANGULAR, selected_keys=Labels( names=["o3_lambda"], values=np.arange(5).reshape(-1, 1), @@ -725,20 +704,19 @@ def test_no_error_with_correct_angular_selection(): frames = h2o_isolated() nu_1 = spherical_expansion(frames) - # Initialize the calculator with only max_angular = SPHEX_HYPERS["max_angular"] - max_angular = SPHEX_HYPERS["max_angular"] + # Initialize the calculator with only max_angular = MAX_ANGULAR density_correlations = DensityCorrelations( n_correlations=2, - max_angular=max_angular, + max_angular=MAX_ANGULAR, ) # If `angular_cutoff` and `selected_keys` were not passed, this should error as - # max_angular = SPHEX_HYPERS["max_angular"] * 3 would be required. + # max_angular = MAX_ANGULAR * 3 would be required. density_correlations.compute( nu_1, - angular_cutoff=max_angular, + angular_cutoff=MAX_ANGULAR, selected_keys=Labels( names=["o3_lambda"], - values=np.arange(max_angular).reshape(-1, 1), + values=np.arange(MAX_ANGULAR).reshape(-1, 1), ), ) diff --git a/python/featomic/tests/clebsch_gordan/equivariant_power_spectrum.py b/python/featomic/tests/clebsch_gordan/equivariant_power_spectrum.py new file mode 100644 index 000000000..1f47efe64 --- /dev/null +++ b/python/featomic/tests/clebsch_gordan/equivariant_power_spectrum.py @@ -0,0 +1,158 @@ +from typing import List + +import metatensor +import numpy as np +import pytest +from metatensor import Labels, TensorBlock, TensorMap +from numpy.testing import assert_equal + +from featomic import SphericalExpansion +from featomic.clebsch_gordan import EquivariantPowerSpectrum, PowerSpectrum + + +# Try to import some modules +ase = pytest.importorskip("ase") +import ase.io # noqa: E402, F811 + + +SPHEX_HYPERS_SMALL = { + "cutoff": { + "radius": 2.5, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, + }, + "density": { + "type": "Gaussian", + "width": 0.2, + }, + "basis": { + "type": "TensorProduct", + "max_angular": 2, + "radial": {"type": "Gto", "max_radial": 1}, + }, +} + +# ============ Helper functions ============ + + +def h2o_periodic(): + return [ + ase.Atoms( + symbols=["O", "H", "H"], + positions=[ + [2.56633400, 2.50000000, 2.50370100], + [1.97361700, 1.73067300, 2.47063400], + [1.97361700, 3.26932700, 2.47063400], + ], + cell=[5, 5, 5], + pbc=[True, True, True], + ) + ] + + +def power_spectrum(frames: List[ase.Atoms]): + """Returns a featomic PowerSpectrum constructed from a + SphericalExpansion""" + return PowerSpectrum(SphericalExpansion(**SPHEX_HYPERS_SMALL)).compute(frames) + + +# ============ Test EquivariantPowerSpectrum vs PowerSpectrum ============ + + +def test_equivariant_power_spectrum_vs_powerspectrum(): + """ + Tests for exact equivalence between the invariant block of a generated + EquivariantPowerSpectrum the Python implementation of PowerSpectrum in featomic + utils. + """ + # Build a PowerSpectrum + ps_1 = power_spectrum(h2o_periodic()) + + # Build an EquivariantPowerSpectrum + ps_2 = EquivariantPowerSpectrum(SphericalExpansion(**SPHEX_HYPERS_SMALL)).compute( + h2o_periodic(), + selected_keys=Labels(names=["o3_lambda"], values=np.array([0]).reshape(-1, 1)), + neighbors_to_properties=False, + ) + + # Manipulate metadata to match + ps_2 = ps_2.keys_to_properties(["neighbor_1_type", "neighbor_2_type"]) + keys = ps_2.keys.remove(name="o3_lambda") # redundant as all 0 + keys = keys.remove("o3_sigma") # redundant as all 1 + + blocks = [] + for block in ps_2.blocks(): + n_samples, n_props = block.values.shape[0], block.values.shape[2] + new_props = block.properties + new_props = new_props.remove(name="l_1") + new_props = new_props.rename(old="l_2", new="l") + blocks.append( + TensorBlock( + values=block.values.reshape((n_samples, n_props)), + samples=block.samples, + components=[], + properties=new_props, + ) + ) + ps_2 = TensorMap(keys=keys, blocks=blocks) + + # Permute properties dimension to match ps_1 and sort + ps_2 = metatensor.sort( + metatensor.permute_dimensions(ps_2, "properties", [2, 0, 3, 1, 4]), + "properties", + ) + + metatensor.equal_metadata_raise(ps_1, ps_2) + metatensor.allclose_raise(ps_1, ps_2) + + +def test_equivariant_power_spectrum_neighbors_to_properties(): + """ + Tests that computing an EquivariantPowerSpectrum is equivalent when passing + `neighbors_to_properties` as both True and False (after metadata manipulation). + """ + # Build an EquivariantPowerSpectrum + powspec_calc = EquivariantPowerSpectrum(SphericalExpansion(**SPHEX_HYPERS_SMALL)) + + # Compute the first. Move keys after CG step + powspec_1 = powspec_calc.compute( + h2o_periodic(), + neighbors_to_properties=False, + ) + powspec_1 = powspec_1.keys_to_properties(["neighbor_1_type", "neighbor_2_type"]) + + # Compute the second. Move keys before the CG step + powspec_2 = powspec_calc.compute( + h2o_periodic(), + neighbors_to_properties=True, + ) + + # Permute properties dimensions to match ``powspec_1`` and sort + powspec_2 = metatensor.sort( + metatensor.permute_dimensions(powspec_2, "properties", [2, 4, 0, 1, 3, 5]) + ) + + # Check equivalent + metatensor.equal_metadata_raise(powspec_1, powspec_2) + metatensor.equal_raise(powspec_1, powspec_2) + + +def test_fill_types_option() -> None: + """ + Test that ``neighbor_types`` options adds arbitrary atomic neighbor types. + """ + + frames = [ + ase.Atoms("H", positions=np.zeros([1, 3])), + ase.Atoms("O", positions=np.zeros([1, 3])), + ] + + neighbor_types = [1, 8, 10] + calculator = EquivariantPowerSpectrum( + calculator_1=SphericalExpansion(**SPHEX_HYPERS_SMALL), + neighbor_types=neighbor_types, + ) + + descriptor = calculator.compute(frames, neighbors_to_properties=True) + + assert_equal(np.unique(descriptor[0].properties["neighbor_1_type"]), neighbor_types) + assert_equal(np.unique(descriptor[0].properties["neighbor_2_type"]), neighbor_types) diff --git a/python/featomic/tests/clebsch_gordan/power_spectrum.py b/python/featomic/tests/clebsch_gordan/power_spectrum.py new file mode 100644 index 000000000..50f1e5aed --- /dev/null +++ b/python/featomic/tests/clebsch_gordan/power_spectrum.py @@ -0,0 +1,270 @@ +import copy + +import numpy as np +import pytest +from numpy.testing import assert_allclose, assert_equal + +import featomic +from featomic.clebsch_gordan import PowerSpectrum + +from ..test_systems import SystemForTests + + +ase = pytest.importorskip("ase") + + +SOAP_HYPERS = { + "cutoff": { + "radius": 5.0, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, + }, + "density": { + "type": "Gaussian", + "width": 0.3, + }, + "basis": { + "type": "TensorProduct", + "max_angular": 4, + "radial": {"type": "Gto", "max_radial": 6}, + }, +} + +LODE_HYPERS = { + "density": { + "type": "SmearedPowerLaw", + "smearing": 0.3, + "exponent": 1, + }, + "basis": { + "type": "TensorProduct", + "max_angular": 4, + "radial": { + "type": "Gto", + "max_radial": 6, + "radius": 5.0, + }, + }, +} + +N_ATOMIC_TYPES = len(np.unique(SystemForTests().types())) + + +def soap_spx(): + return featomic.SphericalExpansion(**SOAP_HYPERS) + + +def soap_ps(): + return featomic.SoapPowerSpectrum(**SOAP_HYPERS) + + +def lode_spx(): + return featomic.LodeSphericalExpansion(**LODE_HYPERS) + + +def soap(): + return soap_spx().compute(SystemForTests()) + + +def power_spectrum(): + return PowerSpectrum(soap_spx()).compute(SystemForTests()) + + +@pytest.mark.parametrize("calculator", [soap_spx(), lode_spx()]) +def test_power_spectrum(calculator) -> None: + """Test that power spectrum works and that the shape is correct.""" + ps_python = PowerSpectrum(calculator).compute(SystemForTests()) + ps_python = ps_python.keys_to_samples(["center_type"]) + + # Test the number of properties is correct + n_props_actual = len(ps_python.block().properties) + + n_props_expected = ( + N_ATOMIC_TYPES**2 + * (SOAP_HYPERS["basis"]["radial"]["max_radial"] + 1) ** 2 + * (SOAP_HYPERS["basis"]["max_angular"] + 1) + ) + + assert n_props_actual == n_props_expected + + +def test_error_max_angular(): + """Test error raise if max_angular are different.""" + hypers_2 = copy.deepcopy(SOAP_HYPERS) + hypers_2["basis"]["radial"]["max_radial"] = 3 + hypers_2["basis"]["max_angular"] = 2 + + se_calculator2 = featomic.SphericalExpansion(**hypers_2) + + message = "'basis.max_angular' must be the same in both calculators" + with pytest.raises(ValueError, match=message): + PowerSpectrum(soap_spx(), se_calculator2) + + +def test_wrong_calculator(): + message = ( + "Only \\[lode_spherical_expansion, spherical_expansion\\] " + "are supported for `calculator_1`, got 'soap_power_spectrum'" + ) + with pytest.raises(ValueError, match=message): + PowerSpectrum(soap_ps()) + + message = ( + "Only \\[lode_spherical_expansion, spherical_expansion\\] " + "are supported for `calculator_2`, got 'soap_power_spectrum'" + ) + with pytest.raises(ValueError, match=message): + PowerSpectrum(soap_spx(), soap_ps()) + + +def test_power_spectrum_different_hypers() -> None: + """Test that power spectrum works with different spherical expansions.""" + + hypers_2 = copy.deepcopy(SOAP_HYPERS) + hypers_2["basis"]["radial"]["max_radial"] = 3 + + soap_spx_2 = featomic.SphericalExpansion(**hypers_2) + + PowerSpectrum(soap_spx(), soap_spx_2).compute(SystemForTests()) + + +def test_power_spectrum_rust() -> None: + """Test that the dot kernels of the rust and python version are the same.""" + + power_spectrum_python = power_spectrum() + power_spectrum_python = power_spectrum_python.keys_to_samples(["center_type"]) + kernel_python = np.dot( + power_spectrum_python[0].values, power_spectrum_python[0].values.T + ) + + power_spectrum_rust = featomic.SoapPowerSpectrum(**SOAP_HYPERS).compute( + SystemForTests() + ) + power_spectrum_rust = power_spectrum_rust.keys_to_samples(["center_type"]) + power_spectrum_rust = power_spectrum_rust.keys_to_properties( + ["neighbor_1_type", "neighbor_2_type"] + ) + kernel_rust = np.dot(power_spectrum_rust[0].values, power_spectrum_rust[0].values.T) + assert_allclose(kernel_python, kernel_rust) + + +def test_power_spectrum_gradients() -> None: + """Test that gradients are correct using finite differences.""" + calculator = PowerSpectrum(soap_spx()) + + # An ASE atoms object with the same properties as SystemForTests() + atoms = ase.Atoms( + symbols="HHOO", + positions=[[0, 0, 0], [0, 0, 1], [0, 0, 2], [0, 0, 3]], + pbc=True, + cell=[[10, 0, 0], [0, 10, 0], [0, 0, 10]], + ) + + _finite_differences_positions(calculator, atoms) + + +def test_power_spectrum_unknown_gradient() -> None: + """Test error raise if an unknown gradient is present.""" + + message = "PowerSpectrum currently only supports gradients w.r.t. to positions" + with pytest.raises(NotImplementedError, match=message): + PowerSpectrum(soap_spx()).compute(SystemForTests(), gradients=["strain"]) + + +def test_fill_neighbor_type() -> None: + """Test that ``center_type`` keys can be merged for different blocks.""" + + frames = [ + ase.Atoms("H", positions=np.zeros([1, 3])), + ase.Atoms("O", positions=np.zeros([1, 3])), + ] + + calculator = PowerSpectrum( + calculator_1=soap_spx(), + calculator_2=soap_spx(), + ) + + descriptor = calculator.compute(frames) + + descriptor.keys_to_samples("center_type") + + +def test_fill_types_option() -> None: + """Test that ``neighbor_types`` options adds arbitrary atomic neighbor types.""" + + frames = [ + ase.Atoms("H", positions=np.zeros([1, 3])), + ase.Atoms("O", positions=np.zeros([1, 3])), + ] + + neighbor_types = [1, 8, 10] + calculator = PowerSpectrum(calculator_1=soap_spx(), neighbor_types=neighbor_types) + + descriptor = calculator.compute(frames) + + assert_equal(np.unique(descriptor[0].properties["neighbor_1_type"]), neighbor_types) + assert_equal(np.unique(descriptor[0].properties["neighbor_2_type"]), neighbor_types) + + +def _finite_differences_positions( + calculator, + system, + displacement=1e-6, + rtol=1e-5, + atol=1e-16, +): + """ + Check that analytical gradients with respect to positions agree with a finite + difference calculation of the gradients. + + The implementation is simular to ``featomic/src/calculators/tests_utils.rs``. + + :param calculator: calculator used to compute the representation + :param system: Atoms object + :param displacement: distance each atom will be displaced in each direction when + computing finite differences + :param max_relative: Maximal relative error. ``10 * displacement`` is a good + starting point + :param atol: Threshold below which all values are considered zero. This should be + very small (1e-16) to prevent false positives (if all values & gradients are + below that threshold, tests will pass even with wrong gradients) + :raises AssertionError: if the two gradients are not equal up to specified precision + """ + reference = calculator.compute(system, gradients=["positions"]) + + for atom_i in range(len(system)): + for xyz in range(3): + system_pos = system.copy() + system_pos.positions[atom_i, xyz] += displacement / 2 + updated_pos = calculator.compute(system_pos) + + system_neg = system.copy() + system_neg.positions[atom_i, xyz] -= displacement / 2 + updated_neg = calculator.compute(system_neg) + + assert updated_pos.keys == reference.keys + assert updated_neg.keys == reference.keys + + for key, block in reference.items(): + gradients = block.gradient("positions") + + block_pos = updated_pos.block(key) + block_neg = updated_neg.block(key) + + for gradient_i, (sample_i, _, atom) in enumerate(gradients.samples): + if atom != atom_i: + continue + + # check that the sample is the same in both descriptors + assert block_pos.samples[sample_i] == block.samples[sample_i] + assert block_neg.samples[sample_i] == block.samples[sample_i] + + value_pos = block_pos.values[sample_i] + value_neg = block_neg.values[sample_i] + gradient = gradients.values[gradient_i, xyz] + + assert value_pos.shape == gradient.shape + assert value_neg.shape == gradient.shape + + finite_difference = (value_pos - value_neg) / displacement + + assert_allclose(finite_difference, gradient, rtol=rtol, atol=atol) diff --git a/python/rascaline/tests/utils/rotations.py b/python/featomic/tests/clebsch_gordan/rotations.py similarity index 58% rename from python/rascaline/tests/utils/rotations.py rename to python/featomic/tests/clebsch_gordan/rotations.py index 0ec729254..db67bc79d 100644 --- a/python/rascaline/tests/utils/rotations.py +++ b/python/featomic/tests/clebsch_gordan/rotations.py @@ -87,16 +87,15 @@ class WignerDReal: real-valued coefficients. """ - def __init__(self, lmax: int, angles: Sequence[float] = None): + def __init__(self, max_angular: int, angles: Sequence[float] = None): """ Initialize the WignerDReal class. - :param lmax: int, the maximum angular momentum channel for which the + :param max_angular: int, the maximum angular momentum channel for which the Wigner D matrices are computed - :param angles: Sequence[float], the alpha, beta, gamma Euler angles, in - radians. + :param angles: Sequence[float], the alpha, beta, gamma Euler angles, in radians. """ - self.lmax = lmax + self.max_angular = max_angular # Randomly generate Euler angles between 0 and 2 pi if none are provided if angles is None: angles = np.random.uniform(size=(3)) * 2 * np.pi @@ -105,100 +104,24 @@ def __init__(self, lmax: int, angles: Sequence[float] = None): r2c_mats = {} c2r_mats = {} - for L in range(0, self.lmax + 1): + for L in range(0, self.max_angular + 1): r2c_mats[L] = np.hstack( [_r2c(np.eye(2 * L + 1)[i])[:, np.newaxis] for i in range(2 * L + 1)] ) c2r_mats[L] = np.conjugate(r2c_mats[L]).T self.matrices = {} - for L in range(0, self.lmax + 1): + for L in range(0, self.max_angular + 1): wig = _wigner_d(L, self.angles) self.matrices[L] = np.real(c2r_mats[L] @ np.conjugate(wig) @ r2c_mats[L]) - def rotate_coeff_vector( - self, - atoms: ase.Atoms, - coeffs: np.ndarray, - lmax: dict, - nmax: dict, - ) -> np.ndarray: - """ - Rotates the irreducible spherical components (ISCs) of basis set coefficients in - the spherical basis passed in as a flat vector. - - Required is the basis set definition specified by ``lmax`` and ``nmax``. This - are dicts of the form: - - lmax = {symbol: lmax_value, ...} nmax = {(symbol, l): nmax_value, ...} - - where ``symbol`` is the chemical symbol of the atom, ``lmax_value`` is its - corresponding max l channel value. For each combination of species symbol and - lmax, there exists a max radial channel value ``nmax_value``. - - Then, the assumed ordering of basis function coefficients follows a hierarchy, - which can be read as nested loops over the various indices. Be mindful that some - indices range are from 0 to x (exclusive) and others from 0 to x + 1 - (exclusive). The ranges reported below are ordered. - - 1. Loop over atoms (index ``i``, of chemical species ``a``) in the system. ``i`` - takes values 0 to N (** exclusive **), where N is the number of atoms in the - system. - - 2. Loop over spherical harmonics channel (index ``l``) for each atom. ``l`` - takes values from 0 to ``lmax[a] + 1`` (** exclusive **), where ``a`` is the - chemical species of atom ``i``, given by the chemical symbol at the ``i``th - position of ``symbol_list``. - - 3. Loop over radial channel (index ``n``) for each atom ``i`` and spherical - harmonics channel ``l`` combination. ``n`` takes values from 0 to - ``nmax[(a, l)]`` (** exclusive **). - - 4. Loop over spherical harmonics component (index ``m``) for each atom. ``m`` - takes values from ``-l`` to ``l`` (** inclusive **). - - :param atoms: the atomic systems in ASE format for which the coefficients are - defined. - :param coeffs: the coefficients in the spherical basis, as a flat vector. - :param lmax: dict containing the maximum spherical harmonics (l) value for each - atom type. - :param nmax: dict containing the maximum radial channel (n) value for each - combination of atom type and l. - - :return: the rotated coefficients in the spherical basis, as a flat vector with - the same order as the input vector. - """ - # Initialize empty vector for storing the rotated ISCs - rot_vect = np.empty_like(coeffs) - - # Iterate over atomic species of the atoms in the frame - curr_idx = 0 - for symbol in atoms.get_chemical_symbols(): - # Get the basis set lmax value for this species - sym_lmax = lmax[symbol] - for angular_l in range(sym_lmax + 1): - # Get the number of radial functions for this species and l value - sym_l_nmax = nmax[(symbol, angular_l)] - # Get the Wigner D Matrix for this l value - wig_mat = self.matrices[angular_l].T - for _n in range(sym_l_nmax): - # Retrieve the irreducible spherical component - isc = coeffs[curr_idx : curr_idx + (2 * angular_l + 1)] - # Rotate the ISC and store - rot_isc = isc @ wig_mat - rot_vect[curr_idx : curr_idx + (2 * angular_l + 1)][:] = rot_isc[:] - # Update the start index for the next ISC - curr_idx += 2 * angular_l + 1 - - return rot_vect - - def rotate_tensorblock(self, angular_l: int, block: TensorBlock) -> TensorBlock: + def rotate_tensorblock(self, o3_lambda: int, block: TensorBlock) -> TensorBlock: """ Rotates a TensorBlock ``block``, represented in the spherical basis, according to the Wigner D Real matrices for the given ``l`` value. Assumes the components of the block are [("o3_mu",),]. """ # Get the Wigner matrix for this l value - wig = self.matrices[angular_l].T + wig = self.matrices[o3_lambda].T # Copy the block block_rotated = block.copy() @@ -227,7 +150,7 @@ def transform_tensormap_so3(self, tensor: TensorMap) -> TensorMap: matrices. Assumes the input tensor follows the metadata structure consistent with - those produce by rascaline. + those produce by featomic. """ # Retrieve the key and the position of the l value in the key names keys = tensor.keys @@ -237,10 +160,10 @@ def transform_tensormap_so3(self, tensor: TensorMap) -> TensorMap: rotated_blocks = [] for key in keys: # Retrieve the l value - angular_l = key[idx_l_value] + o3_lambda = key[idx_l_value] # Rotate the block and store - rotated_blocks.append(self.rotate_tensorblock(angular_l, tensor[key])) + rotated_blocks.append(self.rotate_tensorblock(o3_lambda, tensor[key])) return TensorMap(keys, rotated_blocks) @@ -250,7 +173,7 @@ def transform_tensormap_o3(self, tensor: TensorMap) -> TensorMap: SO(3) rigid rotation using Wigner-D Matrices followed by an inversion. Assumes the input tensor follows the metadata structure consistent with - those produce by rascaline. + those produce by featomic. """ # Retrieve the key and the position of the l value in the key names keys = tensor.keys @@ -260,10 +183,10 @@ def transform_tensormap_o3(self, tensor: TensorMap) -> TensorMap: new_blocks = [] for key in keys: # Retrieve the l value - angular_l = key[idx_l_value] + o3_lambda = key[idx_l_value] # Rotate the block - new_block = self.rotate_tensorblock(angular_l, tensor[key]) + new_block = self.rotate_tensorblock(o3_lambda, tensor[key]) # Work out the inversion multiplier according to the convention inversion_multiplier = 1 @@ -271,7 +194,7 @@ def transform_tensormap_o3(self, tensor: TensorMap) -> TensorMap: inversion_multiplier *= -1 # "o3_sigma" may not be present if CG iterations haven't been - # performed (i.e. nu=1 rascaline SphericalExpansion) + # performed (i.e. nu=1 featomic SphericalExpansion) if "o3_sigma" in keys.names: if key["o3_sigma"] == -1: inversion_multiplier *= -1 @@ -291,14 +214,13 @@ def transform_tensormap_o3(self, tensor: TensorMap) -> TensorMap: # ===== Helper functions for WignerDReal -def _wigner_d(angular_l: int, angles: Sequence[float]) -> np.ndarray: +def _wigner_d(o3_lambda: int, angles: Sequence[float]) -> np.ndarray: """ - Computes the Wigner D matrix: - D^l_{mm'}(alpha, beta, gamma) - from sympy and converts it to numerical values. + Computes the Wigner D matrix: ``D^l_{mm'}(alpha, beta, gamma)`` from sympy and + converts it to numerical values. - `angles` are the alpha, beta, gamma Euler angles (radians, ZYZ convention) - and l the irrep. + ``angles`` are the alpha, beta, gamma Euler angles (radians, ZYZ convention) and l + the irrep. """ try: from sympy.physics.wigner import wigner_d @@ -306,7 +228,7 @@ def _wigner_d(angular_l: int, angles: Sequence[float]) -> np.ndarray: raise ModuleNotFoundError( "Calculation of Wigner D matrices requires a sympy installation" ) - return np.complex128(wigner_d(angular_l, *angles)) + return np.complex128(wigner_d(o3_lambda, *angles)) def _r2c(sp): @@ -317,12 +239,12 @@ def _r2c(sp): i_sqrt_2 = 1.0 / np.sqrt(2) - angular_l = (len(sp) - 1) // 2 # infers l from the vector size + o3_lambda = (len(sp) - 1) // 2 # infers l from the vector size rc = np.zeros(len(sp), dtype=np.complex128) - rc[angular_l] = sp[angular_l] - for m in range(1, angular_l + 1): - rc[angular_l + m] = ( - (sp[angular_l + m] + 1j * sp[angular_l - m]) * i_sqrt_2 * (-1) ** m + rc[o3_lambda] = sp[o3_lambda] + for m in range(1, o3_lambda + 1): + rc[o3_lambda + m] = ( + (sp[o3_lambda + m] + 1j * sp[o3_lambda - m]) * i_sqrt_2 * (-1) ** m ) - rc[angular_l - m] = (sp[angular_l + m] - 1j * sp[angular_l - m]) * i_sqrt_2 + rc[o3_lambda - m] = (sp[o3_lambda + m] - 1j * sp[o3_lambda - m]) * i_sqrt_2 return rc diff --git a/python/featomic/tests/density.py b/python/featomic/tests/density.py new file mode 100644 index 000000000..548b2ce7c --- /dev/null +++ b/python/featomic/tests/density.py @@ -0,0 +1,20 @@ +import numpy as np +import pytest + +from featomic.density import Gaussian, SmearedPowerLaw + + +pytest.importorskip("scipy") + + +@pytest.mark.parametrize( + "density", + [Gaussian(width=1.2), SmearedPowerLaw(smearing=1.2, exponent=1)], +) +def test_derivative(density): + positions = np.linspace(0, 5, num=int(1e6)) + rho = density.compute(positions, derivative=False) + analytical_grad = density.compute(positions, derivative=True) + numerical_grad = np.gradient(rho, positions) + + np.testing.assert_allclose(numerical_grad, analytical_grad, atol=1e-6) diff --git a/python/rascaline/tests/log.py b/python/featomic/tests/log.py similarity index 77% rename from python/rascaline/tests/log.py rename to python/featomic/tests/log.py index 7e7791103..ae613af58 100644 --- a/python/rascaline/tests/log.py +++ b/python/featomic/tests/log.py @@ -1,10 +1,10 @@ import pytest -from rascaline import set_logging_callback -from rascaline.calculators import DummyCalculator -from rascaline.log import ( - RASCAL_LOG_LEVEL_INFO, - RASCAL_LOG_LEVEL_WARN, +from featomic import set_logging_callback +from featomic.calculators import DummyCalculator +from featomic.log import ( + FEATOMIC_LOG_LEVEL_INFO, + FEATOMIC_LOG_LEVEL_WARN, default_logging_callback, ) @@ -35,10 +35,9 @@ def record_log_events(level, message): calculator.compute(SystemForTests()) message = ( - "rascaline::calculators::dummy_calculator -- " - "log-test-info: test info message" + "featomic::calculators::dummy_calculator -- " "log-test-info: test info message" ) - event = (RASCAL_LOG_LEVEL_INFO, message) + event = (FEATOMIC_LOG_LEVEL_INFO, message) assert event in recorded_events calculator = DummyCalculator( @@ -49,10 +48,10 @@ def record_log_events(level, message): calculator.compute(SystemForTests()) message = ( - "rascaline::calculators::dummy_calculator -- " + "featomic::calculators::dummy_calculator -- " "log-test-warn: this is a test warning message" ) - event = (RASCAL_LOG_LEVEL_WARN, message) + event = (FEATOMIC_LOG_LEVEL_WARN, message) assert event in recorded_events diff --git a/python/featomic/tests/misc.py b/python/featomic/tests/misc.py new file mode 100644 index 000000000..37d2be1a2 --- /dev/null +++ b/python/featomic/tests/misc.py @@ -0,0 +1,19 @@ +import os + +import featomic + + +def test_cmake_prefix_path_exists(): + assert hasattr(featomic.utils, "cmake_prefix_path") + assert isinstance(featomic.utils.cmake_prefix_path, str) + + # there is both the path to metatensor and featomic cmake prefix in here + assert len(featomic.utils.cmake_prefix_path.split(";")), 2 + + +def test_cmake_files_exists(): + featomic_cmake_path = featomic.utils.cmake_prefix_path.split(";")[0] + cmake = os.path.join(featomic_cmake_path, "featomic") + + assert os.path.isfile(os.path.join(cmake, "featomic-config.cmake")) + assert os.path.isfile(os.path.join(cmake, "featomic-config-version.cmake")) diff --git a/python/featomic/tests/splines.py b/python/featomic/tests/splines.py new file mode 100644 index 000000000..20a707a90 --- /dev/null +++ b/python/featomic/tests/splines.py @@ -0,0 +1,179 @@ +import numpy as np +import pytest + +import featomic +from featomic import LodeSphericalExpansion, SphericalExpansion + +from .test_systems import SystemForTests + + +pytest.importorskip("scipy") +from scipy.special import gamma # noqa: E402 + + +def test_soap_spliner(): + """Compare splined spherical expansion with GTOs and a Gaussian density to + analytical implementation.""" + cutoff = 8.0 + + hypers = { + "cutoff": featomic.cutoff.Cutoff( + radius=cutoff, + smoothing=featomic.cutoff.Step(), + ), + "density": featomic.density.Gaussian(width=0.6), + "basis": featomic.basis.TensorProduct( + max_angular=6, + radial=featomic.basis.Gto(max_radial=6, radius=cutoff), + # We choose an accuracy that is lower then the default one (1e-8) + # to limit the time taken by this test. + spline_accuracy=1e-3, + ), + } + + spliner = featomic.splines.SoapSpliner(**hypers) + + analytic = SphericalExpansion(**hypers).compute(SystemForTests()) + splined = SphericalExpansion(**spliner.get_hypers()).compute(SystemForTests()) + + for key, block_analytic in analytic.items(): + block_splined = splined.block(key) + np.testing.assert_allclose( + block_splined.values, block_analytic.values, rtol=1e-5, atol=1e-5 + ) + + +@pytest.mark.parametrize("exponent", [0, 1, 4]) +def test_lode_spliner(exponent): + """Compare splined LODE spherical expansion with GTOs and a Gaussian density to + analytical implementation.""" + + hypers = { + "density": featomic.density.SmearedPowerLaw(smearing=0.8, exponent=exponent), + "basis": featomic.basis.TensorProduct( + max_angular=4, + radial=featomic.basis.Gto(max_radial=4, radius=2), + ), + } + + spliner = featomic.splines.LodeSpliner(**hypers) + + analytic = LodeSphericalExpansion(**hypers).compute(SystemForTests()) + splined = LodeSphericalExpansion(**spliner.get_hypers()).compute(SystemForTests()) + + for key, block_analytic in analytic.items(): + block_splined = splined.block(key) + np.testing.assert_allclose( + block_splined.values, block_analytic.values, rtol=1e-6, atol=1e-6 + ) + + +def test_lode_center_contribution(): + """Compare the numerical center_contribution calculation with the analytical + formula""" + + def center_contribution_analytical(radial_size, smearing, gto_radius): + result = np.zeros((radial_size)) + + normalization = 1.0 / (np.pi * smearing**2) ** (3 / 4) + sigma_radial = np.ones(radial_size, dtype=float) + + for n in range(1, radial_size): + sigma_radial[n] = np.sqrt(n) + sigma_radial *= gto_radius / radial_size + + for n in range(radial_size): + tmp = 1.0 / (1.0 / smearing**2 + 1.0 / sigma_radial[n] ** 2) + n_eff = 0.5 * (3 + n) + result[n] = (2 * tmp) ** n_eff * gamma(n_eff) + + result *= normalization * 2 * np.pi / np.sqrt(4 * np.pi) + return result + + gto_radius = 2.0 + max_radial = 5 + smearing = 1.2 + hypers = { + "density": featomic.density.SmearedPowerLaw(smearing=smearing, exponent=0), + "basis": featomic.basis.TensorProduct( + max_angular=4, + radial=featomic.basis.Gto(max_radial=max_radial, radius=gto_radius), + # We choose an accuracy that is lower then the default one (1e-8) + # to limit the time taken by this test. + spline_accuracy=1e-3, + ), + } + + spliner = featomic.splines.LodeSpliner(**hypers) + + np.testing.assert_allclose( + # Numerical evaluation of center contributions + spliner._center_contribution(), + # Analytical evaluation of center contributions + center_contribution_analytical(max_radial + 1, smearing, gto_radius), + rtol=1e-5, + ) + + +def test_custom_radial_integral(): + hypers = { + "cutoff": featomic.cutoff.Cutoff( + radius=0.2, + smoothing=featomic.cutoff.Step(), + ), + "density": featomic.density.Gaussian(width=1.2), + "basis": featomic.basis.TensorProduct( + max_angular=2, + radial=featomic.basis.Gto(max_radial=3, radius=2.0), + ), + } + + n_spline_points = 3 + spliner_gaussian = featomic.splines.SoapSpliner( + **hypers, n_spline_points=n_spline_points + ) + + # Create a custom density that has not type "Gaussian" to trigger full + # numerical evaluation of the radial integral. + class NotAGaussian(featomic.density.AtomicDensity): + def __init__(self, *, width: float): + super().__init__(center_atom_weight=1.0, scaling=None) + self.width = float(width) + + def compute(self, positions: np.ndarray, *, derivative: bool) -> np.ndarray: + width_sq = self.width**2 + x = positions**2 / (2 * width_sq) + + density = np.exp(-x) / (np.pi * width_sq) ** (3 / 4) + + if derivative: + density *= -positions / width_sq + + return density + + hypers["density"] = NotAGaussian(width=1.2) + + spliner_custom = featomic.splines.SoapSpliner( + **hypers, n_spline_points=n_spline_points + ) + + splines_gaussian = spliner_gaussian.get_hypers()["basis"].by_angular + splines_custom = spliner_custom.get_hypers()["basis"].by_angular + + for ell in [0, 1, 2]: + spline_gaussian = splines_gaussian[ell].spline + spline_custom = splines_custom[ell].spline + + assert np.all(spline_gaussian.positions == spline_custom.positions) + + np.testing.assert_allclose( + spline_gaussian.values, + spline_custom.values, + atol=1e-9, + ) + + np.testing.assert_allclose( + spline_gaussian.derivatives, + spline_custom.derivatives, + atol=1e-9, + ) diff --git a/python/rascaline/tests/utils/__init__.py b/python/featomic/tests/systems/__init__.py similarity index 100% rename from python/rascaline/tests/utils/__init__.py rename to python/featomic/tests/systems/__init__.py diff --git a/python/rascaline/tests/systems/ase.py b/python/featomic/tests/systems/ase.py similarity index 91% rename from python/rascaline/tests/systems/ase.py rename to python/featomic/tests/systems/ase.py index 71a1bfe0b..fc74633d9 100644 --- a/python/rascaline/tests/systems/ase.py +++ b/python/featomic/tests/systems/ase.py @@ -1,8 +1,8 @@ import numpy as np import pytest -from rascaline import SphericalExpansion -from rascaline.systems import AseSystem +from featomic import SphericalExpansion +from featomic.systems import AseSystem ase = pytest.importorskip("ase") @@ -167,16 +167,22 @@ def test_same_spherical_expansion(): calculator = SphericalExpansion( # make sure to choose a cutoff larger then the cell to test for pairs crossing # multiple periodic boundaries - cutoff=9, - max_radial=5, - max_angular=5, - atomic_gaussian_width=0.3, - radial_basis={"Gto": {}}, - center_atom_weight=1.0, - cutoff_function={"Step": {}}, + cutoff={ + "radius": 9.0, + "smoothing": {"type": "Step"}, + }, + basis={ + "type": "TensorProduct", + "max_angular": 4, + "radial": {"type": "Gto", "max_radial": 4}, + }, + density={ + "type": "Gaussian", + "width": 0.3, + }, ) - rascaline_nl = calculator.compute( + featomic_nl = calculator.compute( system, gradients=["positions", "strain", "cell"], use_native_system=True ) @@ -184,7 +190,7 @@ def test_same_spherical_expansion(): system, gradients=["positions", "strain", "cell"], use_native_system=False ) - for key, block in rascaline_nl.items(): + for key, block in featomic_nl.items(): ase_block = ase_nl.block(key) assert ase_block.samples == block.samples diff --git a/python/rascaline/tests/systems/chemfiles.py b/python/featomic/tests/systems/chemfiles.py similarity index 87% rename from python/rascaline/tests/systems/chemfiles.py rename to python/featomic/tests/systems/chemfiles.py index 7e09e0dac..d3c0d8491 100644 --- a/python/rascaline/tests/systems/chemfiles.py +++ b/python/featomic/tests/systems/chemfiles.py @@ -1,9 +1,9 @@ import numpy as np import pytest -from rascaline.calculators import DummyCalculator -from rascaline.status import RascalError -from rascaline.systems import ChemfilesSystem +from featomic.calculators import DummyCalculator +from featomic.status import FeatomicError +from featomic.systems import ChemfilesSystem chemfiles = pytest.importorskip("chemfiles") @@ -58,9 +58,9 @@ def test_compute(): message = ( "error from external code \\(status -1\\): " - "call to rascal_system_t.compute_neighbors failed" + "call to featomic_system_t.compute_neighbors failed" ) - with pytest.raises(RascalError, match=message) as cm: + with pytest.raises(FeatomicError, match=message) as cm: calculator.compute(frame, use_native_system=False) cause = "chemfiles systems can only be used with 'use_native_system=True'" diff --git a/python/rascaline/tests/systems/errors.py b/python/featomic/tests/systems/errors.py similarity index 63% rename from python/rascaline/tests/systems/errors.py rename to python/featomic/tests/systems/errors.py index 781a8b9c2..b2ddbd242 100644 --- a/python/rascaline/tests/systems/errors.py +++ b/python/featomic/tests/systems/errors.py @@ -1,8 +1,8 @@ import pytest -from rascaline import RascalError -from rascaline.calculators import DummyCalculator -from rascaline.systems import SystemBase +from featomic import FeatomicError +from featomic.calculators import DummyCalculator +from featomic.systems import SystemBase class UnimplementedSystem(SystemBase): @@ -15,9 +15,9 @@ def test_unimplemented(): message = ( "error from external code \\(status -1\\): " - "call to rascal_system_t.types failed" + "call to featomic_system_t.types failed" ) - with pytest.raises(RascalError, match=message) as cm: + with pytest.raises(FeatomicError, match=message) as cm: calculator.compute(system, use_native_system=False) assert cm.value.__cause__.args[0] == "System.types method is not implemented" diff --git a/python/rascaline/tests/systems/pyscf.py b/python/featomic/tests/systems/pyscf.py similarity index 97% rename from python/rascaline/tests/systems/pyscf.py rename to python/featomic/tests/systems/pyscf.py index ac742f198..cbec4007b 100644 --- a/python/rascaline/tests/systems/pyscf.py +++ b/python/featomic/tests/systems/pyscf.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from rascaline.systems import PyscfSystem +from featomic.systems import PyscfSystem pyscf = pytest.importorskip("pyscf") diff --git a/python/rascaline/tests/test_systems.py b/python/featomic/tests/test_systems.py similarity index 97% rename from python/rascaline/tests/test_systems.py rename to python/featomic/tests/test_systems.py index fd28b3c5a..b65c8893c 100644 --- a/python/rascaline/tests/test_systems.py +++ b/python/featomic/tests/test_systems.py @@ -1,4 +1,4 @@ -from rascaline import SystemBase +from featomic import SystemBase class SystemForTests(SystemBase): diff --git a/python/featomic_torch/AUTHORS b/python/featomic_torch/AUTHORS new file mode 120000 index 000000000..f04b7e8a2 --- /dev/null +++ b/python/featomic_torch/AUTHORS @@ -0,0 +1 @@ +../../AUTHORS \ No newline at end of file diff --git a/python/featomic_torch/LICENSE b/python/featomic_torch/LICENSE new file mode 120000 index 000000000..30cff7403 --- /dev/null +++ b/python/featomic_torch/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/python/featomic_torch/MANIFEST.in b/python/featomic_torch/MANIFEST.in new file mode 100644 index 000000000..931117acb --- /dev/null +++ b/python/featomic_torch/MANIFEST.in @@ -0,0 +1,7 @@ +include featomic-torch-cxx-*.tar.gz +include git_version_info +include build-backend/backend.py + +include pyproject.toml +include AUTHORS +include LICENSE diff --git a/python/featomic_torch/README.rst b/python/featomic_torch/README.rst new file mode 100644 index 000000000..931d4085f --- /dev/null +++ b/python/featomic_torch/README.rst @@ -0,0 +1,4 @@ +featomic-torch +=============== + +This package contains the TorchScript bindings to featomic. diff --git a/python/featomic_torch/build-backend/backend.py b/python/featomic_torch/build-backend/backend.py new file mode 100644 index 000000000..9db6bdeba --- /dev/null +++ b/python/featomic_torch/build-backend/backend.py @@ -0,0 +1,47 @@ +# this is a custom Python build backend wrapping setuptool's to add a build-time +# dependencies to featomic, using the local version if it exists, and otherwise +# falling back to the one on PyPI. +import os + +from setuptools import build_meta + + +ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..")) +FEATOMIC_SRC = os.path.realpath(os.path.join(ROOT, "..", "featomic")) +FORCED_FEATOMIC_VERSION = os.environ.get("FEATOMIC_TORCH_BUILD_WITH_FEATOMIC_VERSION") + +FEATOMIC_NO_LOCAL_DEPS = os.environ.get("FEATOMIC_NO_LOCAL_DEPS", "0") == "1" + +if FORCED_FEATOMIC_VERSION is not None: + # force a specific version for metatensor-core, this is used when checking the build + # from a sdist on a non-released version + FEATOMIC_DEP = f"featomic =={FORCED_FEATOMIC_VERSION}" + +elif not FEATOMIC_NO_LOCAL_DEPS and os.path.exists(FEATOMIC_SRC): + # we are building from a git checkout + FEATOMIC_DEP = f"featomic @ file://{FEATOMIC_SRC}" +else: + # we are building from a sdist + FEATOMIC_DEP = "featomic >=0.6.0,<0.7.0" + +FORCED_TORCH_VERSION = os.environ.get("FEATOMIC_TORCH_BUILD_WITH_TORCH_VERSION") +if FORCED_TORCH_VERSION is not None: + TORCH_DEP = f"torch =={FORCED_TORCH_VERSION}" +else: + TORCH_DEP = "torch >=1.12" + + +get_requires_for_build_sdist = build_meta.get_requires_for_build_sdist +prepare_metadata_for_build_wheel = build_meta.prepare_metadata_for_build_wheel +build_wheel = build_meta.build_wheel +build_sdist = build_meta.build_sdist + + +def get_requires_for_build_wheel(config_settings=None): + defaults = build_meta.get_requires_for_build_wheel(config_settings) + return defaults + [ + "cmake", + TORCH_DEP, + "metatensor-torch >=0.6.0,<0.7.0", + FEATOMIC_DEP, + ] diff --git a/python/rascaline-torch/rascaline/torch/__init__.py b/python/featomic_torch/featomic/torch/__init__.py similarity index 80% rename from python/rascaline-torch/rascaline/torch/__init__.py rename to python/featomic_torch/featomic/torch/__init__.py index 0d63d3242..202d30f55 100644 --- a/python/rascaline-torch/rascaline/torch/__init__.py +++ b/python/featomic_torch/featomic/torch/__init__.py @@ -1,7 +1,7 @@ import importlib.metadata -__version__ = importlib.metadata.version("rascaline-torch") +__version__ = importlib.metadata.version("featomic-torch") from ._c_lib import _load_library @@ -12,8 +12,8 @@ from . import utils # noqa: E402, F401 from .calculator_base import CalculatorModule, register_autograd # noqa: E402, F401 -# don't forget to also update `rascaline/__init__.py` and -# `rascaline/torch/calculators.py` when modifying this file +# don't forget to also update `featomic/__init__.py` and +# `featomic/torch/calculators.py` when modifying this file from .calculators import ( # noqa: E402, F401 AtomicComposition, LodeSphericalExpansion, diff --git a/python/featomic_torch/featomic/torch/_c_lib.py b/python/featomic_torch/featomic/torch/_c_lib.py new file mode 100644 index 000000000..94efc2572 --- /dev/null +++ b/python/featomic_torch/featomic/torch/_c_lib.py @@ -0,0 +1,138 @@ +import glob +import os +import re +import sys +from collections import namedtuple + +import metatensor.torch +import torch + +import featomic + +from ._build_versions import BUILD_FEATOMIC_VERSION + + +Version = namedtuple("Version", ["major", "minor", "patch"]) + + +def parse_version(version): + match = re.match(r"(\d+)\.(\d+)\.(\d+).*", version) + if match: + return Version(*map(int, match.groups())) + else: + raise ValueError("Invalid version string format") + + +def version_compatible(actual, required): + actual = parse_version(actual) + required = parse_version(required) + + if actual.major != required.major: + return False + elif actual.minor != required.minor: + return False + else: + return True + + +if not version_compatible(featomic.__version__, BUILD_FEATOMIC_VERSION): + raise ImportError( + f"Trying to load featomic-torch with featomic v{featomic.__version__}, " + f"but it was compiled against featomic v{BUILD_FEATOMIC_VERSION}, which " + "is not ABI compatible" + ) + +_HERE = os.path.realpath(os.path.dirname(__file__)) + + +def _lib_path(): + torch_version = parse_version(torch.__version__) + install_prefix = os.path.join( + _HERE, f"torch-{torch_version.major}.{torch_version.minor}" + ) + + if os.path.exists(install_prefix): + if sys.platform.startswith("darwin"): + path = os.path.join(install_prefix, "lib", "libfeatomic_torch.dylib") + windows = False + elif sys.platform.startswith("linux"): + path = os.path.join(install_prefix, "lib", "libfeatomic_torch.so") + windows = False + elif sys.platform.startswith("win"): + path = os.path.join(install_prefix, "bin", "featomic_torch.dll") + windows = True + else: + raise ImportError("Unknown platform. Please edit this file") + + if os.path.isfile(path): + if windows: + _check_dll(path) + return path + else: + raise ImportError("Could not find featomic_torch shared library at " + path) + + # gather which torch version(s) the current install was built + # with to create the error message + existing_versions = [] + for prefix in glob.glob(os.path.join(_HERE, "torch-*")): + existing_versions.append(os.path.basename(prefix)[6:]) + + if len(existing_versions) == 1: + raise ImportError( + f"Trying to load featomic-torch with torch v{torch.__version__}, " + f"but it was compiled against torch v{existing_versions[0]}, which " + "is not ABI compatible" + ) + else: + all_versions = ", ".join(map(lambda version: f"v{version}", existing_versions)) + raise ImportError( + f"Trying to load featomic-torch with torch v{torch.__version__}, " + f"we found builds for torch {all_versions}; which are not ABI compatible.\n" + "You can try to re-install from source with " + "`pip install featomic-torch --no-binary=featomic-torch`" + ) + + +def _check_dll(path): + """ + Check if the DLL pointer size matches Python (32-bit or 64-bit) + """ + import platform + import struct + + IMAGE_FILE_MACHINE_I386 = 332 + IMAGE_FILE_MACHINE_AMD64 = 34404 + + machine = None + with open(path, "rb") as fd: + header = fd.read(2).decode(encoding="utf-8", errors="strict") + if header != "MZ": + raise ImportError(path + " is not a DLL") + else: + fd.seek(60) + header = fd.read(4) + header_offset = struct.unpack(" List[NeighborListOptions]: NeighborListOptions( cutoff=cutoff, full_list=False, - # we will re-filter the NL when converting to rascaline internal + # we will re-filter the NL when converting to featomic internal # type, so we don't need the engine to pre-filter it for us strict=False, - requestor="rascaline", + requestor="featomic", ) ) return options @@ -111,7 +109,7 @@ def compute( .. seealso:: - :py:func:`rascaline.calculators.CalculatorBase.compute` for more information + :py:func:`featomic.calculators.CalculatorBase.compute` for more information on the different parameters of this function. :param systems: single system or list of systems on which to run the @@ -127,17 +125,17 @@ def compute( :param use_native_system: This can only be ``True``, and is here for compatibility with the same parameter on - :py:meth:`rascaline.calculators.CalculatorBase.compute`. + :py:meth:`featomic.calculators.CalculatorBase.compute`. :param selected_samples: Set of samples on which to run the calculation, with the same meaning as in - :py:func:`rascaline.calculators.CalculatorBase.compute`. + :py:func:`featomic.calculators.CalculatorBase.compute`. :param selected_properties: Set of properties to compute, with the same meaning - as in :py:func:`rascaline.calculators.CalculatorBase.compute`. + as in :py:func:`featomic.calculators.CalculatorBase.compute`. :param selected_keys: Selection for the keys to include in the output, with the - same meaning as in :py:func:`rascaline.calculators.CalculatorBase.compute`. + same meaning as in :py:func:`featomic.calculators.CalculatorBase.compute`. """ if gradients is None: gradients = [] @@ -145,11 +143,11 @@ def compute( if not isinstance(systems, list): systems = [systems] - # We have this parameter to have the same API as rascaline. + # We have this parameter to have the same API as featomic. if not use_native_system: raise ValueError("only `use_native_system=True` is supported") - options = torch.classes.rascaline.CalculatorOptions() + options = torch.classes.featomic.CalculatorOptions() options.gradients = gradients options.selected_samples = selected_samples options.selected_properties = selected_properties diff --git a/python/rascaline-torch/rascaline/torch/calculators.py b/python/featomic_torch/featomic/torch/calculators.py similarity index 71% rename from python/rascaline-torch/rascaline/torch/calculators.py rename to python/featomic_torch/featomic/torch/calculators.py index 07c8af04b..18544da4c 100644 --- a/python/rascaline-torch/rascaline/torch/calculators.py +++ b/python/featomic_torch/featomic/torch/calculators.py @@ -1,7 +1,7 @@ import importlib import sys -import rascaline.calculators +import featomic.calculators from .calculator_base import CalculatorModule @@ -21,28 +21,28 @@ # \______(_______;;; __;;; # # -# This module tries to re-use code from `rascaline.calculators`, which contains a more +# This module tries to re-use code from `featomic.calculators`, which contains a more # user-friendly interface to the different calculator. At the C-API level calculators -# are just defined by a name & JSON parameter string. `rascaline.calculators` defines +# are just defined by a name & JSON parameter string. `featomic.calculators` defines # one class for each name and set the `__init__` parameters with the top-level keys of # the JSON parameters. # # To achieve this, we import the module in a special mode with `importlib`, defining a # global variable `CalculatorBase` which is pointing to `CalculatorModule`. Then, -# `rascaline.calculators` checks if `CalculatorBase` is defined and otherwise imports it -# from `rascaline.calculator_base`. +# `featomic.calculators` checks if `CalculatorBase` is defined and otherwise imports it +# from `featomic.calculator_base`. # # This means the same code is used to define two versions of each class: one will be -# used in `rascaline` and have a base class of `rascaline.CalculatorBase`, and one in -# `rascaline.torch` with base classes `rascaline.torch.CalculatorModule` and +# used in `featomic` and have a base class of `featomic.CalculatorBase`, and one in +# `featomic.torch` with base classes `featomic.torch.CalculatorModule` and # `torch.nn.Module`. spec = importlib.util.spec_from_file_location( # create a module with this name - "rascaline.torch.calculators", + "featomic.torch.calculators", # using the code from there - rascaline.calculators.__file__, + featomic.calculators.__file__, ) module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module diff --git a/python/rascaline-torch/rascaline/torch/utils.py b/python/featomic_torch/featomic/torch/clebsch_gordan.py similarity index 75% rename from python/rascaline-torch/rascaline/torch/utils.py rename to python/featomic_torch/featomic/torch/clebsch_gordan.py index a0b08d158..a9aa2bc78 100644 --- a/python/rascaline-torch/rascaline/torch/utils.py +++ b/python/featomic_torch/featomic/torch/clebsch_gordan.py @@ -6,26 +6,23 @@ import metatensor.torch import torch -import rascaline.utils +import featomic.utils from .calculator_base import CalculatorModule from .system import System -_HERE = os.path.dirname(__file__) - - -# For details what is happening here take a look an `rascaline.torch.calculators`. +# For details what is happening here take a look an `featomic.torch.calculators`. # create the `_backend` module as an empty module spec = importlib.util.spec_from_loader( - "rascaline.torch.utils._backend", + "featomic.torch.clebsch_gordan._backend", loader=None, ) module = importlib.util.module_from_spec(spec) # This module only exposes a handful of things, defined here. Any changes here MUST also -# be made to the `metatensor/operations/_classes.py` file, which is used in non -# TorchScript mode. +# be made to the `featomic/clebsch_gordan/_backend.py` file, which is used in +# non-TorchScript mode. module.__dict__["BACKEND_IS_METATENSOR_TORCH"] = True module.__dict__["Labels"] = metatensor.torch.Labels @@ -57,7 +54,7 @@ def is_labels(obj: Any): return isinstance(obj, metatensor.torch.Labels) -if os.environ.get("RASCALINE_IMPORT_FOR_SPHINX") is None: +if os.environ.get("FEATOMIC_IMPORT_FOR_SPHINX") is None: is_labels = torch.jit.script(is_labels) module.__dict__["is_labels"] = is_labels @@ -78,23 +75,15 @@ def check_isinstance(obj, ty): # register the module in sys.modules, so future import find it directly sys.modules[spec.name] = module -# create a module named `rascaline.torch.utils` using code from -# `rascaline.utils` +# create a module named `featomic.torch.clebsch_gordan` using code from +# `featomic.clebsch_gordan` spec = importlib.util.spec_from_file_location( - "rascaline.torch.utils", rascaline.utils.__file__ + "featomic.torch.clebsch_gordan", featomic.clebsch_gordan.__file__ ) module = importlib.util.module_from_spec(spec) - -cmake_prefix_path = os.path.realpath(os.path.join(_HERE, "..", "lib", "cmake")) -""" -Path containing the CMake configuration files for the underlying C library -""" - -module.__dict__["cmake_prefix_path"] = cmake_prefix_path - -# override `rascaline.torch.utils` (the module associated with the current file) +# override `featomic.torch.clebsch_gordan` (the module associated with the current file) # with the newly created module sys.modules[spec.name] = module spec.loader.exec_module(module) diff --git a/python/rascaline-torch/rascaline/torch/system.py b/python/featomic_torch/featomic/torch/system.py similarity index 89% rename from python/rascaline-torch/rascaline/torch/system.py rename to python/featomic_torch/featomic/torch/system.py index f0bba1599..ba8112e7b 100644 --- a/python/rascaline-torch/rascaline/torch/system.py +++ b/python/featomic_torch/featomic/torch/system.py @@ -3,14 +3,13 @@ import numpy as np import torch from metatensor.torch.atomistic import System -from packaging import version -import rascaline +import featomic @overload def systems_to_torch( - systems: rascaline.systems.IntoSystem, + systems: featomic.systems.IntoSystem, positions_requires_grad: Optional[bool] = None, cell_requires_grad: Optional[bool] = None, ) -> System: @@ -19,7 +18,7 @@ def systems_to_torch( @overload def systems_to_torch( - systems: Sequence[rascaline.systems.IntoSystem], + systems: Sequence[featomic.systems.IntoSystem], positions_requires_grad: Optional[bool] = None, cell_requires_grad: Optional[bool] = None, ) -> List[System]: @@ -36,7 +35,7 @@ def systems_to_torch( :py:class:`metatensor.torch.atomistic.System`, putting all the data in :py:class:`torch.Tensor` and making the overall object compatible with TorchScript. - :param system: any system supported by rascaline. If this is an iterable of system, + :param system: any system supported by featomic. If this is an iterable of system, this function converts them all and returns a list of converted systems. :param positions_requires_grad: The value of ``requires_grad`` on the output @@ -61,7 +60,7 @@ def systems_to_torch( def _system_to_torch(system, positions_requires_grad, cell_requires_grad): if not _is_torch_system(system): - system = rascaline.systems.wrap_system(system) + system = featomic.systems.wrap_system(system) system = System( types=torch.tensor(system.types()), positions=torch.tensor(system.positions()), @@ -86,7 +85,8 @@ def _is_torch_system(system): if not isinstance(system, torch.ScriptObject): return False - if version.parse(torch.__version__) >= version.parse("2.1"): + torch_version_tuple = tuple(map(int, torch.__version__.split(".")[:2])) + if torch_version_tuple >= (2, 1): return system._type().name() == "System" # For older torch version, we check that we have the right properties diff --git a/python/featomic_torch/featomic/torch/utils.py b/python/featomic_torch/featomic/torch/utils.py new file mode 100644 index 000000000..9e9eb8a3e --- /dev/null +++ b/python/featomic_torch/featomic/torch/utils.py @@ -0,0 +1,19 @@ +import os + +import torch + +from ._c_lib import parse_version + + +_HERE = os.path.dirname(__file__) + + +_TORCH_VERSION = parse_version(torch.__version__) +install_prefix = os.path.join( + _HERE, f"torch-{_TORCH_VERSION.major}.{_TORCH_VERSION.minor}" +) + +cmake_prefix_path = os.path.join(install_prefix, "lib", "cmake") +""" +Path containing the CMake configuration files for the underlying C++ library +""" diff --git a/python/rascaline-torch/pyproject.toml b/python/featomic_torch/pyproject.toml similarity index 74% rename from python/rascaline-torch/pyproject.toml rename to python/featomic_torch/pyproject.toml index 937b885e9..bec2e4c15 100644 --- a/python/rascaline-torch/pyproject.toml +++ b/python/featomic_torch/pyproject.toml @@ -1,11 +1,11 @@ [project] -name = "rascaline-torch" +name = "featomic-torch" dynamic = ["version", "authors", "dependencies"] requires-python = ">=3.9" readme = "README.rst" license = {text = "BSD-3-Clause"} -description = "TorchScript bindings to rascaline" +description = "TorchScript bindings to featomic" keywords = ["computational science", "machine learning", "molecular modeling", "atomistic representations", "torch"] classifiers = [ @@ -26,20 +26,20 @@ classifiers = [ ] [project.urls] -homepage = "https://luthaf.fr/rascaline/latest/" -documentation = "https://luthaf.fr/rascaline/latest/" -repository = "https://github.com/Luthaf/rascaline" +homepage = "https://metatensor.github.io/featomic/latest/" +documentation = "https://metatensor.github.io/featomic/latest/" +repository = "https://github.com/metatensor/featomic" # changelog = "TODO" ### ======================================================================== ### [build-system] requires = [ - "setuptools >=61", - "wheel >=0.38", - "cmake", + "setuptools", + "wheel", + "packaging", ] -# use a custom build backend to add a dependency on the right version of rascaline +# use a custom build backend to add a dependency on the right version of featomic build-backend = "backend" backend-path = ["build-backend"] @@ -47,10 +47,16 @@ backend-path = ["build-backend"] zip-safe = false [tool.setuptools.packages.find] -include = ["rascaline*"] +include = ["featomic*"] namespaces = true ### ======================================================================== ### + [tool.pytest.ini_options] python_files = ["*.py"] testpaths = ["tests"] + +### ======================================================================== ### + +[tool.uv.pip] +reinstall-package = ["featomic", "featomic-torch"] diff --git a/python/featomic_torch/setup.py b/python/featomic_torch/setup.py new file mode 100644 index 000000000..e6a449312 --- /dev/null +++ b/python/featomic_torch/setup.py @@ -0,0 +1,379 @@ +import glob +import os +import subprocess +import sys + +import packaging +from setuptools import Extension, setup +from setuptools.command.bdist_egg import bdist_egg +from setuptools.command.build_ext import build_ext +from setuptools.command.sdist import sdist +from wheel.bdist_wheel import bdist_wheel + + +ROOT = os.path.realpath(os.path.dirname(__file__)) + +FEATOMIC_PYTHON_SRC = os.path.realpath(os.path.join(ROOT, "..", "featomic")) +FEATOMIC_TORCH_SRC = os.path.realpath(os.path.join(ROOT, "..", "..", "featomic-torch")) + + +class universal_wheel(bdist_wheel): + # When building the wheel, the `wheel` package assumes that if we have a + # binary extension then we are linking to `libpython.so`; and thus the wheel + # is only usable with a single python version. This is not the case for + # here, and the wheel will be compatible with any Python >=3. This is + # tracked in https://github.com/pypa/wheel/issues/185, but until then we + # manually override the wheel tag. + def get_tag(self): + tag = bdist_wheel.get_tag(self) + # tag[2:] contains the os/arch tags, we want to keep them + return ("py3", "none") + tag[2:] + + +class cmake_ext(build_ext): + """Build the native library using cmake""" + + def run(self): + import metatensor + import metatensor.torch + import torch + + import featomic + + source_dir = FEATOMIC_TORCH_SRC + build_dir = os.path.join(ROOT, "build", "cmake-build") + install_dir = os.path.join(os.path.realpath(self.build_lib), "featomic/torch") + + os.makedirs(build_dir, exist_ok=True) + + # Tell CMake where to find featomic & torch + cmake_prefix_path = [ + featomic.utils.cmake_prefix_path, + metatensor.utils.cmake_prefix_path, + metatensor.torch.utils.cmake_prefix_path, + torch.utils.cmake_prefix_path, + ] + + # Install the shared library in a prefix matching the torch version used to + # compile the code. This allows having multiple version of this shared library + # inside the wheel; and dynamically pick the right one. + torch_major, torch_minor, *_ = torch.__version__.split(".") + cmake_install_prefix = os.path.join( + install_dir, f"torch-{torch_major}.{torch_minor}" + ) + + cmake_options = [ + "-DCMAKE_BUILD_TYPE=Release", + f"-DCMAKE_INSTALL_PREFIX={cmake_install_prefix}", + f"-DCMAKE_PREFIX_PATH={';'.join(cmake_prefix_path)}", + ] + + # ==================================================================== # + # HACK: Torch cmake build system has a hard time finding CuDNN, so we + # help it by pointing it to the right files + + # First try using the `nvidia.cudnn` package (dependency of torch on PyPI) + try: + import nvidia.cudnn + + cudnn_root = os.path.dirname(nvidia.cudnn.__file__) + except ImportError: + # Otherwise try to find CuDNN inside PyTorch itself + cudnn_root = os.path.join(torch.utils.cmake_prefix_path, "..", "..") + + cudnn_version = os.path.join(cudnn_root, "include", "cudnn_version.h") + if not os.path.exists(cudnn_version): + # create a minimal cudnn_version.h (with a made-up version), + # because it is not bundled together with the CuDNN shared + # library in PyTorch conda distribution, see + # https://github.com/pytorch/pytorch/issues/47743 + with open(cudnn_version, "w") as fd: + fd.write("#define CUDNN_MAJOR 8\n") + fd.write("#define CUDNN_MINOR 5\n") + fd.write("#define CUDNN_PATCHLEVEL 0\n") + + cmake_options.append(f"-DCUDNN_INCLUDE_DIR={cudnn_root}/include") + cmake_options.append(f"-DCUDNN_LIBRARY={cudnn_root}/lib") + # do not warn if the two variables above aren't used + cmake_options.append("--no-warn-unused-cli") + + # end of HACK + # ==================================================================== # + + subprocess.run( + ["cmake", source_dir, *cmake_options], + cwd=build_dir, + check=True, + ) + subprocess.run( + [ + "cmake", + "--build", + build_dir, + "--parallel", + "--config", + "Release", + "--target", + "install", + ], + check=True, + ) + + with open(os.path.join(install_dir, "_build_versions.py"), "w") as fd: + fd.write("# Autogenerated file, do not edit\n\n\n") + # Store the version of featomic used to build featomic_torch, to give a + # nice error message to the user when trying to load the package + # with an older featomic version installed + fd.write( + "# version of featomic used when compiling this package\n" + f"BUILD_FEATOMIC_VERSION = '{featomic.__version__}'\n" + ) + + +class bdist_egg_disabled(bdist_egg): + """Disabled version of bdist_egg + + Prevents setup.py install performing setuptools' default easy_install, + which it should never ever do. + """ + + def run(self): + sys.exit( + "Aborting implicit building of eggs. " + + "Use `pip install .` or `python setup.py bdist_wheel && pip " + + "install dist/metatensor-*.whl` to install from source." + ) + + +class sdist_generate_data(sdist): + """ + Create a sdist with an additional generated files: + - `git_version_info` + - `featomic-torch-cxx-*.tar.gz` + """ + + def run(self): + n_commits, git_hash = git_version_info() + with open("git_version_info", "w") as fd: + fd.write(f"{n_commits}\n{git_hash}\n") + + generate_cxx_tar() + + # run original sdist + super().run() + + os.unlink("git_version_info") + for path in glob.glob("featomic-torch-cxx-*.tar.gz"): + os.unlink(path) + + +def generate_cxx_tar(): + script = os.path.join(ROOT, "..", "..", "scripts", "package-featomic-torch.sh") + assert os.path.exists(script) + + try: + output = subprocess.run( + ["bash", "--version"], + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + encoding="utf8", + ) + except Exception as e: + raise RuntimeError("could not run `bash`, is it installed?") from e + + output = subprocess.run( + ["bash", script, os.getcwd()], + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + encoding="utf8", + ) + if output.returncode != 0: + stderr = output.stderr + stdout = output.stdout + raise RuntimeError( + "failed to collect C++ sources for Python sdist\n" + f"stdout:\n {stdout}\n\nstderr:\n {stderr}" + ) + + +def git_version_info(): + """ + If git is available and we are building from a checkout, get the number of commits + since the last tag & full hash of the code. Otherwise, this always returns (0, ""). + """ + TAG_PREFIX = "featomic-torch-v" + + if os.path.exists("git_version_info"): + # we are building from a sdist, without git available, but the git + # version was recorded in the `git_version_info` file + with open("git_version_info") as fd: + n_commits = int(fd.readline().strip()) + git_hash = fd.readline().strip() + else: + script = os.path.join(ROOT, "..", "..", "scripts", "git-version-info.py") + assert os.path.exists(script) + + output = subprocess.run( + [sys.executable, script, TAG_PREFIX], + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + encoding="utf8", + ) + + if output.returncode != 0: + raise Exception( + "failed to get git version info.\n" + f"stdout: {output.stdout}\n" + f"stderr: {output.stderr}\n" + ) + elif output.stderr: + print(output.stderr, file=sys.stderr) + n_commits = 0 + git_hash = "" + else: + lines = output.stdout.splitlines() + n_commits = int(lines[0].strip()) + git_hash = lines[1].strip() + + return n_commits, git_hash + + +def create_version_number(version): + version = packaging.version.parse(version) + + n_commits, git_hash = git_version_info() + if n_commits != 0: + # `n_commits` will be non zero only if we have commits since the last tag. This + # mean we are in a pre-release of the next version. So we increase either the + # minor version number or the release candidate number (if we are closing up on + # a release) + if version.pre is not None: + assert version.pre[0] == "rc" + pre = ("rc", version.pre[1] + 1) + release = version.release + else: + major, minor, patch = version.release + release = (major, minor + 1, 0) + pre = None + + # this is using a private API which is intended to become public soon: + # https://github.com/pypa/packaging/pull/698. In the mean time we'll + # use this + version._version = version._version._replace(release=release) + version._version = version._version._replace(pre=pre) + version._version = version._version._replace(dev=("dev", n_commits)) + version._version = version._version._replace(local=(git_hash,)) + + return str(version) + + +if __name__ == "__main__": + if sys.platform == "win32": + # On Windows, starting with PyTorch 2.3, the file shm.dll in torch has a + # dependency on mkl DLLs. When building the code using pip build isolation, pip + # installs the mkl package in a place where the os is not trying to load + # + # This is a very similar fix to https://github.com/pytorch/pytorch/pull/126095, + # except only applying when importing torch from a build-isolation virtual + # environment created by pip (`python -m build` does not seems to suffer from + # this). + import wheel + + pip_virtualenv = os.path.realpath( + os.path.join( + os.path.dirname(wheel.__file__), + "..", + "..", + "..", + "..", + ) + ) + mkl_dll_dir = os.path.join( + pip_virtualenv, + "normal", + "Library", + "bin", + ) + + if os.path.exists(mkl_dll_dir): + os.add_dll_directory(mkl_dll_dir) + + # End of Windows/MKL/PIP hack + + if not os.path.exists(FEATOMIC_TORCH_SRC): + # we are building from a sdist, which should include featomic-torch + # sources as a tarball + tarballs = glob.glob(os.path.join(ROOT, "featomic-torch-cxx-*.tar.gz")) + + if not len(tarballs) == 1: + raise RuntimeError( + "expected a single 'featomic-torch-cxx-*.tar.gz' file containing " + "featomic-torch C++ sources" + ) + + FEATOMIC_TORCH_SRC = os.path.realpath(tarballs[0]) + subprocess.run( + ["cmake", "-E", "tar", "xf", FEATOMIC_TORCH_SRC], + cwd=ROOT, + check=True, + ) + + FEATOMIC_TORCH_SRC = ".".join(FEATOMIC_TORCH_SRC.split(".")[:-2]) + + with open(os.path.join(FEATOMIC_TORCH_SRC, "VERSION")) as fd: + version = create_version_number(fd.read().strip()) + + with open(os.path.join(ROOT, "AUTHORS")) as fd: + authors = fd.read().splitlines() + + if authors[0].startswith(".."): + # handle "raw" symlink files (on Windows or from full repo tarball) + with open(os.path.join(ROOT, authors[0])) as fd: + authors = fd.read().splitlines() + + try: + import torch + + # if we have torch, we are building a wheel, which will only be compatible with + # a single torch version + torch_v_major, torch_v_minor, *_ = torch.__version__.split(".") + torch_version = f"== {torch_v_major}.{torch_v_minor}.*" + except ImportError: + # otherwise we are building a sdist + torch_version = ">= 1.12" + + install_requires = [ + f"torch {torch_version}", + "metatensor-torch >=0.6.0,<0.7.0", + ] + + # when packaging a sdist for release, we should never use local dependencies + FEATOMIC_NO_LOCAL_DEPS = os.environ.get("FEATOMIC_NO_LOCAL_DEPS", "0") == "1" + if not FEATOMIC_NO_LOCAL_DEPS and os.path.exists(FEATOMIC_PYTHON_SRC): + # we are building from a git checkout + install_requires.append(f"featomic @ file://{FEATOMIC_PYTHON_SRC}") + else: + # we are building from a sdist/installing from a wheel + install_requires.append("featomic >=0.6.0,<0.7.0") + + setup( + version=version, + author=", ".join(authors), + install_requires=install_requires, + ext_modules=[ + Extension(name="featomic_torch", sources=[]), + ], + cmdclass={ + "build_ext": cmake_ext, + "bdist_egg": bdist_egg if "bdist_egg" in sys.argv else bdist_egg_disabled, + "bdist_wheel": universal_wheel, + "sdist": sdist_generate_data, + }, + package_data={ + "featomic-torch": [ + "featomic/torch*/bin/*", + "featomic/torch*/lib/*", + "featomic/torch*/include/*", + ] + }, + ) diff --git a/python/rascaline-torch/tests/autograd.py b/python/featomic_torch/tests/autograd.py similarity index 94% rename from python/rascaline-torch/tests/autograd.py rename to python/featomic_torch/tests/autograd.py index 05ce80129..e89dd2641 100644 --- a/python/rascaline-torch/tests/autograd.py +++ b/python/featomic_torch/tests/autograd.py @@ -6,18 +6,24 @@ import torch from metatensor.torch.atomistic import System -import rascaline.torch -from rascaline.torch import SoapPowerSpectrum, SphericalExpansion +import featomic.torch +from featomic.torch import SoapPowerSpectrum, SphericalExpansion HYPERS = { - "cutoff": 8, - "max_radial": 10, - "max_angular": 5, - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "cutoff_function": {"ShiftedCosine": {"width": 0.5}}, - "radial_basis": {"Gto": {}}, + "cutoff": { + "radius": 8.0, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, + }, + "density": { + "type": "Gaussian", + "width": 0.3, + }, + "basis": { + "type": "TensorProduct", + "max_angular": 5, + "radial": {"type": "Gto", "max_radial": 10}, + }, } @@ -124,7 +130,7 @@ def compute(new_positions, new_cell): if same_positions and same_cell: # we can only re-use the calculation when working with the same input - descriptor = rascaline.torch.register_autograd(system, precomputed) + descriptor = featomic.torch.register_autograd(system, precomputed) else: descriptor = calculator(system) diff --git a/python/rascaline-torch/tests/calculator.py b/python/featomic_torch/tests/calculator.py similarity index 96% rename from python/rascaline-torch/tests/calculator.py rename to python/featomic_torch/tests/calculator.py index 392866229..30577f480 100644 --- a/python/rascaline-torch/tests/calculator.py +++ b/python/featomic_torch/tests/calculator.py @@ -6,8 +6,8 @@ from metatensor.torch import Labels, TensorMap from metatensor.torch.atomistic import System -from rascaline.torch import CalculatorModule -from rascaline.torch.calculators import DummyCalculator +from featomic.torch import CalculatorModule +from featomic.torch.calculators import DummyCalculator @pytest.fixture @@ -158,7 +158,7 @@ def test_properties_selection(system): def test_base_classes(): - assert DummyCalculator.__module__ == "rascaline.torch.calculators" + assert DummyCalculator.__module__ == "featomic.torch.calculators" assert DummyCalculator.__bases__ == (CalculatorModule,) assert CalculatorModule.__bases__ == (torch.nn.Module,) @@ -176,7 +176,7 @@ def test_different_device_dtype_errors(system): ] ) - message = "rascaline only supports float64 and float32 data" + message = "featomic only supports float64 and float32 data" with pytest.raises(TypeError, match=message): calculator.compute(system.to(dtype=torch.float16)) @@ -193,7 +193,7 @@ def test_different_device_dtype_errors(system): torch.set_warn_always(True) message = ( - "Systems data is on device .* but rascaline only supports calculations " + "Systems data is on device .* but featomic only supports calculations " "on CPU. All the data will be moved to CPU and then back on device on " "your behalf" ) diff --git a/python/rascaline-torch/tests/utils/cartesian_spherical.py b/python/featomic_torch/tests/clebsch_gordan/cartesian_spherical.py similarity index 97% rename from python/rascaline-torch/tests/utils/cartesian_spherical.py rename to python/featomic_torch/tests/clebsch_gordan/cartesian_spherical.py index dea2cbe96..9f19fc5e8 100644 --- a/python/rascaline-torch/tests/utils/cartesian_spherical.py +++ b/python/featomic_torch/tests/clebsch_gordan/cartesian_spherical.py @@ -2,7 +2,7 @@ import torch from metatensor.torch import Labels, TensorBlock, TensorMap -from rascaline.torch.utils.clebsch_gordan import cartesian_to_spherical +from featomic.torch.clebsch_gordan import cartesian_to_spherical @pytest.fixture diff --git a/python/rascaline-torch/tests/utils/cg_product.py b/python/featomic_torch/tests/clebsch_gordan/cg_product.py similarity index 79% rename from python/rascaline-torch/tests/utils/cg_product.py rename to python/featomic_torch/tests/clebsch_gordan/cg_product.py index 9263da8a5..143d67538 100644 --- a/python/rascaline-torch/tests/utils/cg_product.py +++ b/python/featomic_torch/tests/clebsch_gordan/cg_product.py @@ -7,18 +7,24 @@ from metatensor.torch import Labels from metatensor.torch.atomistic import System -import rascaline.torch -from rascaline.torch.utils.clebsch_gordan import ClebschGordanProduct +import featomic.torch +from featomic.torch.clebsch_gordan import ClebschGordanProduct SPHERICAL_EXPANSION_HYPERS = { - "cutoff": 2.5, - "max_radial": 3, - "max_angular": 3, - "atomic_gaussian_width": 0.2, - "radial_basis": {"Gto": {}}, - "cutoff_function": {"ShiftedCosine": {"width": 0.5}}, - "center_atom_weight": 1.0, + "cutoff": { + "radius": 2.5, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, + }, + "density": { + "type": "Gaussian", + "width": 0.2, + }, + "basis": { + "type": "TensorProduct", + "max_angular": 2, + "radial": {"type": "Gto", "max_radial": 3}, + }, } SELECTED_KEYS = Labels(names=["o3_lambda"], values=torch.tensor([1, 3]).reshape(-1, 1)) @@ -40,8 +46,8 @@ def system(): def spherical_expansion(): - """Returns a rascaline SphericalExpansion""" - calculator = rascaline.torch.SphericalExpansion(**SPHERICAL_EXPANSION_HYPERS) + """Returns a featomic SphericalExpansion""" + calculator = featomic.torch.SphericalExpansion(**SPHERICAL_EXPANSION_HYPERS) return calculator.compute(system()) @@ -56,7 +62,7 @@ def test_torch_script_tensor_compute(selected_keys: Labels, keys_filter): # Initialize the calculator and scripted calculator calculator = ClebschGordanProduct( - max_angular=SPHERICAL_EXPANSION_HYPERS["max_angular"] * 2, + max_angular=SPHERICAL_EXPANSION_HYPERS["basis"]["max_angular"] * 2, keys_filter=keys_filter, ) scripted_calculator = torch.jit.script(calculator) diff --git a/python/rascaline-torch/tests/utils/density_correlations.py b/python/featomic_torch/tests/clebsch_gordan/density_correlations.py similarity index 78% rename from python/rascaline-torch/tests/utils/density_correlations.py rename to python/featomic_torch/tests/clebsch_gordan/density_correlations.py index 0063ec5fb..3d2517562 100644 --- a/python/rascaline-torch/tests/utils/density_correlations.py +++ b/python/featomic_torch/tests/clebsch_gordan/density_correlations.py @@ -7,18 +7,24 @@ from metatensor.torch import Labels from metatensor.torch.atomistic import System -import rascaline.torch -from rascaline.torch.utils.clebsch_gordan import DensityCorrelations +import featomic.torch +from featomic.torch.clebsch_gordan import DensityCorrelations SPHERICAL_EXPANSION_HYPERS = { - "cutoff": 2.5, - "max_radial": 3, - "max_angular": 3, - "atomic_gaussian_width": 0.2, - "radial_basis": {"Gto": {}}, - "cutoff_function": {"ShiftedCosine": {"width": 0.5}}, - "center_atom_weight": 1.0, + "cutoff": { + "radius": 2.5, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, + }, + "density": { + "type": "Gaussian", + "width": 0.2, + }, + "basis": { + "type": "TensorProduct", + "max_angular": 3, + "radial": {"type": "Gto", "max_radial": 3}, + }, } SELECTED_KEYS = Labels(names=["o3_lambda"], values=torch.tensor([1, 3]).reshape(-1, 1)) @@ -40,8 +46,8 @@ def system(): def spherical_expansion(): - """Returns a rascaline SphericalExpansion""" - calculator = rascaline.torch.SphericalExpansion(**SPHERICAL_EXPANSION_HYPERS) + """Returns a featomic SphericalExpansion""" + calculator = featomic.torch.SphericalExpansion(**SPHERICAL_EXPANSION_HYPERS) return calculator.compute(system()) @@ -56,7 +62,7 @@ def test_torch_script_correlate_density_angular_selection( # Initialize the calculator and scripted calculator calculator = DensityCorrelations( n_correlations=1, - max_angular=SPHERICAL_EXPANSION_HYPERS["max_angular"] * 2, + max_angular=SPHERICAL_EXPANSION_HYPERS["basis"]["max_angular"] * 2, skip_redundant=skip_redundant, ) scripted_calculator = torch.jit.script(calculator) diff --git a/python/featomic_torch/tests/clebsch_gordan/equivariant_power_spectrum.py b/python/featomic_torch/tests/clebsch_gordan/equivariant_power_spectrum.py new file mode 100644 index 000000000..d4af57a30 --- /dev/null +++ b/python/featomic_torch/tests/clebsch_gordan/equivariant_power_spectrum.py @@ -0,0 +1,37 @@ +import io + +import torch + +from featomic.torch import SphericalExpansion +from featomic.torch.clebsch_gordan import EquivariantPowerSpectrum + + +SPHEX_HYPERS_SMALL = { + "cutoff": { + "radius": 5.5, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, + }, + "density": { + "type": "Gaussian", + "width": 0.2, + }, + "basis": { + "type": "TensorProduct", + "max_angular": 6, + "radial": {"type": "Gto", "max_radial": 4}, + }, +} + + +def test_jit_save_load(): + calculator = torch.jit.script( + EquivariantPowerSpectrum( + SphericalExpansion(**SPHEX_HYPERS_SMALL), + dtype=torch.float64, + ) + ) + + with io.BytesIO() as buffer: + torch.jit.save(calculator, buffer) + buffer.seek(0) + torch.jit.load(buffer) diff --git a/python/rascaline-torch/tests/utils/power_spectrum.py b/python/featomic_torch/tests/clebsch_gordan/power_spectrum.py similarity index 64% rename from python/rascaline-torch/tests/utils/power_spectrum.py rename to python/featomic_torch/tests/clebsch_gordan/power_spectrum.py index de14cff94..5af8fc084 100644 --- a/python/rascaline-torch/tests/utils/power_spectrum.py +++ b/python/featomic_torch/tests/clebsch_gordan/power_spectrum.py @@ -2,8 +2,8 @@ from metatensor.torch.atomistic import System from packaging import version -from rascaline.torch.calculators import SphericalExpansion -from rascaline.torch.utils import PowerSpectrum +from featomic.torch.calculators import SphericalExpansion +from featomic.torch.clebsch_gordan import PowerSpectrum def system(): @@ -17,28 +17,22 @@ def system(): def spherical_expansion_calculator(): return SphericalExpansion( - cutoff=5.0, - max_radial=6, - max_angular=4, - atomic_gaussian_width=0.3, - center_atom_weight=1.0, - radial_basis={ - "Gto": {}, + cutoff={ + "radius": 5.0, + "smoothing": {"type": "ShiftedCosine", "width": 0.5}, }, - cutoff_function={ - "ShiftedCosine": {"width": 0.5}, + density={ + "type": "Gaussian", + "width": 0.3, + }, + basis={ + "type": "TensorProduct", + "max_angular": 4, + "radial": {"type": "Gto", "max_radial": 5}, }, ) -def test_forward() -> None: - """Test that forward results in the same as compute.""" - ps_compute = PowerSpectrum(spherical_expansion_calculator()).compute(system()) - ps_forward = PowerSpectrum(spherical_expansion_calculator()).forward(system()) - - assert ps_compute.keys == ps_forward.keys - - def check_operation(calculator): # this only runs basic checks functionality checks, and that the code produces # output with the right type diff --git a/python/rascaline-torch/tests/export.py b/python/featomic_torch/tests/export.py similarity index 79% rename from python/rascaline-torch/tests/export.py rename to python/featomic_torch/tests/export.py index b23fa192a..8f2ae2a97 100644 --- a/python/rascaline-torch/tests/export.py +++ b/python/featomic_torch/tests/export.py @@ -14,17 +14,23 @@ ) from metatensor.torch.atomistic.ase_calculator import _compute_ase_neighbors -from rascaline.torch import SoapPowerSpectrum, systems_to_torch +from featomic.torch import SoapPowerSpectrum, systems_to_torch HYPERS = { - "cutoff": 3.6, - "max_radial": 12, - "max_angular": 3, - "atomic_gaussian_width": 0.2, - "center_atom_weight": 1.0, - "radial_basis": {"Gto": {}}, - "cutoff_function": {"ShiftedCosine": {"width": 0.3}}, + "cutoff": { + "radius": 3.6, + "smoothing": {"type": "ShiftedCosine", "width": 0.3}, + }, + "density": { + "type": "Gaussian", + "width": 0.2, + }, + "basis": { + "type": "TensorProduct", + "max_angular": 3, + "radial": {"type": "Gto", "max_radial": 11}, + }, } @@ -37,9 +43,12 @@ def __init__(self, types: List[int]): torch.tensor([(t1, t2) for t1 in types for t2 in types if t1 < t2]), ) - n_max = HYPERS["max_radial"] - l_max = HYPERS["max_angular"] - in_features = (len(types) * (len(types) + 1) * n_max**2 // 4) * (l_max + 1) + n_types = len(types) + max_radial = HYPERS["basis"]["radial"]["max_radial"] + max_angular = HYPERS["basis"]["max_angular"] + in_features = ( + (n_types * (n_types + 1)) * (max_radial + 1) ** 2 // 4 * (max_angular + 1) + ) self.linear = torch.nn.Linear( in_features=in_features, out_features=1, dtype=torch.float64 @@ -91,7 +100,7 @@ def test_export_as_metatensor_model(tmpdir): capabilities = ModelCapabilities( supported_devices=["cpu"], length_unit="A", - interaction_range=HYPERS["cutoff"], + interaction_range=HYPERS["cutoff"]["radius"], atomic_types=[1, 6, 8], dtype="float64", outputs={"energy": energy_output}, @@ -101,9 +110,9 @@ def test_export_as_metatensor_model(tmpdir): # Check we are requesting the right set of neighbors requests = export.requested_neighbor_lists() assert len(requests) == 1 - assert requests[0].cutoff == HYPERS["cutoff"] + assert requests[0].cutoff == HYPERS["cutoff"]["radius"] assert not requests[0].full_list - assert requests[0].requestors() == ["rascaline", "Model.calculator"] + assert requests[0].requestors() == ["featomic", "Model.calculator"] # check we can save the model export.save(os.path.join(tmpdir, "model.pt")) diff --git a/python/rascaline-torch/tests/system.py b/python/featomic_torch/tests/system.py similarity index 97% rename from python/rascaline-torch/tests/system.py rename to python/featomic_torch/tests/system.py index cdebea253..ba03f8d8e 100644 --- a/python/rascaline-torch/tests/system.py +++ b/python/featomic_torch/tests/system.py @@ -2,7 +2,7 @@ import numpy as np import torch -from rascaline.torch import systems_to_torch +from featomic.torch import systems_to_torch def test_system_conversion_from_ase(): diff --git a/python/featomic_torch/tests/utils.py b/python/featomic_torch/tests/utils.py new file mode 100644 index 000000000..95ae70860 --- /dev/null +++ b/python/featomic_torch/tests/utils.py @@ -0,0 +1,7 @@ +import os + +import featomic.torch + + +def test_cmake_prefix(): + assert os.path.exists(featomic.torch.utils.cmake_prefix_path) diff --git a/python/rascaline-torch/.gitignore b/python/rascaline-torch/.gitignore deleted file mode 100644 index 63ebfdb9e..000000000 --- a/python/rascaline-torch/.gitignore +++ /dev/null @@ -1 +0,0 @@ -rascaline-torch.tar.gz diff --git a/python/rascaline-torch/README.rst b/python/rascaline-torch/README.rst deleted file mode 100644 index c26118e02..000000000 --- a/python/rascaline-torch/README.rst +++ /dev/null @@ -1,4 +0,0 @@ -rascaline-torch -=============== - -This package contains the TorchScript bindings to rascaline. diff --git a/python/rascaline-torch/build-backend/backend.py b/python/rascaline-torch/build-backend/backend.py deleted file mode 100644 index 72c8bf849..000000000 --- a/python/rascaline-torch/build-backend/backend.py +++ /dev/null @@ -1,40 +0,0 @@ -# this is a custom Python build backend wrapping setuptool's to add a build-time -# dependencies to rascaline, using the local version if it exists, and otherwise -# falling back to the one on PyPI. -import os -import uuid - -from setuptools import build_meta - - -ROOT = os.path.realpath(os.path.dirname(__file__)) -RASCALINE = os.path.realpath(os.path.join(ROOT, "..", "..", "..")) -if os.path.exists(os.path.join(RASCALINE, "rascaline-c-api")): - # we are building from a git checkout - - # add a random uuid to the file url to prevent pip from using a cached - # wheel for metatensor-core, and force it to re-build from scratch - uuid = uuid.uuid4() - RASCALINE_DEP = f"rascaline @ file://{RASCALINE}?{uuid}" -else: - # we are building from a sdist - RASCALINE_DEP = "rascaline >=0.1.0.dev0,<0.2.0" - - -prepare_metadata_for_build_wheel = build_meta.prepare_metadata_for_build_wheel -build_wheel = build_meta.build_wheel -build_sdist = build_meta.build_sdist - - -def get_requires_for_build_wheel(config_settings=None): - defaults = build_meta.get_requires_for_build_wheel(config_settings) - return defaults + [ - "torch >= 1.12", - "metatensor-torch >=0.6.0,<0.7.0", - RASCALINE_DEP, - ] - - -def get_requires_for_build_sdist(config_settings=None): - defaults = build_meta.get_requires_for_build_sdist(config_settings) - return defaults + [RASCALINE_DEP] diff --git a/python/rascaline-torch/rascaline/torch/_c_lib.py b/python/rascaline-torch/rascaline/torch/_c_lib.py deleted file mode 100644 index e62c4453f..000000000 --- a/python/rascaline-torch/rascaline/torch/_c_lib.py +++ /dev/null @@ -1,117 +0,0 @@ -import os -import re -import sys -from collections import namedtuple - -import metatensor.torch -import torch - -import rascaline - -from ._build_versions import BUILD_RASCALINE_VERSION, BUILD_TORCH_VERSION - - -Version = namedtuple("Version", ["major", "minor", "patch"]) - - -def parse_version(version): - match = re.match(r"(\d+)\.(\d+)\.(\d+).*", version) - if match: - return Version(*map(int, match.groups())) - else: - raise ValueError("Invalid version string format") - - -def version_compatible(actual, required): - actual = parse_version(actual) - required = parse_version(required) - - if actual.major != required.major: - return False - elif actual.minor != required.minor: - return False - else: - return True - - -if not version_compatible(torch.__version__, BUILD_TORCH_VERSION): - raise ImportError( - f"Trying to load rascaline-torch with torch v{torch.__version__}, " - f"but it was compiled against torch v{BUILD_TORCH_VERSION}, which " - "is not ABI compatible" - ) - -if not version_compatible(rascaline.__version__, BUILD_RASCALINE_VERSION): - raise ImportError( - f"Trying to load rascaline-torch with rascaline v{rascaline.__version__}, " - f"but it was compiled against rascaline v{BUILD_RASCALINE_VERSION}, which " - "is not ABI compatible" - ) - -_HERE = os.path.realpath(os.path.dirname(__file__)) - - -def _lib_path(): - if sys.platform.startswith("darwin"): - path = os.path.join(_HERE, "lib", "librascaline_torch.dylib") - windows = False - elif sys.platform.startswith("linux"): - path = os.path.join(_HERE, "lib", "librascaline_torch.so") - windows = False - elif sys.platform.startswith("win"): - path = os.path.join(_HERE, "bin", "rascaline_torch.dll") - windows = True - else: - raise ImportError("Unknown platform. Please edit this file") - - if os.path.isfile(path): - if windows: - _check_dll(path) - return path - - raise ImportError("Could not find rascaline_torch shared library at " + path) - - -def _check_dll(path): - """ - Check if the DLL pointer size matches Python (32-bit or 64-bit) - """ - import platform - import struct - - IMAGE_FILE_MACHINE_I386 = 332 - IMAGE_FILE_MACHINE_AMD64 = 34404 - - machine = None - with open(path, "rb") as fd: - header = fd.read(2).decode(encoding="utf-8", errors="strict") - if header != "MZ": - raise ImportError(path + " is not a DLL") - else: - fd.seek(60) - header = fd.read(4) - header_offset = struct.unpack("= 1.12", - "metatensor-torch >=0.6.0,<0.7.0", - ] - if os.path.exists(RASCALINE_C_API): - # we are building from a git checkout - rascaline_path = os.path.realpath(os.path.join(ROOT, "..", "..")) - - # add a random uuid to the file url to prevent pip from using a cached - # wheel for rascaline, and force it to re-build from scratch - uuid = uuid.uuid4() - install_requires.append(f"rascaline @ file://{rascaline_path}?{uuid}") - else: - # we are building from a sdist/installing from a wheel - install_requires.append("rascaline >=0.1.0.dev0,<0.2.0") - - setup( - version=version, - author=", ".join(authors), - install_requires=install_requires, - ext_modules=[ - Extension(name="rascaline_torch", sources=[]), - ], - cmdclass={ - "build_ext": cmake_ext, - "bdist_egg": bdist_egg if "bdist_egg" in sys.argv else bdist_egg_disabled, - "sdist": sdist_git_version, - }, - package_data={ - "rascaline-torch": [ - "rascaline/torch/bin/*", - "rascaline/torch/lib/*", - "rascaline/torch/include/*", - ] - }, - ) diff --git a/python/rascaline-torch/tests/utils/cmake_prefix.py b/python/rascaline-torch/tests/utils/cmake_prefix.py deleted file mode 100644 index d6172a3bb..000000000 --- a/python/rascaline-torch/tests/utils/cmake_prefix.py +++ /dev/null @@ -1,7 +0,0 @@ -import os - -import rascaline.torch - - -def test_cmake_prefix(): - assert os.path.exists(rascaline.torch.utils.cmake_prefix_path) diff --git a/python/rascaline/examples/le-basis.py b/python/rascaline/examples/le-basis.py deleted file mode 100644 index 21899cdca..000000000 --- a/python/rascaline/examples/le-basis.py +++ /dev/null @@ -1,282 +0,0 @@ -""" -.. _userdoc-tutorials-le-basis: - -LE basis -======== - -.. start-body - -This example illustrates how to generate a spherical expansion using the Laplacian -eigenstate (LE) basis (https://doi.org/10.1063/5.0124363), using two different basis -truncations approaches. The basis can be truncated in the "traditional" way, using all -values below a limit in the angular and radial direction; or using a "ragged -truncation", where basis functions are selected according to an eigenvalue threshold. - -The main ideas behind the LE basis are: - -1. use a basis of controllable *smoothness* (intended in the same sense as the - smoothness of a low-pass-truncated Fourier expansion) -2. apply a "ragged truncation" strategy in which different angular channels are - truncated at a different number of radial channels, so as to obtain more balanced - smoothness level in the radial and angular direction, for a given number of basis - functions. - -Here we use :class:`rascaline.utils.SphericalBesselBasis` to create a spline of the -radial integral corresponding to the LE basis. An detailed how-to guide how to construct -radial integrals is given in :ref:`userdoc-how-to-splined-radial-integral`. -""" - -import ase.io -import matplotlib.pyplot as plt -import numpy as np -from metatensor import Labels, TensorBlock, TensorMap - -import rascaline - - -# %% -# -# Let's start by using a traditional/square basis truncation. Here we will select all -# basis functions with ``l <= max_angular`` and ``n < max_radial``. The basis functions -# are the solution of a radial Laplacian eigenvalue problem (spherical Bessel -# functions). - -cutoff = 4.4 -max_angular = 6 -max_radial = 8 - -# create a spliner for the SOAP radial integral, using delta functions for the atomic -# density and spherical Bessel functions for the basis -spliner = rascaline.utils.SoapSpliner( - cutoff=cutoff, - max_radial=max_radial, - max_angular=max_angular, - basis=rascaline.utils.SphericalBesselBasis( - cutoff=cutoff, max_radial=max_radial, max_angular=max_angular - ), - density=rascaline.utils.DeltaDensity(), - accuracy=1e-8, -) - -# %% -# -# We can now plot the radial integral splines for a couple of functions. This gives an -# idea of the smoothness of the different components - -splined_basis = spliner.compute() -grid = [p["position"] for p in splined_basis["TabulatedRadialIntegral"]["points"]] -values = np.array( - [ - np.array(p["values"]["data"]).reshape(p["values"]["dim"]) - for p in splined_basis["TabulatedRadialIntegral"]["points"] - ] -) - -plt.plot(grid, values[:, 1, 1], "b-", label="l=1, n=1") -plt.plot(grid, values[:, 4, 1], "r-", label="l=4, n=1") -plt.plot(grid, values[:, 1, 4], "g-", label="l=1, n=4") -plt.plot(grid, values[:, 4, 4], "y-", label="l=4, n=4") -plt.xlabel("$r$") -plt.ylabel(r"$R_{nl}$") -plt.legend() -plt.show() - -# %% -# -# We can use this spline basis in a :py:class:`SphericalExpansion` calculator to -# evaluate spherical expansion coefficients. - -calculator = rascaline.SphericalExpansion( - cutoff=cutoff, - max_radial=max_radial, - max_angular=max_angular, - center_atom_weight=1.0, - radial_basis=splined_basis, - atomic_gaussian_width=-1.0, # will not be used due to the delta density above - cutoff_function={"ShiftedCosine": {"width": 0.5}}, -) - -# %% -# -# This calculator defaults to the "traditional" basis function selection, so we have the -# same maximal ``n`` value for all ``l``. - -systems = ase.io.read("dataset.xyz", ":10") - -descriptor = calculator.compute(systems) -descriptor = descriptor.keys_to_properties("neighbor_type") -descriptor = descriptor.keys_to_samples("center_type") - -for key, block in descriptor.items(): - n_max = np.max(block.properties["n"]) + 1 - print(f"l = {key['o3_lambda']}, n_max = {n_max}") - -# %% -# -# **Selecting basis with an eigenvalue threshold** -# -# Now we will calculate the same basis with an eigenvalue threshold. The idea is to -# treat on the same footings the radial and angular dimension, and select all functions -# with a mean Laplacian below a certain threshold. This is similar to the common -# practice in plane-wave electronic-structure methods to use a kinetic energy cutoff -# where :math:`k_x^2 + k_y^2 + k_z^2 < k_\text{max}^2` - -eigenvalue_threshold = 20 - -# %% -# -# Let's start by computing a lot of Laplacian eigenvalues, which are related to the -# squares of the zeros of spherical Bessel functions. - -l_max_large = 49 # just used to get the eigenvalues -n_max_large = 50 # just used to get the eigenvalues - -# compute the zeros of the spherical Bessel functions -zeros_ln = rascaline.utils.SphericalBesselBasis.compute_zeros(l_max_large, n_max_large) - -# %% -# -# We have a 50x50 array containing the position of the zero of the different spherical -# Bessel functions, indexed by ``l`` and ``n``. - -print("zeros_ln.shape =", zeros_ln.shape) -print("zeros_ln =", zeros_ln[:3, :3]) - -# calculate the Laplacian eigenvalues -eigenvalues_ln = zeros_ln**2 / cutoff**2 - -# %% -# -# We can now determine the set of ``l, n`` pairs to include all eigenvalues below the -# threshold. - -max_radial_by_angular = [] -for ell in range(l_max_large + 1): - # for each l, calculate how many radial basis functions we want to include - max_radial = len(np.where(eigenvalues_ln[ell] < eigenvalue_threshold)[0]) - max_radial_by_angular.append(max_radial) - if max_radial_by_angular[-1] == 0: - # all eigenvalues for this `l` are over the threshold - max_radial_by_angular.pop() - max_angular = ell - 1 - break - -# %% -# -# Comparing this eigenvalues threshold with the one based on a square selection, we see -# that the eigenvalues threshold leads to a gradual decrease of ``max_radial`` for high -# ``l`` values - -square_max_angular = 10 -square_max_radial = 4 -plt.fill_between( - [0, square_max_angular], - [square_max_radial, square_max_radial], - label=r"$l_\mathrm{max}$, $n_\mathrm{max}$ threshold " - + f"({(square_max_angular + 1) * square_max_radial} functions)", - color="gray", -) -plt.fill_between( - np.arange(max_angular + 1), - max_radial_by_angular, - label=f"Eigenvalues threshold ({sum(max_radial_by_angular)} functions)", - alpha=0.5, -) -plt.xlabel(r"$\ell$") -plt.ylabel("n radial basis functions") -plt.ylim(-0.5, max_radial_by_angular[0] + 0.5) -plt.legend() -plt.show() - -# %% -# -# **Using a subset of basis functions with rascaline** -# -# We can tweak the default basis selection of rascaline by specifying a larger total -# basis; and then only asking for a subset of properties to be computed. See -# :ref:`userdoc-how-to-property-selection` for more details on properties selection. - -# extract all the atomic types from our dataset -all_atomic_types = list( - np.unique(np.concatenate([system.numbers for system in systems])) -) - -keys = [] -blocks = [] -for center_type in all_atomic_types: - for neighbor_type in all_atomic_types: - for ell in range(max_angular + 1): - max_radial = max_radial_by_angular[ell] - - keys.append([ell, 1, center_type, neighbor_type]) - blocks.append( - TensorBlock( - values=np.zeros((0, max_radial)), - samples=Labels.empty("_"), - components=[], - properties=Labels("n", np.arange(max_radial).reshape(-1, 1)), - ) - ) - -selected_properties = TensorMap( - keys=Labels( - names=["o3_lambda", "o3_sigma", "center_type", "neighbor_type"], - values=np.array(keys), - ), - blocks=blocks, -) - -# %% -# -# With this, we can build a calculator and calculate the spherical expansion -# coefficients - -# the biggest max_radial will be for l=0 -max_radial = max_radial_by_angular[0] - - -# set up a spliner object for the spherical Bessel functions this radial basis will be -# used to compute the spherical expansion -spliner = rascaline.utils.SoapSpliner( - cutoff=cutoff, - max_radial=max_radial, - max_angular=max_angular, - basis=rascaline.utils.SphericalBesselBasis( - cutoff=cutoff, max_radial=max_radial, max_angular=max_angular - ), - density=rascaline.utils.DeltaDensity(), - accuracy=1e-8, -) - -calculator = rascaline.SphericalExpansion( - cutoff=cutoff, - max_radial=max_radial, - max_angular=max_angular, - center_atom_weight=1.0, - radial_basis=spliner.compute(), - atomic_gaussian_width=-1.0, # will not be used due to the delta density above - cutoff_function={"ShiftedCosine": {"width": 0.5}}, -) - -# %% -# -# And check that we do get the expected Eigenvalues truncation for the calculated -# features! - -descriptor = calculator.compute( - systems, - # we tell the calculator to only compute the selected properties - # (the desired set of (l,n) expansion coefficients - selected_properties=selected_properties, -) - -descriptor = descriptor.keys_to_properties("neighbor_type") -descriptor = descriptor.keys_to_samples("center_type") - -for key, block in descriptor.items(): - n_max = np.max(block.properties["n"]) + 1 - print(f"l = {key['o3_lambda']}, n_max = {n_max}") - -# %% -# -# .. end-body diff --git a/python/rascaline/examples/splined-radial-integral.py b/python/rascaline/examples/splined-radial-integral.py deleted file mode 100644 index f3a2d9a16..000000000 --- a/python/rascaline/examples/splined-radial-integral.py +++ /dev/null @@ -1,165 +0,0 @@ -""" -Splined radial integrals -======================== - -.. start-body - -This example illustrates how to generate splined radial basis functions/integrals, using -a "rectangular" Laplacian eigenstate (LE) basis (https://doi.org/10.1063/5.0124363) as -the example, i.e, a LE basis truncated with ``l_max``, ``n_max`` hyper-parameters. - -Note that the same basis is also directly available through -:class:`rascaline.utils.SphericalBesselBasis` with an how-to guide given in -:ref:`userdoc-how-to-le-basis`. -""" - -# %% - -import ase -import numpy as np -import scipy as sp -from scipy.special import spherical_jn as j_l - -from rascaline import SphericalExpansion -from rascaline.utils import RadialIntegralFromFunction, SphericalBesselBasis - - -# %% -# Set some hyper-parameters - -max_angular = 6 -max_radial = 8 -cutoff = 5.0 - -# %% -# -# where ``cutoff`` is also the radius of the LE sphere. Now we compute the zeros of the -# spherical bessel functions. - -z_ln = SphericalBesselBasis.compute_zeros(max_angular, max_radial) -z_nl = z_ln.T - -# %% -# and define the radial basis functions - - -def R_nl(n, el, r): - # Un-normalized LE radial basis functions - return j_l(el, z_nl[n, el] * r / cutoff) - - -def N_nl(n, el): - # Normalization factor for LE basis functions, excluding the a**(-1.5) factor - def function_to_integrate_to_get_normalization_factor(x): - return j_l(el, x) ** 2 * x**2 - - integral, _ = sp.integrate.quadrature( - function_to_integrate_to_get_normalization_factor, 0.0, z_nl[n, el] - ) - return (1.0 / z_nl[n, el] ** 3 * integral) ** (-0.5) - - -def laplacian_eigenstate_basis(n, el, r): - R = np.zeros_like(r) - for i in range(r.shape[0]): - R[i] = R_nl(n, el, r[i]) - return N_nl(n, el) * R * cutoff ** (-1.5) - - -# %% -# Quick normalization check: - -normalization_check_integral, _ = sp.integrate.quadrature( - lambda x: laplacian_eigenstate_basis(1, 1, x) ** 2 * x**2, - 0.0, - cutoff, -) -print(f"Normalization check (needs to be close to 1): {normalization_check_integral}") - - -# %% -# Now the derivatives (by finite differences): - - -def laplacian_eigenstate_basis_derivative(n, el, r): - delta = 1e-6 - all_derivatives_except_at_zero = ( - laplacian_eigenstate_basis(n, el, r[1:] + delta) - - laplacian_eigenstate_basis(n, el, r[1:] - delta) - ) / (2.0 * delta) - derivative_at_zero = ( - laplacian_eigenstate_basis(n, el, np.array([delta / 10.0])) - - laplacian_eigenstate_basis(n, el, np.array([0.0])) - ) / (delta / 10.0) - return np.concatenate([derivative_at_zero, all_derivatives_except_at_zero]) - - -# %% -# The radial basis functions and their derivatives can be input into a spline generator -# class. This will output the positions of the spline points, the values of the basis -# functions evaluated at the spline points, and the corresponding derivatives. - -spliner = RadialIntegralFromFunction( - radial_integral=laplacian_eigenstate_basis, - radial_integral_derivative=laplacian_eigenstate_basis_derivative, - spline_cutoff=cutoff, - max_radial=max_radial, - max_angular=max_angular, - accuracy=1e-5, -) - -# %% -# The, we feed the splines to the Rust calculator: Note that the -# ``atomic_gaussian_width`` will be ignored since we are not uisng a Gaussian basis. - -hypers_spherical_expansion = { - "cutoff": cutoff, - "max_radial": max_radial, - "max_angular": max_angular, - "center_atom_weight": 0.0, - "radial_basis": spliner.compute(), - "atomic_gaussian_width": 1.0, # ignored - "cutoff_function": {"Step": {}}, -} -calculator = SphericalExpansion(**hypers_spherical_expansion) - -# %% -# -# Create dummy systems to test if the calculator outputs correct radial functions: - - -def get_dummy_systems(r_array): - dummy_systems = [] - for r in r_array: - dummy_systems.append(ase.Atoms("CH", positions=[(0, 0, 0), (0, 0, r)])) - return dummy_systems - - -r = np.linspace(0.1, 4.9, 20) -systems = get_dummy_systems(r) -spherical_expansion_coefficients = calculator.compute(systems) - -# %% -# Extract ``l = 0`` features and check that the ``n = 2`` predictions are the same: - -block_C_l0 = spherical_expansion_coefficients.block( - center_type=6, o3_lambda=0, neighbor_type=1 -) -block_C_l0_n2 = block_C_l0.values[:, :, 2].flatten() -spherical_harmonics_0 = 1.0 / np.sqrt(4.0 * np.pi) - -# %% -# radial function = feature / spherical harmonics function -rascaline_output_radial_function = block_C_l0_n2 / spherical_harmonics_0 - -assert np.allclose( - rascaline_output_radial_function, - laplacian_eigenstate_basis(2, 0, r), - atol=1e-5, -) -print("Assertion passed successfully!") - - -# %% -# -# .. end-body diff --git a/python/rascaline/examples/understanding-hypers.py b/python/rascaline/examples/understanding-hypers.py deleted file mode 100644 index 24f0a0ccf..000000000 --- a/python/rascaline/examples/understanding-hypers.py +++ /dev/null @@ -1,418 +0,0 @@ -""" -.. _userdoc-tutorials-understanding-hypers: - -Changing SOAP hyper parameters -============================== - -In the first :ref:`tutorial ` we show how to -calculate a descriptor using default hyper parameters. Here we will look at how the -change of some hyper parameters affects the values of the descriptor. The -definition of every hyper parameter is given in the :ref:`userdoc-calculators` and -background on the mathematical foundation of the spherical expansion is given in -the :ref:`userdoc-explanations` section. -""" - -# %% -# -# We use the same molecular crystals dataset as in the first -# :ref:`tutorial ` which can downloaded from our -# :download:`website <../../static/dataset.xyz>`. - -# We first import the crucial packages, load the dataset using chemfiles and -# save the first frame in a variable. - -import time - -import chemfiles -import matplotlib.pyplot as plt -import numpy as np - -from rascaline import SphericalExpansion - - -with chemfiles.Trajectory("dataset.xyz") as trajectory: - frames = [frame for frame in trajectory] - -frame0 = frames[0] - -# %% -# -# Increasing ``max_radial`` and ``max_angular`` -# --------------------------------------------- -# -# As mentioned above changing ``max_radial`` has an effect on the accuracy of -# the descriptor and on the computation time. We now will increase the number of -# radial channels and angular channels. Note, that here we directly pass the -# parameters into the ``SphericalExpansion`` class without defining a -# ``HYPERPARAMETERS`` dictionary like we did in the previous tutorial. - -calculator_ext = SphericalExpansion( - cutoff=4.5, - max_radial=12, - max_angular=8, - atomic_gaussian_width=0.3, - center_atom_weight=1.0, - radial_basis={"Gto": {"spline_accuracy": 1e-6}}, - cutoff_function={"ShiftedCosine": {"width": 0.5}}, - radial_scaling={"Willatt2018": {"scale": 2.0, "rate": 1.0, "exponent": 4}}, -) - -descriptor_ext = calculator_ext.compute(frame0) - -# %% -# -# Compared to our previous set of hypers we now have 144 blocks instead of 112 -# because we increased the number of angular channels. - -print(len(descriptor_ext.blocks())) - -# %% -# -# The increase of the radial channels to 12 is reflected in the shape of the 0th -# block values. - -print(descriptor_ext.block(0).values.shape) - -# %% -# -# Note that the increased number of radial and angular channels can increase the -# accuracy of your representation but will increase the computational time -# transforming the coordinates into a descriptor. A very simple time measurement -# of the computation shows that the extended calculator takes more time for -# the computation compared to a calculation using the default hyper parameters - -start_time = time.time() -calculator_ext.compute(frames) -print(f"Extended hypers took {time.time() - start_time:.2f} s.") - -# using smaller max_radial and max_angular, everything else stays the same -calculator_small = SphericalExpansion( - cutoff=4.5, - max_radial=9, - max_angular=6, - atomic_gaussian_width=0.3, - center_atom_weight=1.0, - radial_basis={"Gto": {"spline_accuracy": 1e-6}}, - cutoff_function={"ShiftedCosine": {"width": 0.5}}, - radial_scaling={"Willatt2018": {"scale": 2.0, "rate": 1.0, "exponent": 4}}, -) - -start_time = time.time() -calculator_small.compute(frames) -print(f"Smaller hypers took {time.time() - start_time:.2f} s.") - -# %% -# -# Reducing the ``cutoff`` and the ``center_atom_weight`` -# ------------------------------------------------------ -# -# The cutoff controls how many neighboring atoms are taken into account for a -# descriptor. By decreasing the cutoff from 6 Å to 0.1 Å fewer and fewer atoms -# contribute to the descriptor which can be seen by the reduced range of the -# features. - -for cutoff in [6.0, 4.5, 3.0, 1.0, 0.1]: - calculator_cutoff = SphericalExpansion( - cutoff=cutoff, - max_radial=6, - max_angular=6, - atomic_gaussian_width=0.3, - center_atom_weight=1.0, - radial_basis={"Gto": {"spline_accuracy": 1e-6}}, - cutoff_function={"ShiftedCosine": {"width": 0.5}}, - radial_scaling={"Willatt2018": {"scale": 2.0, "rate": 1.0, "exponent": 4}}, - ) - - descriptor = calculator_cutoff.compute(frame0) - - print(f"Descriptor for cutoff={cutoff} Å: {descriptor.block(0).values[0]}") - -# %% -# -# For a ``cutoff`` of 0.1 Å there is no neighboring atom within the cutoff and -# one could expect all features to be 0. This is not the case because the -# central atom also contributes to the descriptor. We can vary this contribution -# using the ``center_atom_weight`` parameter so that the descriptor finally is 0 -# everywhere. -# -# ..Add a sophisticated and referenced note on how the ``center_atom_weight`` -# could affect ML models. - -for center_weight in [1.0, 0.5, 0.0]: - calculator_cutoff = SphericalExpansion( - cutoff=0.1, - max_radial=6, - max_angular=6, - atomic_gaussian_width=0.3, - center_atom_weight=center_weight, - radial_basis={"Gto": {"spline_accuracy": 1e-6}}, - cutoff_function={"ShiftedCosine": {"width": 0.5}}, - radial_scaling={"Willatt2018": {"scale": 2.0, "rate": 1.0, "exponent": 4}}, - ) - - descriptor = calculator_cutoff.compute(frame0) - - print( - f"Descriptor for center_weight={center_weight}: " - f"{descriptor.block(0).values[0]}" - ) - -# %% -# -# Choosing the ``cutoff_function`` -# -------------------------------- -# -# In a naive descriptor approach all atoms within the cutoff are taken in into -# account equally and atoms without the cutoff are ignored. This behavior is -# implemented using the ``cutoff_function={"Step": {}}`` parameter in each -# calculator. However, doing so means that small movements of an atom near the -# cutoff result in large changes in the descriptor: there is a discontinuity in -# the representation as atoms enter or leave the cutoff. A solution is to use -# some smoothing function to get rid of this discontinuity, such as a shifted -# cosine function: -# -# .. math:: -# -# f(r) = \begin{cases} -# 1 &r < r_c - w,\\ -# 0.5 + 0.5 \cos[\pi (r - r_c + w) / w] &r_c - w < r <= r_c, \\ -# 0 &r_c < r, -# \end{cases} -# -# where :math:`r_\mathrm{c}` is the cutoff distance and :math:`w` the width. -# Such smoothing function is used as a multiplicative weight for the -# contribution to the representation coming from each neighbor one by one -# -# The following functions compute such a shifted cosine weighting. - - -def shifted_cosine(r, cutoff, width): - """A shifted cosine switching function. - - Parameters - ---------- - r : float - distance between neighboring atoms in Å - cutoff : float - cutoff distance in Å - width : float - width of the switching in Å - - Returns - ------- - float - weighting of the features - """ - if r <= (cutoff - width): - return 1.0 - elif r >= cutoff: - return 0.0 - else: - s = np.pi * (r - cutoff + width) / width - return 0.5 * (1.0 + np.cos(s)) - - -# %% -# -# Let us plot the weighting for different widths. - -r = np.linspace(1e-3, 4.5, num=100) - -plt.plot([0, 4.5, 4.5, 5.0], [1, 1, 0, 0], c="k", label=r"Step function") - -for width in [4.5, 2.5, 1.0, 0.5, 0.1]: - weighting_values = [shifted_cosine(r=r_i, cutoff=4.5, width=width) for r_i in r] - plt.plot(r, weighting_values, label=f"Shifted cosine: $width={width}\\,Å$") - -plt.legend() -plt.xlabel(r"distance $r$ from the central atom in $Å$") -plt.ylabel("feature weighting") -plt.show() - -# %% -# -# From the plot we conclude that a larger ``width`` of the shifted cosine -# function will decrease the feature values already for smaller distances ``r`` -# from the central atom. - -# %% -# -# Choosing the ``radial_scaling`` -# ------------------------------- -# -# As mentioned above all atoms within the cutoff are taken equally for a -# descriptor. This might limit the accuracy of a model, so it is sometimes -# useful to weigh neighbors that further away from the central atom less than -# neighbors closer to the central atom. This can be achieved by a -# ``radial_scaling`` function with a long-range algebraic decay and smooth -# behavior at :math:`r \rightarrow 0`. The ``'Willatt2018'`` radial scaling -# available in rascaline corresponds to the function introduced in this -# `publication `_: -# -# .. math:: -# -# u(r) = \begin{cases} -# 1 / (r/r_0)^m & \text{if c=0,} \\ -# 1 & \text{if m=0,} \\ -# c / (c+(r/r_0)^m) & \text{else}, -# \end{cases} -# -# where :math:`c` is the ``rate``, :math:`r_0` is the ``scale`` parameter and -# :math:`m` the ``exponent`` of the RadialScaling function. -# -# The following functions compute such a radial scaling. - - -def radial_scaling(r, rate, scale, exponent): - """Radial scaling function. - - Parameters - ---------- - r : float - distance between neighboring atoms in Å - rate : float - decay rate of the scaling - scale : float - scaling of the distance between atoms in Å - exponent : float - exponent of the decay - - Returns - ------- - float - weighting of the features - """ - if rate == 0: - return 1 / (r / scale) ** exponent - if exponent == 0: - return 1 - else: - return rate / (rate + (r / scale) ** exponent) - - -# %% -# -# In the following we show three different radial scaling functions, where the -# first one uses the parameters we use for the calculation of features in the -# :ref:`first tutorial `. - -r = np.linspace(1e-3, 4.5, num=100) - -plt.axvline(4.5, c="k", ls="--", label="cutoff") - -radial_scaling_params = {"scale": 2.0, "rate": 1.0, "exponent": 4} -plt.plot(r, radial_scaling(r, **radial_scaling_params), label=radial_scaling_params) - -radial_scaling_params = {"scale": 2.0, "rate": 3.0, "exponent": 6} -plt.plot(r, radial_scaling(r, **radial_scaling_params), label=radial_scaling_params) - -radial_scaling_params = {"scale": 2.0, "rate": 0.8, "exponent": 2} -plt.plot(r, radial_scaling(r, **radial_scaling_params), label=radial_scaling_params) - -plt.legend() -plt.xlabel(r"distance $r$ from the central atom in $Å$") -plt.ylabel("feature weighting") -plt.show() - -# %% -# -# In the end the total weight is the product of ``cutoff_function`` and the -# ``radial_scaling`` -# -# .. math: -# -# rs(r) = sc(r) \cdot u(r) -# -# The shape of this function should be a "S" like but the optimal shape depends -# on each dataset. - - -def feature_scaling(r, cutoff, width, rate, scale, exponent): - """Features Scaling factor using cosine shifting and radial scaling. - - Parameters - ---------- - r : float - distance between neighboring atoms - cutoff : float - cutoff distance in Å - width : float - width of the decay in Å - rate : float - decay rate of the scaling - scale : float - scaling of the distance between atoms in Å - exponent : float - exponent of the decay - - Returns - ------- - float - weighting of the features - """ - s = radial_scaling(r, rate, scale, exponent) - s *= np.array([shifted_cosine(ri, cutoff, width) for ri in r]) - return s - - -r = np.linspace(1e-3, 4.5, num=100) - -plt.axvline(4.5, c="k", ls="--", label=r"$r_\mathrm{cut}$") - -radial_scaling_params = {} -plt.plot( - r, - feature_scaling(r, scale=2.0, rate=4.0, exponent=6, cutoff=4.5, width=0.5), - label="feature weighting function", -) - -plt.legend() -plt.xlabel(r"distance $r$ from the central atom $[Å]$") -plt.ylabel("feature weighting") -plt.show() - -# %% -# -# Finally we see how the magnitude of the features further away from the central -# atom reduces when we apply both a ``shifted_cosine`` and a ``radial_scaling``. - -calculator_step = SphericalExpansion( - cutoff=4.5, - max_radial=6, - max_angular=6, - atomic_gaussian_width=0.3, - center_atom_weight=1.0, - radial_basis={"Gto": {"spline_accuracy": 1e-6}}, - cutoff_function={"Step": {}}, -) - -descriptor_step = calculator_step.compute(frame0) -print(f"Step cutoff: {str(descriptor_step.block(0).values[0]):>97}") - -calculator_cosine = SphericalExpansion( - cutoff=4.5, - max_radial=6, - max_angular=6, - atomic_gaussian_width=0.3, - center_atom_weight=1.0, - radial_basis={"Gto": {"spline_accuracy": 1e-6}}, - cutoff_function={"ShiftedCosine": {"width": 0.5}}, -) - -descriptor_cosine = calculator_cosine.compute(frame0) -print(f"Cosine smoothing: {str(descriptor_cosine.block(0).values[0]):>92}") - -calculator_rs = SphericalExpansion( - cutoff=4.5, - max_radial=6, - max_angular=6, - atomic_gaussian_width=0.3, - center_atom_weight=1.0, - radial_basis={"Gto": {"spline_accuracy": 1e-6}}, - cutoff_function={"ShiftedCosine": {"width": 0.5}}, - radial_scaling={"Willatt2018": {"scale": 2.0, "rate": 1.0, "exponent": 4}}, -) - -descriptor_rs = calculator_rs.compute(frame0) - -print(f"cosine smoothing + radial scaling: {str(descriptor_rs.block(0).values[0]):>50}") diff --git a/python/rascaline/rascaline/__init__.py b/python/rascaline/rascaline/__init__.py deleted file mode 100644 index c11deb7a8..000000000 --- a/python/rascaline/rascaline/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -from . import utils # noqa -from .calculator_base import CalculatorBase # noqa - -# don't forget to also update `rascaline/torch/__init__.py` and -# `rascaline/torch/calculators.py` when modifying this file -from .calculators import ( - AtomicComposition, - LodeSphericalExpansion, - NeighborList, - SoapPowerSpectrum, - SoapRadialSpectrum, - SortedDistances, - SphericalExpansion, - SphericalExpansionByPair, -) -from .log import set_logging_callback # noqa -from .profiling import Profiler # noqa -from .status import RascalError # noqa -from .systems import IntoSystem, SystemBase # noqa -from .version import __version__ # noqa - - -__all__ = [ - "AtomicComposition", - "LodeSphericalExpansion", - "NeighborList", - "SoapPowerSpectrum", - "SoapRadialSpectrum", - "SortedDistances", - "SphericalExpansion", - "SphericalExpansionByPair", -] diff --git a/python/rascaline/rascaline/_c_api.py b/python/rascaline/rascaline/_c_api.py deleted file mode 100644 index b9c509fc1..000000000 --- a/python/rascaline/rascaline/_c_api.py +++ /dev/null @@ -1,166 +0,0 @@ -# fmt: off -# flake8: noqa -'''Automatically-generated file, do not edit!!!''' - -import ctypes -import enum -import platform -from ctypes import CFUNCTYPE, POINTER - -from metatensor._c_api import mts_labels_t, mts_tensormap_t -from numpy.ctypeslib import ndpointer - - -arch = platform.architecture()[0] -if arch == "32bit": - c_uintptr_t = ctypes.c_uint32 -elif arch == "64bit": - c_uintptr_t = ctypes.c_uint64 - -RASCAL_SUCCESS = 0 -RASCAL_INVALID_PARAMETER_ERROR = 1 -RASCAL_JSON_ERROR = 2 -RASCAL_UTF8_ERROR = 3 -RASCAL_CHEMFILES_ERROR = 4 -RASCAL_SYSTEM_ERROR = 128 -RASCAL_BUFFER_SIZE_ERROR = 254 -RASCAL_INTERNAL_ERROR = 255 -RASCAL_LOG_LEVEL_ERROR = 1 -RASCAL_LOG_LEVEL_WARN = 2 -RASCAL_LOG_LEVEL_INFO = 3 -RASCAL_LOG_LEVEL_DEBUG = 4 -RASCAL_LOG_LEVEL_TRACE = 5 - - -rascal_status_t = ctypes.c_int32 -rascal_logging_callback_t = CFUNCTYPE(None, ctypes.c_int32, ctypes.c_char_p) - - -class rascal_calculator_t(ctypes.Structure): - pass - - -class rascal_pair_t(ctypes.Structure): - _fields_ = [ - ("first", c_uintptr_t), - ("second", c_uintptr_t), - ("distance", ctypes.c_double), - ("vector", ctypes.c_double * 3), - ("cell_shift_indices", ctypes.c_int32 * 3), - ] - - -class rascal_system_t(ctypes.Structure): - _fields_ = [ - ("user_data", ctypes.c_void_p), - ("size", CFUNCTYPE(rascal_status_t, ctypes.c_void_p, POINTER(c_uintptr_t))), - ("types", CFUNCTYPE(rascal_status_t, ctypes.c_void_p, POINTER(ndpointer(ctypes.c_int32, flags='C_CONTIGUOUS')))), - ("positions", CFUNCTYPE(rascal_status_t, ctypes.c_void_p, POINTER(ndpointer(ctypes.c_double, flags='C_CONTIGUOUS')))), - ("cell", CFUNCTYPE(rascal_status_t, ctypes.c_void_p, POINTER(ctypes.c_double))), - ("compute_neighbors", CFUNCTYPE(rascal_status_t, ctypes.c_void_p, ctypes.c_double)), - ("pairs", CFUNCTYPE(rascal_status_t, ctypes.c_void_p, POINTER(ndpointer(rascal_pair_t, flags='C_CONTIGUOUS')), POINTER(c_uintptr_t))), - ("pairs_containing", CFUNCTYPE(rascal_status_t, ctypes.c_void_p, c_uintptr_t, POINTER(ndpointer(rascal_pair_t, flags='C_CONTIGUOUS')), POINTER(c_uintptr_t))), - ] - - -class rascal_labels_selection_t(ctypes.Structure): - _fields_ = [ - ("subset", POINTER(mts_labels_t)), - ("predefined", POINTER(mts_tensormap_t)), - ] - - -class rascal_calculation_options_t(ctypes.Structure): - _fields_ = [ - ("gradients", POINTER(ctypes.c_char_p)), - ("gradients_count", c_uintptr_t), - ("use_native_system", ctypes.c_bool), - ("selected_samples", rascal_labels_selection_t), - ("selected_properties", rascal_labels_selection_t), - ("selected_keys", POINTER(mts_labels_t)), - ] - - -def setup_functions(lib): - from .status import _check_rascal_status_t - - lib.rascal_last_error.argtypes = [ - - ] - lib.rascal_last_error.restype = ctypes.c_char_p - - lib.rascal_set_logging_callback.argtypes = [ - rascal_logging_callback_t - ] - lib.rascal_set_logging_callback.restype = _check_rascal_status_t - - lib.rascal_basic_systems_read.argtypes = [ - ctypes.c_char_p, - POINTER(POINTER(rascal_system_t)), - POINTER(c_uintptr_t) - ] - lib.rascal_basic_systems_read.restype = _check_rascal_status_t - - lib.rascal_basic_systems_free.argtypes = [ - POINTER(rascal_system_t), - c_uintptr_t - ] - lib.rascal_basic_systems_free.restype = _check_rascal_status_t - - lib.rascal_calculator.argtypes = [ - ctypes.c_char_p, - ctypes.c_char_p - ] - lib.rascal_calculator.restype = POINTER(rascal_calculator_t) - - lib.rascal_calculator_free.argtypes = [ - POINTER(rascal_calculator_t) - ] - lib.rascal_calculator_free.restype = _check_rascal_status_t - - lib.rascal_calculator_name.argtypes = [ - POINTER(rascal_calculator_t), - ctypes.c_char_p, - c_uintptr_t - ] - lib.rascal_calculator_name.restype = _check_rascal_status_t - - lib.rascal_calculator_parameters.argtypes = [ - POINTER(rascal_calculator_t), - ctypes.c_char_p, - c_uintptr_t - ] - lib.rascal_calculator_parameters.restype = _check_rascal_status_t - - lib.rascal_calculator_cutoffs.argtypes = [ - POINTER(rascal_calculator_t), - POINTER(POINTER(ctypes.c_double)), - POINTER(c_uintptr_t) - ] - lib.rascal_calculator_cutoffs.restype = _check_rascal_status_t - - lib.rascal_calculator_compute.argtypes = [ - POINTER(rascal_calculator_t), - POINTER(POINTER(mts_tensormap_t)), - POINTER(rascal_system_t), - c_uintptr_t, - rascal_calculation_options_t - ] - lib.rascal_calculator_compute.restype = _check_rascal_status_t - - lib.rascal_profiling_clear.argtypes = [ - - ] - lib.rascal_profiling_clear.restype = _check_rascal_status_t - - lib.rascal_profiling_enable.argtypes = [ - ctypes.c_bool - ] - lib.rascal_profiling_enable.restype = _check_rascal_status_t - - lib.rascal_profiling_get.argtypes = [ - ctypes.c_char_p, - ctypes.c_char_p, - c_uintptr_t - ] - lib.rascal_profiling_get.restype = _check_rascal_status_t diff --git a/python/rascaline/rascaline/utils/__init__.py b/python/rascaline/rascaline/utils/__init__.py deleted file mode 100644 index cbff94460..000000000 --- a/python/rascaline/rascaline/utils/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -import os - -from .clebsch_gordan import ( # noqa - ClebschGordanProduct, - DensityCorrelations, - calculate_cg_coefficients, - cartesian_to_spherical, -) -from .power_spectrum import PowerSpectrum # noqa -from .splines import ( # noqa - AtomicDensityBase, - DeltaDensity, - GaussianDensity, - GtoBasis, - LodeDensity, - LodeSpliner, - MonomialBasis, - RadialBasisBase, - RadialIntegralFromFunction, - RadialIntegralSplinerBase, - SoapSpliner, - SphericalBesselBasis, -) - - -_HERE = os.path.dirname(__file__) - -cmake_prefix_path = os.path.realpath(os.path.join(_HERE, "..", "lib", "cmake")) -""" -Path containing the CMake configuration files for the underlying C library -""" diff --git a/python/rascaline/rascaline/utils/splines/__init__.py b/python/rascaline/rascaline/utils/splines/__init__.py deleted file mode 100644 index 247aaa83e..000000000 --- a/python/rascaline/rascaline/utils/splines/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from .atomic_density import ( # noqa - AtomicDensityBase, - DeltaDensity, - GaussianDensity, - LodeDensity, -) -from .radial_basis import ( # noqa - GtoBasis, - MonomialBasis, - RadialBasisBase, - SphericalBesselBasis, -) -from .splines import ( # noqa - LodeSpliner, - RadialIntegralFromFunction, - RadialIntegralSplinerBase, - SoapSpliner, -) diff --git a/python/rascaline/rascaline/utils/splines/atomic_density.py b/python/rascaline/rascaline/utils/splines/atomic_density.py deleted file mode 100644 index db7aba428..000000000 --- a/python/rascaline/rascaline/utils/splines/atomic_density.py +++ /dev/null @@ -1,244 +0,0 @@ -r""" -.. _python-atomic-density: - -Atomic Density -============== - -the atomic density function :math:`g(r)`, often chosen to be a Gaussian or Delta -function, that defined the type of density under consideration. For a given central atom -:math:`i` in the system, the total density function :math:`\rho_i(\boldsymbol{r})` -around is then defined as :math:`\rho_i(\boldsymbol{r}) = \sum_{j} g(\boldsymbol{r} - -\boldsymbol{r}_{ij})`. - -Atomic densities are represented as different child class of -:py:class:`rascaline.utils.AtomicDensityBase`: :py:class:`rascaline.utils.DeltaDensity`, -:py:class:`rascaline.utils.GaussianDensity`, and :py:class:`rascaline.utils.LodeDensity` -are provided, and you can implement your own by defining a new class. - -.. autoclass:: rascaline.utils.AtomicDensityBase - :members: - :show-inheritance: - -.. autoclass:: rascaline.utils.DeltaDensity - :members: - :show-inheritance: - -.. autoclass:: rascaline.utils.GaussianDensity - :members: - :show-inheritance: - -.. autoclass:: rascaline.utils.LodeDensity - :members: - :show-inheritance: - -""" - -import warnings -from abc import ABC, abstractmethod -from typing import Union - -import numpy as np - - -try: - from scipy.special import gamma, gammainc - - HAS_SCIPY = True -except ImportError: - HAS_SCIPY = False - - -class AtomicDensityBase(ABC): - """Base class representing atomic densities.""" - - @abstractmethod - def compute(self, positions: Union[float, np.ndarray]) -> Union[float, np.ndarray]: - """Compute the atomic density arising from atoms at ``positions``. - - :param positions: positions to evaluate the atomic densities - :returns: evaluated atomic density - """ - - @abstractmethod - def compute_derivative( - self, positions: Union[float, np.ndarray] - ) -> Union[float, np.ndarray]: - """Derivative of the atomic density arising from atoms at ``positions``. - - :param positions: positions to evaluate the derivatives atomic densities - :returns: evaluated derivative of the atomic density with respect to positions - """ - - -class DeltaDensity(AtomicDensityBase): - r"""Delta atomic densities of the form :math:`g(r)=\delta(r)`.""" - - def compute(self, positions: Union[float, np.ndarray]) -> Union[float, np.ndarray]: - raise ValueError( - "Compute function of the delta density should never called directly." - ) - - def compute_derivative( - self, positions: Union[float, np.ndarray] - ) -> Union[float, np.ndarray]: - raise ValueError( - "Compute derivative function of the delta density should never called " - "directly." - ) - - -class GaussianDensity(AtomicDensityBase): - r"""Gaussian atomic density function. - - In rascaline, we use the convention - - .. math:: - - g(r) = \frac{1}{(\pi \sigma^2)^{3/4}}e^{-\frac{r^2}{2\sigma^2}} \,. - - The prefactor was chosen such that the "L2-norm" of the Gaussian - - .. math:: - - \|g\|^2 = \int \mathrm{d}^3\boldsymbol{r} |g(r)|^2 = 1\,, - - The derivatives of the Gaussian atomic density with respect to the position is - - .. math:: - - g^\prime(r) = - \frac{\partial g(r)}{\partial r} = \frac{-r}{\sigma^2(\pi - \sigma^2)^{3/4}}e^{-\frac{r^2}{2\sigma^2}} \,. - - :param atomic_gaussian_width: Width of the atom-centered gaussian used to create the - atomic density - """ - - def __init__(self, atomic_gaussian_width: float): - self.atomic_gaussian_width = atomic_gaussian_width - - def _compute( - self, positions: Union[float, np.ndarray], derivative: bool = False - ) -> Union[float, np.ndarray]: - atomic_gaussian_width_sq = self.atomic_gaussian_width**2 - x = positions**2 / (2 * atomic_gaussian_width_sq) - - density = np.exp(-x) / (np.pi * atomic_gaussian_width_sq) ** (3 / 4) - - if derivative: - density *= -positions / atomic_gaussian_width_sq - - return density - - def compute(self, positions: Union[float, np.ndarray]) -> Union[float, np.ndarray]: - return self._compute(positions=positions, derivative=False) - - def compute_derivative( - self, positions: Union[float, np.ndarray] - ) -> Union[float, np.ndarray]: - return self._compute(positions=positions, derivative=True) - - -class LodeDensity(AtomicDensityBase): - r"""Smeared power law density, as used in LODE. - - It is defined as - - .. math:: - - g(r) = \frac{1}{\Gamma\left(\frac{p}{2}\right)} - \frac{\gamma\left( \frac{p}{2}, \frac{r^2}{2\sigma^2} \right)} - {r^p}, - - where :math:`p` is the potential exponent, :math:`\Gamma(z)` is the Gamma function - and :math:`\gamma(a, x)` is the incomplete lower Gamma function. However its - evaluation at :math:`r=0` is problematic because :math:`g(r)` is of the form - :math:`0/0`. For practical implementations, it is thus more convenient to rewrite - the density as - - .. math:: - - g(r) = \frac{1}{\Gamma(a)}\frac{1}{\left(2 \sigma^2\right)^a} - \begin{cases} - \frac{1}{a} - \frac{x}{a+1} + \frac{x^2}{2(a+2)} + \mathcal{O}(x^3) - & x < 10^{-5} \\ - \frac{\gamma(a,x)}{x^a} - & x \geq 10^{-5} - \end{cases} - - where :math:`a=p/2`. It is convenient to use the expression for sufficiently small - :math:`x` since the relative weight of the first neglected term is on the order of - :math:`1/6x^3`. Therefore, the threshold :math:`x = 10^{-5}` leads to relative - errors on the order of the machine epsilon. - - :param atomic_gaussian_width: Width of the atom-centered gaussian used to create the - atomic density - :param potential_exponent: Potential exponent of the decorated atom density. - Currently only implemented for potential_exponent < 10. Some exponents can be - connected to SOAP or physics-based quantities: p=0 uses Gaussian densities as in - SOAP, p=1 uses 1/r Coulomb like densities, p=6 uses 1/r^6 dispersion like - densities. - """ - - def __init__(self, atomic_gaussian_width: float, potential_exponent: int): - if not HAS_SCIPY: - raise ValueError("LodeDensity requires scipy to be installed") - - self.atomic_gaussian_width = atomic_gaussian_width - self.potential_exponent = potential_exponent - - def _short_range( - self, a: float, x: Union[float, np.ndarray], derivative: bool = False - ): - if derivative: - return -1 / (a + 1) + x / (a + 2) - else: - return 1 / a - x / (a + 1) + x**2 / (2 * (a + 2)) - - def _long_range( - self, a: float, x: Union[float, np.ndarray], derivative: bool = False - ): - if derivative: - return (np.exp(-x) - a * gamma(a) * gammainc(a, x) / x**a) / x - else: - return gamma(a) * gammainc(a, x) / x**a - - def _compute( - self, positions: Union[float, np.ndarray], derivative: bool = False - ) -> Union[float, np.ndarray]: - if self.potential_exponent == 0: - return GaussianDensity._compute( - self, positions=positions, derivative=derivative - ) - else: - atomic_gaussian_width_sq = self.atomic_gaussian_width**2 - a = self.potential_exponent / 2 - x = positions**2 / (2 * atomic_gaussian_width_sq) - - # Even though we use `np.where` to apply the `_short_range` method for small - # `x`, the `_long_range` method will also evaluated for small `x` and - # issueing RuntimeWarnings. We filter these warnings to avoid that these are - # presented to the user. - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=RuntimeWarning) - density = np.where( - x < 1e-5, - self._short_range(a, x, derivative=derivative), - self._long_range(a, x, derivative=derivative), - ) - - density *= 1 / gamma(a) / (2 * atomic_gaussian_width_sq) ** a - - # add inner derivative: ∂x/∂r - if derivative: - density *= positions / atomic_gaussian_width_sq - - return density - - def compute(self, positions: Union[float, np.ndarray]) -> Union[float, np.ndarray]: - return self._compute(positions=positions, derivative=False) - - def compute_derivative( - self, positions: Union[float, np.ndarray] - ) -> Union[float, np.ndarray]: - return self._compute(positions=positions, derivative=True) diff --git a/python/rascaline/rascaline/utils/splines/radial_basis.py b/python/rascaline/rascaline/utils/splines/radial_basis.py deleted file mode 100644 index 9057ab9b5..000000000 --- a/python/rascaline/rascaline/utils/splines/radial_basis.py +++ /dev/null @@ -1,345 +0,0 @@ -r""" -.. _python-radial-basis: - -Radial Basis -============ - -Radial basis functions :math:`R_{nl}(\boldsymbol{r})` are besides :ref:`atomic densities -` :math:`\rho_i` the central ingredients to compute spherical -expansion coefficients :math:`\langle anlm\vert\rho_i\rangle`. Radial basis functions, -define how which the atomic density is projected. To be more precise, the actual basis -functions are of - -.. math:: - - B_{nlm}(\boldsymbol{r}) = R_{nl}(r)Y_{lm}(\hat{r}) \,, - -where :math:`Y_{lm}(\hat{r})` are the real spherical harmonics evaluated at the point -:math:`\hat{r}`, i.e. at the spherical angles :math:`(\theta, \phi)` that determine the -orientation of the unit vector :math:`\hat{r} = \boldsymbol{r}/r`. - -Radial basis are represented as different child class of -:py:class:`rascaline.utils.RadialBasisBase`: :py:class:`rascaline.utils.GtoBasis`, -:py:class:`rascaline.utils.MonomialBasis`, and -:py:class:`rascaline.utils.SphericalBesselBasis` are provided, and you can implement -your own by defining a new class. - -.. autoclass:: rascaline.utils.RadialBasisBase - :members: - :show-inheritance: - -.. autoclass:: rascaline.utils.GtoBasis - :members: - :show-inheritance: - -.. autoclass:: rascaline.utils.MonomialBasis - :members: - :show-inheritance: - -.. autoclass:: rascaline.utils.SphericalBesselBasis - :members: - :show-inheritance: -""" - -from abc import ABC, abstractmethod -from typing import Union - -import numpy as np - - -try: - import scipy.integrate - import scipy.optimize - import scipy.special - - HAS_SCIPY = True -except ImportError: - HAS_SCIPY = False - - -class RadialBasisBase(ABC): - r""" - Base class to define radial basis and their evaluation. - - The class provides methods to evaluate the radial basis :math:`R_{nl}(r)` as well as - its (numerical) derivative with respect to positions :math:`r`. - - :parameter integration_radius: Value up to which the radial integral should be - performed. The usual value is :math:`\infty`. - """ - - def __init__(self, integration_radius: float): - self.integration_radius = integration_radius - - @abstractmethod - def compute( - self, n: int, ell: int, integrand_positions: Union[float, np.ndarray] - ) -> Union[float, np.ndarray]: - """Compute the ``n``/``l`` radial basis at all given ``integrand_positions`` - - :param n: radial channel - :param ell: angular channel - :param integrand_positions: positions to evaluate the radial basis - :returns: evaluated radial basis - """ - - def compute_derivative( - self, n: int, ell: int, integrand_positions: np.ndarray - ) -> np.ndarray: - """Compute the derivative of the ``n``/``l`` radial basis at all given - ``integrand_positions`` - - This is used for radial integrals with delta-like atomic densities. If not - defined in a child class, a numerical derivative based on finite differences of - ``integrand_positions`` will be used instead. - - :param n: radial channel - :param ell: angular channel - :param integrand_positions: positions to evaluate the radial basis - :returns: evaluated derivative of the radial basis - """ - displacement = 1e-6 - mean_abs_positions = np.abs(integrand_positions).mean() - - if mean_abs_positions < 1.0: - raise ValueError( - "Numerically derivative of the radial integral can not be performed " - "since positions are too small. Mean of the absolute positions is " - f"{mean_abs_positions:.1e} but should be at least 1." - ) - - radial_basis_pos = self.compute(n, ell, integrand_positions + displacement / 2) - radial_basis_neg = self.compute(n, ell, integrand_positions - displacement / 2) - - return (radial_basis_pos - radial_basis_neg) / displacement - - def compute_gram_matrix( - self, - max_radial: int, - max_angular: int, - ) -> np.ndarray: - """Gram matrix of the current basis. - - :parameter max_radial: number of angular components - :parameter max_angular: number of radial components - :returns: orthonormalization matrix of shape - ``(max_angular + 1, max_radial, max_radial)`` - """ - - if not HAS_SCIPY: - raise ValueError("Orthonormalization requires scipy!") - - # Gram matrix (also called overlap matrix or inner product matrix) - gram_matrix = np.zeros((max_angular + 1, max_radial, max_radial)) - - def integrand( - integrand_positions: np.ndarray, - n1: int, - n2: int, - ell: int, - ) -> np.ndarray: - return ( - integrand_positions**2 - * self.compute(n1, ell, integrand_positions) - * self.compute(n2, ell, integrand_positions) - ) - - for ell in range(max_angular + 1): - for n1 in range(max_radial): - for n2 in range(max_radial): - gram_matrix[ell, n1, n2] = scipy.integrate.quad( - func=integrand, - a=0, - b=self.integration_radius, - args=(n1, n2, ell), - )[0] - - return gram_matrix - - def compute_orthonormalization_matrix( - self, - max_radial: int, - max_angular: int, - ) -> np.ndarray: - """Compute orthonormalization matrix - - :parameter max_radial: number of angular components - :parameter max_angular: number of radial components - :returns: orthonormalization matrix of shape (max_angular + 1, max_radial, - max_radial) - """ - - gram_matrix = self.compute_gram_matrix(max_radial, max_angular) - - # Get the normalization constants from the diagonal entries - normalizations = np.zeros((max_angular + 1, max_radial)) - - for ell in range(max_angular + 1): - for n in range(max_radial): - normalizations[ell, n] = 1 / np.sqrt(gram_matrix[ell, n, n]) - - # Rescale orthonormalization matrix to be defined - # in terms of the normalized (but not yet orthonormalized) - # basis functions - gram_matrix[ell, n, :] *= normalizations[ell, n] - gram_matrix[ell, :, n] *= normalizations[ell, n] - - orthonormalization_matrix = np.zeros_like(gram_matrix) - for ell in range(max_angular + 1): - eigvals, eigvecs = np.linalg.eigh(gram_matrix[ell]) - orthonormalization_matrix[ell] = ( - eigvecs @ np.diag(np.sqrt(1.0 / eigvals)) @ eigvecs.T - ) - - # Rescale the orthonormalization matrix so that it - # works with respect to the primitive (not yet normalized) - # radial basis functions - for ell in range(max_angular + 1): - for n in range(max_radial): - orthonormalization_matrix[ell, :, n] *= normalizations[ell, n] - - return orthonormalization_matrix - - -class GtoBasis(RadialBasisBase): - r"""Primitive (not normalized nor orthonormalized) GTO radial basis. - - It is defined as - - .. math:: - - R_{nl}(r) = R_n(r) = r^n e^{-\frac{r^2}{2\sigma_n^2}}, - - where :math:`\sigma_n = \sqrt{n} r_\mathrm{cut}/n_\mathrm{max}` with - :math:`r_\mathrm{cut}` being the ``cutoff`` and :math:`n_\mathrm{max}` the maximal - number of radial components. - - :parameter cutoff: spherical cutoff for the radial basis - :parameter max_radial: number of radial components - """ - - def __init__(self, cutoff, max_radial): - # choosing infinity leads to problems when calculating the radial integral with - # `quad`! - super().__init__(integration_radius=5 * cutoff) - self.max_radial = max_radial - self.cutoff = cutoff - self.sigmas = np.ones(self.max_radial, dtype=float) - - for n in range(1, self.max_radial): - self.sigmas[n] = np.sqrt(n) - self.sigmas *= self.cutoff / self.max_radial - - def compute( - self, n: int, ell: int, integrand_positions: Union[float, np.ndarray] - ) -> Union[float, np.ndarray]: - return integrand_positions**n * np.exp( - -0.5 * (integrand_positions / self.sigmas[n]) ** 2 - ) - - def compute_derivative( - self, n: int, ell: int, integrand_positions: Union[float, np.ndarray] - ) -> Union[float, np.ndarray]: - return n / integrand_positions * self.compute( - n, ell, integrand_positions - ) - integrand_positions / self.sigmas[n] ** 2 * self.compute( - n, ell, integrand_positions - ) - - -class MonomialBasis(RadialBasisBase): - r"""Monomial basis. - - Basis is consisting of functions - - .. math:: - R_{nl}(r) = r^{l+2n}, - - where :math:`n` runs from :math:`0,1,...,n_\mathrm{max}-1`. These capture precisely - the radial dependence if we compute the Taylor expansion of a generic function - defined in 3D space. - - :parameter cutoff: spherical cutoff for the radial basis - """ - - def __init__(self, cutoff): - super().__init__(integration_radius=cutoff) - - def compute( - self, n: int, ell: int, integrand_positions: Union[float, np.ndarray] - ) -> Union[float, np.ndarray]: - return integrand_positions ** (ell + 2 * n) - - def compute_derivative( - self, n: int, ell: int, integrand_positions: Union[float, np.ndarray] - ) -> Union[float, np.ndarray]: - return (ell + 2 * n) * integrand_positions ** (ell + 2 * n - 1) - - -class SphericalBesselBasis(RadialBasisBase): - """Spherical Bessel functions used in the Laplacian eigenstate (LE) basis. - - :parameter cutoff: spherical cutoff for the radial basis - :parameter max_radial: number of angular components - :parameter max_angular: number of radial components - """ - - def __init__(self, cutoff, max_radial, max_angular): - if not HAS_SCIPY: - raise ValueError("SphericalBesselBasis requires scipy!") - - super().__init__(integration_radius=cutoff) - - self.max_radial = max_radial - self.max_angular = max_angular - self.roots = SphericalBesselBasis.compute_zeros(max_angular, max_radial) - - def compute( - self, n: int, ell: int, integrand_positions: Union[float, np.ndarray] - ) -> Union[float, np.ndarray]: - return scipy.special.spherical_jn( - ell, - integrand_positions * self.roots[ell, n] / self.integration_radius, - ) - - def compute_derivative( - self, n: int, ell: int, integrand_positions: Union[float, np.ndarray] - ) -> Union[float, np.ndarray]: - return ( - self.roots[ell, n] - / self.integration_radius - * scipy.special.spherical_jn( - ell, - integrand_positions * self.roots[ell, n] / self.integration_radius, - derivative=True, - ) - ) - - @staticmethod - def compute_zeros(max_angular: int, max_radial: int) -> np.ndarray: - """Zeros of spherical bessel functions. - - Code is taken from the - `Scipy Cookbook `_. - - :parameter max_radial: number of angular components - :parameter max_angular: number of radial components - :returns: computed zeros of the spherical bessel functions - """ # noqa: E501 - - def Jn(r: float, n: int) -> float: - return np.sqrt(np.pi / (2 * r)) * scipy.special.jv(n + 0.5, r) - - def Jn_zeros(n: int, nt: int) -> np.ndarray: - zeros_j = np.zeros((n + 1, nt), dtype=np.float64) - zeros_j[0] = np.arange(1, nt + 1) * np.pi - points = np.arange(1, nt + n + 1) * np.pi - roots = np.zeros(nt + n, dtype=np.float64) - for i in range(1, n + 1): - for j in range(nt + n - i): - roots[j] = scipy.optimize.brentq(Jn, points[j], points[j + 1], (i,)) - points = roots - zeros_j[i][:nt] = roots[:nt] - return zeros_j - - return Jn_zeros(max_angular, max_radial) diff --git a/python/rascaline/rascaline/utils/splines/splines.py b/python/rascaline/rascaline/utils/splines/splines.py deleted file mode 100644 index 96ee74874..000000000 --- a/python/rascaline/rascaline/utils/splines/splines.py +++ /dev/null @@ -1,908 +0,0 @@ -""" -.. _python-splined-radial-integral: - -Splined radial integrals -======================== - -Classes for generating splines which can be used as tabulated radial integrals in the -various SOAP and LODE calculators. - -All classes are based on :py:class:`rascaline.utils.RadialIntegralSplinerBase`. We -provides several ways to compute a radial integral: you may chose and initialize a pre -defined atomic density and radial basis and provide them to -:py:class:`rascaline.utils.SoapSpliner` or :py:class:`rascaline.utils.LodeSpliner`. Both -classes require `scipy`_ to be installed in order to perform the numerical integrals. - -Alternatively, you can also explicitly provide functions for the radial integral and its -derivative and passing them to :py:class:`rascaline.utils.RadialIntegralFromFunction`. - -.. autoclass:: rascaline.utils.RadialIntegralSplinerBase - :members: - :show-inheritance: - -.. autoclass:: rascaline.utils.SoapSpliner - :members: - :show-inheritance: - -.. autoclass:: rascaline.utils.LodeSpliner - :members: - :show-inheritance: - -.. autoclass:: rascaline.utils.RadialIntegralFromFunction - :members: - :show-inheritance: - - -.. _`scipy`: https://scipy.org -""" - -from abc import ABC, abstractmethod -from typing import Callable, Dict, Optional, Union - -import numpy as np - - -try: - from scipy.integrate import dblquad, quad, quad_vec - from scipy.special import legendre, spherical_in, spherical_jn - - HAS_SCIPY = True -except ImportError: - HAS_SCIPY = False - -from .atomic_density import AtomicDensityBase, DeltaDensity, GaussianDensity -from .radial_basis import RadialBasisBase - - -class RadialIntegralSplinerBase(ABC): - """Base class for splining arbitrary radial integrals. - - If :py:meth:`RadialIntegralSplinerBase.radial_integral_derivative` is not - implemented in a child class it will computed based on finite differences. - - :parameter max_angular: number of radial components - :parameter max_radial: number of angular components - :parameter spline_cutoff: cutoff radius for the spline interpolation. This is also - the maximal value that can be interpolated. - :parameter basis: Provide a :class:`RadialBasisBase` instance to orthonormalize the - radial integral. - :parameter accuracy: accuracy of the numerical integration and the splining. - Accuracy is reached when either the mean absolute error or the mean relative - error gets below the ``accuracy`` threshold. - """ - - def __init__( - self, - max_radial: int, - max_angular: int, - spline_cutoff: float, - basis: Optional[RadialBasisBase], - accuracy: float, - ): - self.max_radial = max_radial - self.max_angular = max_angular - self.spline_cutoff = spline_cutoff - self.basis = basis - self.accuracy = accuracy - - def compute( - self, - n_spline_points: Optional[int] = None, - ) -> Dict: - """Compute the spline for rascaline's tabulated radial integrals. - - :parameter n_spline_points: Use fixed number of spline points instead of find - the number based on the provided ``accuracy``. - :returns dict: dictionary for the input as the ``radial_basis`` parameter of a - rascaline calculator. - """ - - if self.basis is not None: - orthonormalization_matrix = self.basis.compute_orthonormalization_matrix( - self.max_radial, self.max_angular - ) - else: - orthonormalization_matrix = None - - def value_evaluator_3D(positions): - return self._value_evaluator_3D( - positions, orthonormalization_matrix, derivative=False - ) - - def derivative_evaluator_3D(positions): - return self._value_evaluator_3D( - positions, orthonormalization_matrix, derivative=True - ) - - if n_spline_points is not None: - positions = np.linspace(0, self.spline_cutoff, n_spline_points) - values = value_evaluator_3D(positions) - derivatives = derivative_evaluator_3D(positions) - else: - dynamic_spliner = DynamicSpliner( - 0, - self.spline_cutoff, - value_evaluator_3D, - derivative_evaluator_3D, - self.accuracy, - ) - positions, values, derivatives = dynamic_spliner.spline() - - # Convert positions, values, derivatives into the appropriate json formats: - spline_points = [] - for position, value, derivative in zip(positions, values, derivatives): - spline_points.append( - { - "position": position, - "values": { - "v": 1, - "dim": value.shape, - "data": value.flatten().tolist(), - }, - "derivatives": { - "v": 1, - "dim": derivative.shape, - "data": derivative.flatten().tolist(), - }, - } - ) - - parameters = {"points": spline_points} - - center_contribution = self.center_contribution - if center_contribution is not None: - if self.basis is not None: - # consider only `l=0` component of the `orthonormalization_matrix` - parameters["center_contribution"] = list( - orthonormalization_matrix[0] @ center_contribution - ) - else: - parameters["center_contribution"] = center_contribution - - return {"TabulatedRadialIntegral": parameters} - - @abstractmethod - def radial_integral(self, n: int, ell: int, positions: np.ndarray) -> np.ndarray: - """evaluate the radial integral""" - ... - - @property - def center_contribution(self) -> Union[None, np.ndarray]: - r"""Pre-computed value for the contribution of the central atom. - - Required for LODE calculations. The central atom contribution will be - orthonormalized in the same way as the radial integral. - """ - - return None - - def radial_integral_derivative( - self, n: int, ell: int, positions: np.ndarray - ) -> np.ndarray: - """evaluate the derivative of the radial integral""" - displacement = 1e-6 - mean_abs_positions = np.mean(np.abs(positions)) - - if mean_abs_positions < 1.0: - raise ValueError( - "Numerically derivative of the radial integral can not be performed " - "since positions are too small. Mean of the absolute positions is " - f"{mean_abs_positions:.1e} but should be at least 1." - ) - - radial_integral_pos = self.radial_integral(n, ell, positions + displacement / 2) - radial_integral_neg = self.radial_integral(n, ell, positions - displacement / 2) - - return (radial_integral_pos - radial_integral_neg) / displacement - - def _value_evaluator_3D( - self, - positions: np.ndarray, - orthonormalization_matrix: Optional[np.ndarray], - derivative: bool, - ): - values = np.zeros([len(positions), self.max_angular + 1, self.max_radial]) - for ell in range(self.max_angular + 1): - for n in range(self.max_radial): - if derivative: - values[:, ell, n] = self.radial_integral_derivative( - n, ell, positions - ) - else: - values[:, ell, n] = self.radial_integral(n, ell, positions) - - if orthonormalization_matrix is not None: - # For each l channel we do a dot product of the orthonormalization_matrix of - # shape (n, n) with the values which should have the shape (n, n_positions). - # To achieve the correct broadcasting we have to transpose twice. - for ell in range(self.max_angular + 1): - values[:, ell, :] = ( - orthonormalization_matrix[ell] @ values[:, ell, :].T - ).T - - return values - - -class DynamicSpliner: - def __init__( - self, - start: float, - stop: float, - values_fn: Callable[[np.ndarray], np.ndarray], - derivatives_fn: Callable[[np.ndarray], np.ndarray], - accuracy: float = 1e-8, - ) -> None: - """Dynamic spline generator. - - This class can be used to spline any set of functions defined within the - start-stop interval. Cubic Hermite splines - (https://en.wikipedia.org/wiki/Cubic_Hermite_spline) are used. The same spline - points will be used for all functions, and more will be added until either the - relative error or the absolute error fall below the requested accuracy on - average across all functions. The functions are specified via values_fn and - derivatives_fn. These must be able to take a numpy 1D array of positions as - their input, and they must output a numpy array where the first dimension - corresponds to the input positions, while other dimensions are arbitrary and can - correspond to any way in which the target functions can be classified. The - splines can be obtained via the spline method. - """ - - self.start = start - self.stop = stop - self.values_fn = values_fn - self.derivatives_fn = derivatives_fn - self.requested_accuracy = accuracy - - # initialize spline with 11 points - positions = np.linspace(start, stop, 11) - self.spline_positions = positions - self.spline_values = values_fn(positions) - self.spline_derivatives = derivatives_fn(positions) - - self.number_of_custom_axes = len(self.spline_values.shape) - 1 - - def spline(self): - """Calculates and outputs the splines. - - The outputs of this function are, respectively: - - - A numpy 1D array containing the spline positions. These are equally spaced in - the start-stop interval. - - A numpy ndarray containing the values of the splined functions at the spline - positions. The first dimension corresponds to the spline positions, while all - subsequent dimensions are consistent with the values_fn and - `get_function_derivative` provided during initialization of the class. - - A numpy ndarray containing the derivatives of the splined functions at the - spline positions, with the same structure as that of the ndarray of values. - """ - - while True: - n_intermediate_positions = len(self.spline_positions) - 1 - - if n_intermediate_positions >= 50000: - raise ValueError( - "Maximum number of spline points reached. \ - There might be a problem with the functions to be splined" - ) - - half_step = (self.spline_positions[1] - self.spline_positions[0]) / 2 - intermediate_positions = np.linspace( - self.start + half_step, self.stop - half_step, n_intermediate_positions - ) - - estimated_values = self._compute_from_spline(intermediate_positions) - new_values = self.values_fn(intermediate_positions) - - mean_absolute_error = np.mean(np.abs(estimated_values - new_values)) - with np.errstate(divide="ignore"): # Ignore divide-by-zero warnings - mean_relative_error = np.mean( - np.abs((estimated_values - new_values) / new_values) - ) - - if ( - mean_absolute_error < self.requested_accuracy - or mean_relative_error < self.requested_accuracy - ): - break - - new_derivatives = self.derivatives_fn(intermediate_positions) - - concatenated_positions = np.concatenate( - [self.spline_positions, intermediate_positions], axis=0 - ) - concatenated_values = np.concatenate( - [self.spline_values, new_values], axis=0 - ) - concatenated_derivatives = np.concatenate( - [self.spline_derivatives, new_derivatives], axis=0 - ) - - sort_indices = np.argsort(concatenated_positions, axis=0) - - self.spline_positions = concatenated_positions[sort_indices] - self.spline_values = concatenated_values[sort_indices] - self.spline_derivatives = concatenated_derivatives[sort_indices] - - return self.spline_positions, self.spline_values, self.spline_derivatives - - def _compute_from_spline(self, positions): - x = positions - delta_x = self.spline_positions[1] - self.spline_positions[0] - n = (np.floor(x / delta_x)).astype(np.int32) - - t = (x - n * delta_x) / delta_x - t_2 = t**2 - t_3 = t**3 - - h00 = 2.0 * t_3 - 3.0 * t_2 + 1.0 - h10 = t_3 - 2.0 * t_2 + t - h01 = -2.0 * t_3 + 3.0 * t_2 - h11 = t_3 - t_2 - - p_k = self.spline_values[n] - p_k_1 = self.spline_values[n + 1] - - m_k = self.spline_derivatives[n] - m_k_1 = self.spline_derivatives[n + 1] - - new_shape = (-1,) + (1,) * self.number_of_custom_axes - h00 = h00.reshape(new_shape) - h10 = h10.reshape(new_shape) - h01 = h01.reshape(new_shape) - h11 = h11.reshape(new_shape) - - interpolated_values = ( - h00 * p_k + h10 * delta_x * m_k + h01 * p_k_1 + h11 * delta_x * m_k_1 - ) - - return interpolated_values - - -class RadialIntegralFromFunction(RadialIntegralSplinerBase): - r"""Compute radial integral spline points based on a provided function. - - :parameter radial_integral: Function to compute the radial integral. Function must - take ``n``, ``l``, and ``positions`` as inputs, where ``n`` and ``l`` are - integers and ``positions`` is a numpy 1-D array that contains the spline points - at which the radial integral will be evaluated. The function must return a numpy - 1-D array containing the values of the radial integral. - :parameter spline_cutoff: cutoff radius for the spline interpolation. This is also - the maximal value that can be interpolated. - :parameter max_radial: number of angular components - :parameter max_angular: number of radial components - :parameter radial_integral_derivative: The derivative of the radial integral taking - the same parameters as ``radial_integral``. If it is ``None`` (default), finite - differences are used to calculate the derivative of the radial integral. It is - recommended to provide this parameter if possible. Derivatives from finite - differences can cause problems when evaluating at the edges of the domain (i.e., - at ``0`` and ``spline_cutoff``) because the function might not be defined - outside of the domain. - :parameter accuracy: accuracy of the numerical integration and the splining. - Accuracy is reached when either the mean absolute error or the mean relative - error gets below the ``accuracy`` threshold. - :parameter center_contribution: Contribution of the central atom required for LODE - calculations. The ``center_contribution`` is defined as - - .. math:: - c_n = \sqrt{4π}\int_0^\infty dr r^2 R_n(r) g(r) - - where :math:`g(r)` is the radially symmetric density function, `R_n(r)` the - radial basis function and :math:`n` the current radial channel. This should be - pre-computed and provided as a separate parameter. - - Example - ------- - First define a ``radial_integral`` function - - >>> def radial_integral(n, ell, r): - ... return np.sin(r) - ... - - and provide this as input to the spline generator - - >>> spliner = RadialIntegralFromFunction( - ... radial_integral=radial_integral, - ... max_radial=12, - ... max_angular=9, - ... spline_cutoff=8.0, - ... ) - - Finally, we can use the ``spliner`` directly in the ``radial_integral`` section of a - calculator - - >>> from rascaline import SoapPowerSpectrum - >>> calculator = SoapPowerSpectrum( - ... cutoff=8.0, - ... max_radial=12, - ... max_angular=9, - ... center_atom_weight=1.0, - ... radial_basis=spliner.compute(), - ... atomic_gaussian_width=1.0, # ignored - ... cutoff_function={"Step": {}}, - ... ) - - The ``atomic_gaussian_width`` parameter is required by the calculator but will be - will be ignored during the feature computation. - - A more in depth example using a "rectangular" Laplacian eigenstate (LE) basis is - provided in the :ref:`userdoc-how-to-splined-radial-integral` how-to guide. - """ - - def __init__( - self, - radial_integral: Callable[[int, int, np.ndarray], np.ndarray], - spline_cutoff: float, - max_radial: int, - max_angular: int, - radial_integral_derivative: Optional[ - Callable[[int, int, np.ndarray], np.ndarray] - ] = None, - center_contribution: Optional[np.ndarray] = None, - accuracy: float = 1e-8, - ): - self.radial_integral_function = radial_integral - self.radial_integral_derivative_function = radial_integral_derivative - - if center_contribution is not None and len(center_contribution) != max_radial: - raise ValueError( - f"center contribution has {len(center_contribution)} entries but " - f"should be the same as max_radial ({max_radial})" - ) - - self._center_contribution = center_contribution - - super().__init__( - max_radial=max_radial, - max_angular=max_angular, - spline_cutoff=spline_cutoff, - basis=None, # do no orthonormalize the radial integral - accuracy=accuracy, - ) - - def radial_integral(self, n: int, ell: int, positions: np.ndarray) -> np.ndarray: - return self.radial_integral_function(n, ell, positions) - - @property - def center_contribution(self) -> Union[None, np.ndarray]: - return self._center_contribution - - def radial_integral_derivative( - self, n: int, ell: int, positions: np.ndarray - ) -> np.ndarray: - if self.radial_integral_derivative_function is None: - return super().radial_integral_derivative(n, ell, positions) - else: - return self.radial_integral_derivative_function(n, ell, positions) - - -class SoapSpliner(RadialIntegralSplinerBase): - """Compute radial integral spline points for real space calculators. - - Use only in combination with a real space calculators like - :class:`rascaline.SphericalExpansion` or :class:`rascaline.SoapPowerSpectrum`. For - k-space spherical expansions use :class:`LodeSpliner`. - - If ``density`` is either :class:`rascaline.utils.DeltaDensity` or - :class:`rascaline.utils.GaussianDensity` the radial integral will be partly solved - analytical. These simpler expressions result in a faster and more stable evaluation. - - :parameter cutoff: spherical cutoff for the radial basis - :parameter max_radial: number of angular components - :parameter max_angular: number of radial components - :parameter basis: definition of the radial basis - :parameter density: definition of the atomic density - :parameter accuracy: accuracy of the numerical integration and the splining. - Accuracy is reached when either the mean absolute error or the mean relative - error gets below the ``accuracy`` threshold. - :raise ValueError: if `scipy`_ is not available - - Example - ------- - - First import the necessary classed and define hyper parameters for the spherical - expansions. - - >>> from rascaline import SphericalExpansion - >>> from rascaline.utils import GaussianDensity, GtoBasis - - >>> cutoff = 2 - >>> max_radial = 6 - >>> max_angular = 4 - >>> atomic_gaussian_width = 1.0 - - Next we initialize our radial basis and the density - - >>> basis = GtoBasis(cutoff=cutoff, max_radial=max_radial) - >>> density = GaussianDensity(atomic_gaussian_width=atomic_gaussian_width) - - And finally the actual spliner instance - - >>> spliner = SoapSpliner( - ... cutoff=cutoff, - ... max_radial=max_radial, - ... max_angular=max_angular, - ... basis=basis, - ... density=density, - ... accuracy=1e-3, - ... ) - - Above we reduced ``accuracy`` from the default value of ``1e-8`` to ``1e-3`` to - speed up calculations. - - As for all spliner classes you can use the output - :meth:`RadialIntegralSplinerBase.compute` method directly as the ``radial_basis`` - parameter. - - >>> calculator = SphericalExpansion( - ... cutoff=cutoff, - ... max_radial=max_radial, - ... max_angular=max_angular, - ... center_atom_weight=1.0, - ... atomic_gaussian_width=atomic_gaussian_width, - ... radial_basis=spliner.compute(), - ... cutoff_function={"Step": {}}, - ... ) - - You can now use ``calculator`` to obtain the spherical expansion coefficients of - your systems. Note that the the spliner based used here will produce the same - coefficients as if ``radial_basis={"Gto": {}}`` would be used. - - An additional example using a "rectangular" Laplacian eigenstate (LE) basis is - provided in the :ref:`userdoc-how-to-le-basis`. - - .. seealso:: - :class:`LodeSpliner` for a spliner class that works with - :class:`rascaline.LodeSphericalExpansion` - """ - - def __init__( - self, - cutoff: float, - max_radial: int, - max_angular: int, - basis: RadialBasisBase, - density: AtomicDensityBase, - accuracy: float = 1e-8, - ): - if not HAS_SCIPY: - raise ValueError("Spliner class requires scipy!") - - self.density = density - - super().__init__( - max_radial=max_radial, - max_angular=max_angular, - spline_cutoff=cutoff, - basis=basis, - accuracy=accuracy, - ) - - def radial_integral(self, n: int, ell: int, positions: np.ndarray) -> np.ndarray: - if type(self.density) is DeltaDensity: - return self._radial_integral_delta(n, ell, positions) - elif type(self.density) is GaussianDensity: - return self._radial_integral_gaussian(n, ell, positions) - else: - return self._radial_integral_custom(n, ell, positions) - - def radial_integral_derivative( - self, n: int, ell: int, positions: np.ndarray - ) -> np.ndarray: - if type(self.density) is DeltaDensity: - return self._radial_integral_delta_derivative(n, ell, positions) - elif type(self.density) is GaussianDensity: - return self._radial_integral_gaussian_derivative(n, ell, positions) - else: - return self._radial_integral_custom(n, ell, positions, derivative=True) - - def _radial_integral_delta( - self, n: int, ell: int, positions: np.ndarray - ) -> np.ndarray: - return self.basis.compute(n, ell, positions) - - def _radial_integral_delta_derivative( - self, n: int, ell: int, positions: np.ndarray - ) -> np.ndarray: - return self.basis.compute_derivative(n, ell, positions) - - def _radial_integral_gaussian( - self, n: int, ell: int, positions: np.ndarray - ) -> np.ndarray: - atomic_gaussian_width_sq = self.density.atomic_gaussian_width**2 - - prefactor = ( - (4 * np.pi) - / (np.pi * atomic_gaussian_width_sq) ** (3 / 4) - * np.exp(-0.5 * positions**2 / atomic_gaussian_width_sq) - ) - - def integrand( - integrand_position: float, n: int, ell: int, positions: np.array - ) -> np.ndarray: - return ( - integrand_position**2 - * self.basis.compute(n, ell, integrand_position) - * np.exp(-0.5 * integrand_position**2 / atomic_gaussian_width_sq) - * spherical_in( - ell, - integrand_position * positions / atomic_gaussian_width_sq, - ) - ) - - return ( - prefactor - * quad_vec( - f=integrand, - a=0, - b=self.basis.integration_radius, - args=(n, ell, positions), - )[0] - ) - - def _radial_integral_gaussian_derivative( - self, n: int, ell: int, positions: np.ndarray - ) -> np.ndarray: - # The derivative here for `positions=0`, any `n` and `ell=1` are wrong due to a - # bug in Scipy: https://github.com/scipy/scipy/issues/20506 - # - # However, this is not problematic because the derivative at zero is only - # required if two atoms are VERY close and we have checks that should prevent - # very small distances between atoms. The center contribution is also not - # affected becuase it only needs the values but not derivatives of the radial - # integral. - atomic_gaussian_width_sq = self.density.atomic_gaussian_width**2 - - prefactor = ( - (4 * np.pi) - / (np.pi * atomic_gaussian_width_sq) ** (3 / 4) - * np.exp(-0.5 * positions**2 / atomic_gaussian_width_sq) - ) - - def integrand( - integrand_position: float, n: int, ell: int, positions: np.array - ) -> np.ndarray: - return ( - integrand_position**3 - * self.basis.compute(n, ell, integrand_position) - * np.exp(-(integrand_position**2) / (2 * atomic_gaussian_width_sq)) - * spherical_in( - ell, - integrand_position * positions / atomic_gaussian_width_sq, - derivative=True, - ) - ) - - return atomic_gaussian_width_sq**-1 * ( - prefactor - * quad_vec( - f=integrand, - a=0, - b=self.basis.integration_radius, - args=(n, ell, positions), - )[0] - - positions * self._radial_integral_gaussian(n, ell, positions) - ) - - def _radial_integral_custom( - self, n: int, ell: int, positions: np.ndarray, derivative: bool = False - ) -> np.ndarray: - - P_ell = legendre(ell) - - if derivative: - - def integrand( - u: float, integrand_position: float, n: int, ell: int, position: float - ) -> float: - arg = np.sqrt( - integrand_position**2 - + position**2 - - 2 * integrand_position * position * u - ) - - return ( - integrand_position**2 - * self.basis.compute(n, ell, integrand_position) - * P_ell(u) - * (position - u * integrand_position) - * self.density.compute_derivative(arg) - / arg - ) - - else: - - def integrand( - u: float, integrand_position: float, n: int, ell: int, position: float - ) -> float: - arg = np.sqrt( - integrand_position**2 - + position**2 - - 2 * integrand_position * position * u - ) - - return ( - integrand_position**2 - * self.basis.compute(n, ell, integrand_position) - * P_ell(u) - * self.density.compute(arg) - ) - - radial_integral = np.zeros(len(positions)) - - for i, position in enumerate(positions): - radial_integral[i], _ = dblquad( - func=integrand, - a=0, - b=self.basis.integration_radius, - gfun=-1, - hfun=1, - args=(n, ell, position), - ) - - return 2 * np.pi * radial_integral - - -class LodeSpliner(RadialIntegralSplinerBase): - r"""Compute radial integral spline points for k-space calculators. - - Use only in combination with a k-space/Fourier-space calculators like - :class:`rascaline.LodeSphericalExpansion`. For real space spherical expansions use - :class:`SoapSpliner`. - - :parameter k_cutoff: spherical reciprocal cutoff - :parameter max_radial: number of angular components - :parameter max_angular: number of radial components - :parameter basis: definition of the radial basis - :parameter density: definition of the atomic density - :parameter accuracy: accuracy of the numerical integration and the splining. - Accuracy is reached when either the mean absolute error or the mean relative - error gets below the ``accuracy`` threshold. - :raise ValueError: if `scipy`_ is not available - - Example - ------- - - First import the necessary classed and define hyper parameters for the spherical - expansions. - - >>> from rascaline import LodeSphericalExpansion - >>> from rascaline.utils import GaussianDensity, GtoBasis - - Note that ``cutoff`` defined below denotes the maximal distance for the projection - of the density. In contrast to SOAP, LODE also takes atoms outside of this - ``cutoff`` into account for the density. - - >>> cutoff = 2 - >>> max_radial = 6 - >>> max_angular = 4 - >>> atomic_gaussian_width = 1.0 - - :math:`1.2 \, \pi \, \sigma` where :math:`\sigma` is the ``atomic_gaussian_width`` - which is a reasonable value for most systems. - - >>> k_cutoff = 1.2 * np.pi / atomic_gaussian_width - - Next we initialize our radial basis and the density - - >>> basis = GtoBasis(cutoff=cutoff, max_radial=max_radial) - >>> density = GaussianDensity(atomic_gaussian_width=atomic_gaussian_width) - - And finally the actual spliner instance - - >>> spliner = LodeSpliner( - ... k_cutoff=k_cutoff, - ... max_radial=max_radial, - ... max_angular=max_angular, - ... basis=basis, - ... density=density, - ... ) - - As for all spliner classes you can use the output - :meth:`RadialIntegralSplinerBase.compute` method directly as the ``radial_basis`` - parameter. - - >>> calculator = LodeSphericalExpansion( - ... cutoff=cutoff, - ... max_radial=max_radial, - ... max_angular=max_angular, - ... center_atom_weight=1.0, - ... atomic_gaussian_width=atomic_gaussian_width, - ... potential_exponent=1, - ... radial_basis=spliner.compute(), - ... ) - - You can now use ``calculator`` to obtain the spherical expansion coefficients of - your systems. Note that the the spliner based used here will produce the same - coefficients as if ``radial_basis={"Gto": {}}`` would be used. - - .. seealso:: - :class:`SoapSpliner` for a spliner class that works with - :class:`rascaline.SphericalExpansion` - """ - - def __init__( - self, - k_cutoff: float, - max_radial: int, - max_angular: int, - basis: RadialBasisBase, - density: AtomicDensityBase, - accuracy: float = 1e-8, - ): - if not HAS_SCIPY: - raise ValueError("Spliner class requires scipy!") - - self.density = density - - super().__init__( - max_radial=max_radial, - max_angular=max_angular, - basis=basis, - spline_cutoff=k_cutoff, # use k_cutoff here because we spline in k-space - accuracy=accuracy, - ) - - def radial_integral(self, n: int, ell: int, positions: np.ndarray) -> np.ndarray: - def integrand( - integrand_position: float, n: int, ell: int, positions: np.ndarray - ) -> np.ndarray: - return ( - integrand_position**2 - * self.basis.compute(n, ell, integrand_position) - * spherical_jn(ell, integrand_position * positions) - ) - - return quad_vec( - f=integrand, - a=0, - b=self.basis.integration_radius, - args=(n, ell, positions), - )[0] - - def radial_integral_derivative( - self, n: int, ell: int, positions: np.ndarray - ) -> np.ndarray: - def integrand( - integrand_position: float, n: int, ell: int, positions: np.ndarray - ) -> np.ndarray: - return ( - integrand_position**3 - * self.basis.compute(n, ell, integrand_position) - * spherical_jn(ell, integrand_position * positions, derivative=True) - ) - - return quad_vec( - f=integrand, - a=0, - b=self.basis.integration_radius, - args=(n, ell, positions), - )[0] - - @property - def center_contribution(self) -> np.ndarray: - if type(self.density) is DeltaDensity: - center_contrib = self._center_contribution_delta - else: - center_contrib = self._center_contribution_custom - - return [np.sqrt(4 * np.pi) * center_contrib(n) for n in range(self.max_radial)] - - def _center_contribution_delta(self, n: int): - raise NotImplementedError( - "center contribution for delta distributions is not implemented yet." - ) - - def _center_contribution_custom(self, n: int): - def integrand(integrand_position: float, n: int) -> np.ndarray: - return ( - integrand_position**2 - * self.basis.compute(n, 0, integrand_position) - * self.density.compute(integrand_position) - ) - - return quad( - func=integrand, - a=0, - b=self.basis.integration_radius, - args=(n,), - )[0] diff --git a/python/rascaline/rascaline/version.py b/python/rascaline/rascaline/version.py deleted file mode 100644 index e69652e02..000000000 --- a/python/rascaline/rascaline/version.py +++ /dev/null @@ -1,4 +0,0 @@ -import importlib.metadata - - -__version__ = importlib.metadata.version("rascaline") diff --git a/python/rascaline/tests/misc.py b/python/rascaline/tests/misc.py deleted file mode 100644 index cd0e22ecf..000000000 --- a/python/rascaline/tests/misc.py +++ /dev/null @@ -1,19 +0,0 @@ -import os - -import rascaline - - -def test_cmake_prefix_path_exists(): - assert hasattr(rascaline.utils, "cmake_prefix_path") - assert isinstance(rascaline.utils.cmake_prefix_path, str) - - # there is both the path to metatensor and rascaline cmake prefix in here - assert len(rascaline.utils.cmake_prefix_path.split(";")), 2 - - -def test_cmake_files_exists(): - rascaline_cmake_path = rascaline.utils.cmake_prefix_path.split(";")[0] - cmake = os.path.join(rascaline_cmake_path, "rascaline") - - assert os.path.isfile(os.path.join(cmake, "rascaline-config.cmake")) - assert os.path.isfile(os.path.join(cmake, "rascaline-config-version.cmake")) diff --git a/python/rascaline/tests/utils/atomic_density.py b/python/rascaline/tests/utils/atomic_density.py deleted file mode 100644 index 5514691fc..000000000 --- a/python/rascaline/tests/utils/atomic_density.py +++ /dev/null @@ -1,24 +0,0 @@ -import numpy as np -import pytest -from numpy.testing import assert_allclose - -from rascaline.utils import GaussianDensity, LodeDensity - - -pytest.importorskip("scipy") - - -@pytest.mark.parametrize( - "density", - [ - GaussianDensity(atomic_gaussian_width=1.2), - LodeDensity(atomic_gaussian_width=1.2, potential_exponent=1), - ], -) -def test_derivative(density): - positions = np.linspace(0, 5, num=int(1e6)) - dens = density.compute(positions) - grad_ana = density.compute_derivative(positions) - grad_numerical = np.gradient(dens, positions) - - assert_allclose(grad_numerical, grad_ana, atol=1e-6) diff --git a/python/rascaline/tests/utils/power_spectrum.py b/python/rascaline/tests/utils/power_spectrum.py deleted file mode 100644 index f8dda1a20..000000000 --- a/python/rascaline/tests/utils/power_spectrum.py +++ /dev/null @@ -1,190 +0,0 @@ -# -*- coding: utf-8 -*- -import numpy as np -import pytest -from numpy.testing import assert_allclose, assert_equal - -import rascaline -from rascaline.utils import PowerSpectrum - -from ..test_systems import SystemForTests -from .test_utils import finite_differences_positions - - -ase = pytest.importorskip("ase") - - -HYPERS = hypers = { - "cutoff": 5.0, - "max_radial": 6, - "max_angular": 4, - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "radial_basis": { - "Gto": {}, - }, - "cutoff_function": { - "ShiftedCosine": {"width": 0.5}, - }, -} -N_ATOMIC_TYPES = len(np.unique(SystemForTests().types())) - - -def soap_calculator(): - return rascaline.SphericalExpansion(**HYPERS) - - -def lode_calculator(): - hypers = HYPERS.copy() - hypers.pop("cutoff_function") - hypers["potential_exponent"] = 1 - - return rascaline.LodeSphericalExpansion(**hypers) - - -def soap(): - return soap_calculator().compute(SystemForTests()) - - -def power_spectrum(): - return PowerSpectrum(soap_calculator()).compute(SystemForTests()) - - -@pytest.mark.parametrize("calculator", [soap_calculator(), lode_calculator()]) -def test_power_spectrum(calculator) -> None: - """Test that power spectrum works and that the shape is correct.""" - ps_python = PowerSpectrum(calculator).compute(SystemForTests()) - ps_python = ps_python.keys_to_samples(["center_type"]) - - # Test the number of properties is correct - n_props_actual = len(ps_python.block().properties) - - n_props_expected = ( - N_ATOMIC_TYPES**2 * HYPERS["max_radial"] ** 2 * (HYPERS["max_angular"] + 1) - ) - - assert n_props_actual == n_props_expected - - -def test_error_max_angular(): - """Test error raise if max_angular are different.""" - hypers_2 = HYPERS.copy() - hypers_2.update(max_radial=3, max_angular=1) - - se_calculator2 = rascaline.SphericalExpansion(**hypers_2) - - msg = "'max_angular' of both calculators must be the same!" - with pytest.raises(ValueError, match=msg): - PowerSpectrum(soap_calculator(), se_calculator2) - - calculator = rascaline.SoapPowerSpectrum(**HYPERS) - with pytest.raises(ValueError, match="are supported for calculator_1"): - PowerSpectrum(calculator) - - -def test_wrong_calculator_1(): - """Test error raise for wrong calculator_1.""" - - calculator = rascaline.SoapPowerSpectrum(**HYPERS) - with pytest.raises(ValueError, match="are supported for calculator_1"): - PowerSpectrum(calculator) - - -def test_wrong_calculator_2(): - """Test error raise for wrong calculator_2.""" - - calculator = rascaline.SoapPowerSpectrum(**HYPERS) - with pytest.raises(ValueError, match="are supported for calculator_2"): - PowerSpectrum(soap_calculator(), calculator) - - -def test_power_spectrum_different_hypers() -> None: - """Test that power spectrum works with different spherical expansions.""" - - hypers_2 = HYPERS.copy() - hypers_2.update(max_radial=3, max_angular=4) - - se_calculator2 = rascaline.SphericalExpansion(**hypers_2) - - PowerSpectrum(soap_calculator(), se_calculator2).compute(SystemForTests()) - - -def test_power_spectrum_rust() -> None: - """Test that the dot kernels of the rust and python version are the same.""" - - power_spectrum_python = power_spectrum() - power_spectrum_python = power_spectrum_python.keys_to_samples(["center_type"]) - kernel_python = np.dot( - power_spectrum_python[0].values, power_spectrum_python[0].values.T - ) - - power_spectrum_rust = rascaline.SoapPowerSpectrum(**HYPERS).compute( - SystemForTests() - ) - power_spectrum_rust = power_spectrum_rust.keys_to_samples(["center_type"]) - power_spectrum_rust = power_spectrum_rust.keys_to_properties( - ["neighbor_1_type", "neighbor_2_type"] - ) - kernel_rust = np.dot(power_spectrum_rust[0].values, power_spectrum_rust[0].values.T) - assert_allclose(kernel_python, kernel_rust) - - -def test_power_spectrum_gradients() -> None: - """Test that gradients are correct using finite differences.""" - calculator = PowerSpectrum(soap_calculator()) - - # An ASE atoms object with the same properties as SystemForTests() - atoms = ase.Atoms( - symbols="HHOO", - positions=[[0, 0, 0], [0, 0, 1], [0, 0, 2], [0, 0, 3]], - pbc=True, - cell=[[10, 0, 0], [0, 10, 0], [0, 0, 10]], - ) - - finite_differences_positions(calculator, atoms) - - -def test_power_spectrum_unknown_gradient() -> None: - """Test error raise if an unknown gradient is present.""" - - calculator = rascaline.SphericalExpansion(**HYPERS) - - msg = "PowerSpectrum currently only supports gradients w.r.t. to positions" - with pytest.raises(NotImplementedError, match=msg): - PowerSpectrum(calculator).compute(SystemForTests(), gradients=["strain"]) - - -def test_fill_neighbor_type() -> None: - """Test that ``center_type`` keys can be merged for different blocks.""" - - frames = [ - ase.Atoms("H", positions=np.zeros([1, 3])), - ase.Atoms("O", positions=np.zeros([1, 3])), - ] - - calculator = PowerSpectrum( - calculator_1=rascaline.SphericalExpansion(**HYPERS), - calculator_2=rascaline.SphericalExpansion(**HYPERS), - ) - - descriptor = calculator.compute(frames) - - descriptor.keys_to_samples("center_type") - - -def test_fill_types_option() -> None: - """Test that ``types`` options adds arbitrary atomic types.""" - - frames = [ - ase.Atoms("H", positions=np.zeros([1, 3])), - ase.Atoms("O", positions=np.zeros([1, 3])), - ] - - types = [1, 8, 10] - calculator = PowerSpectrum( - calculator_1=rascaline.SphericalExpansion(**HYPERS), types=types - ) - - descriptor = calculator.compute(frames) - - assert_equal(np.unique(descriptor[0].properties["neighbor_1_type"]), types) - assert_equal(np.unique(descriptor[0].properties["neighbor_2_type"]), types) diff --git a/python/rascaline/tests/utils/radial_basis.py b/python/rascaline/tests/utils/radial_basis.py deleted file mode 100644 index de9228a69..000000000 --- a/python/rascaline/tests/utils/radial_basis.py +++ /dev/null @@ -1,93 +0,0 @@ -from typing import Union - -import numpy as np -import pytest -from numpy.testing import assert_allclose - -from rascaline.utils import ( - GtoBasis, - MonomialBasis, - RadialBasisBase, - SphericalBesselBasis, -) - - -pytest.importorskip("scipy") - - -class RtoNRadialBasis(RadialBasisBase): - def compute( - self, n: int, ell: int, integrand_positions: Union[float, np.ndarray] - ) -> Union[float, np.ndarray]: - return integrand_positions**n - - -def test_radial_basis_gram(): - """Test that quad integration of the gram matrix is the same as an analytical.""" - - integration_radius = 1 - max_radial = 4 - max_angular = 2 - - test_basis = RtoNRadialBasis(integration_radius=integration_radius) - - numerical_gram = test_basis.compute_gram_matrix(max_radial, max_angular) - analytical_gram = np.zeros_like(numerical_gram) - - for ell in range(max_angular + 1): - for n1 in range(max_radial): - for n2 in range(max_radial): - exp = 3 + n1 + n2 - analytical_gram[ell, n1, n2] = integration_radius**exp / exp - - assert_allclose(numerical_gram, analytical_gram) - - -def test_radial_basis_orthornormalization(): - integration_radius = 1 - max_radial = 4 - max_angular = 2 - - test_basis = RtoNRadialBasis(integration_radius=integration_radius) - - gram = test_basis.compute_gram_matrix(max_radial, max_angular) - ortho = test_basis.compute_orthonormalization_matrix(max_radial, max_angular) - - for ell in range(max_angular): - eye = ortho[ell] @ gram[ell] @ ortho[ell].T - assert_allclose(eye, np.eye(max_radial, max_radial), atol=1e-11) - - -@pytest.mark.parametrize( - "analytical_basis", - [ - GtoBasis(cutoff=4, max_radial=6), - MonomialBasis(cutoff=4), - SphericalBesselBasis(cutoff=4, max_radial=6, max_angular=4), - ], -) -def test_derivative(analytical_basis: RadialBasisBase): - """Finite difference test for testing the derivative of a radial basis""" - - # Define a helper class that used the numerical derivatives from `RadialBasisBase` - # instead of the explictly implemented analytical ones in the child classes. - class NumericalRadialBasis(RadialBasisBase): - def compute( - self, n: int, ell: int, integrand_positions: Union[float, np.ndarray] - ) -> Union[float, np.ndarray]: - return analytical_basis.compute(n, ell, integrand_positions) - - numerical_basis = NumericalRadialBasis(integration_radius=np.inf) - - cutoff = 4 - max_radial = 6 - max_angular = 4 - positions = np.linspace(2, cutoff) - - for n in range(max_radial): - for ell in range(max_angular): - assert_allclose( - numerical_basis.compute_derivative(n, ell, positions), - analytical_basis.compute_derivative(n, ell, positions), - atol=1e-9, - ) diff --git a/python/rascaline/tests/utils/splines.py b/python/rascaline/tests/utils/splines.py deleted file mode 100644 index c120c66a4..000000000 --- a/python/rascaline/tests/utils/splines.py +++ /dev/null @@ -1,383 +0,0 @@ -import numpy as np -import pytest -from numpy.testing import assert_allclose, assert_equal - -from rascaline import LodeSphericalExpansion, SphericalExpansion -from rascaline.utils import ( - DeltaDensity, - GaussianDensity, - GtoBasis, - LodeDensity, - LodeSpliner, - RadialIntegralFromFunction, - SoapSpliner, -) - -from ..test_systems import SystemForTests - - -pytest.importorskip("scipy") -from scipy.special import gamma, hyp1f1 # noqa - - -def sine(n: int, ell: int, positions: np.ndarray) -> np.ndarray: - return np.sin(positions) - - -def cosine(n: int, ell: int, positions: np.ndarray) -> np.ndarray: - return np.cos(positions) - - -@pytest.mark.parametrize("n_spline_points", [None, 1234]) -def test_splines_with_n_spline_points(n_spline_points): - spline_cutoff = 8.0 - - spliner = RadialIntegralFromFunction( - radial_integral=sine, - max_radial=12, - max_angular=9, - spline_cutoff=spline_cutoff, - radial_integral_derivative=cosine, - ) - - radial_integral = spliner.compute(n_spline_points=n_spline_points)[ - "TabulatedRadialIntegral" - ] - - # check central contribution is not added - with pytest.raises(KeyError): - radial_integral["center_contribution"] - - spline_points = radial_integral["points"] - - # check that the first spline point is at 0 - assert spline_points[0]["position"] == 0.0 - - # check that the last spline point is at the cutoff radius - assert spline_points[-1]["position"] == 8.0 - - # ensure correct length for values representation - assert len(spline_points[52]["values"]["data"]) == (9 + 1) * 12 - - # ensure correct length for derivatives representation - assert len(spline_points[23]["derivatives"]["data"]) == (9 + 1) * 12 - - # check values at r = 0.0 - assert np.allclose( - np.array(spline_points[0]["values"]["data"]), np.zeros((9 + 1) * 12) - ) - - # check derivatives at r = 0.0 - assert np.allclose( - np.array(spline_points[0]["derivatives"]["data"]), np.ones((9 + 1) * 12) - ) - - n_spline_points = len(spline_points) - random_spline_point = 123 - random_x = random_spline_point * spline_cutoff / (n_spline_points - 1) - - # check value of a random spline point - assert np.allclose( - np.array(spline_points[random_spline_point]["values"]["data"]), - np.sin(random_x) * np.ones((9 + 1) * 12), - ) - - -def test_splines_numerical_derivative(): - kwargs = { - "radial_integral": sine, - "max_radial": 12, - "max_angular": 9, - "spline_cutoff": 8.0, - } - - spliner = RadialIntegralFromFunction(**kwargs, radial_integral_derivative=cosine) - spliner_numerical = RadialIntegralFromFunction(**kwargs) - - spline_points = spliner.compute()["TabulatedRadialIntegral"]["points"] - spline_points_numerical = spliner_numerical.compute()["TabulatedRadialIntegral"][ - "points" - ] - - for s, s_num in zip(spline_points, spline_points_numerical): - assert_equal(s["values"]["data"], s_num["values"]["data"]) - assert_allclose( - s["derivatives"]["data"], s_num["derivatives"]["data"], rtol=1e-7 - ) - - -def test_splines_numerical_derivative_error(): - kwargs = { - "radial_integral": sine, - "max_radial": 12, - "max_angular": 9, - "spline_cutoff": 1e-3, - } - - match = "Numerically derivative of the radial integral can not be performed" - with pytest.raises(ValueError, match=match): - RadialIntegralFromFunction(**kwargs).compute() - - -def test_kspace_radial_integral(): - """Test against analytical integral with Gaussian densities and GTOs""" - - cutoff = 2 - max_radial = 6 - max_angular = 3 - atomic_gaussian_width = 1.0 - k_cutoff = 1.2 * np.pi / atomic_gaussian_width - - basis = GtoBasis(cutoff=cutoff, max_radial=max_radial) - - spliner = LodeSpliner( - max_radial=max_radial, - max_angular=max_angular, - k_cutoff=k_cutoff, - basis=basis, - density=DeltaDensity(), # density does not enter in a Kspace radial integral - accuracy=1e-8, - ) - - Neval = 100 - kk = np.linspace(0, k_cutoff, Neval) - - sigma = np.ones(max_radial, dtype=float) - for i in range(1, max_radial): - sigma[i] = np.sqrt(i) - sigma *= cutoff / max_radial - - factors = np.sqrt(np.pi) * np.ones((max_radial, max_angular + 1)) - - coeffs_num = np.zeros([max_radial, max_angular + 1, Neval]) - coeffs_exact = np.zeros_like(coeffs_num) - - for ell in range(max_angular + 1): - for n in range(max_radial): - i1 = 0.5 * (3 + n + ell) - i2 = 1.5 + ell - factors[n, ell] *= ( - 2 ** (0.5 * (n - ell - 1)) - * gamma(i1) - / gamma(i2) - * sigma[n] ** (2 * i1) - ) - coeffs_exact[n, ell] = ( - factors[n, ell] * kk**ell * hyp1f1(i1, i2, -0.5 * (kk * sigma[n]) ** 2) - ) - - coeffs_num[n, ell] = spliner.radial_integral(n, ell, kk) - - assert_allclose(coeffs_num, coeffs_exact) - - -def test_rspace_delta(): - cutoff = 2 - max_radial = 6 - max_angular = 3 - - basis = GtoBasis(cutoff=cutoff, max_radial=max_radial) - density = DeltaDensity() - - spliner = SoapSpliner( - max_radial=max_radial, - max_angular=max_angular, - cutoff=cutoff, - basis=basis, - density=density, - accuracy=1e-8, - ) - - positions = np.linspace(1e-10, cutoff) - - for ell in range(max_angular + 1): - for n in range(max_radial): - assert_equal( - spliner.radial_integral(n, ell, positions), - basis.compute(n, ell, positions), - ) - assert_equal( - spliner.radial_integral_derivative(n, ell, positions), - basis.compute_derivative(n, ell, positions), - ) - - -def test_real_space_spliner(): - """Compare splined spherical expansion with GTOs and a Gaussian density to - analytical implementation.""" - cutoff = 8.0 - max_radial = 12 - max_angular = 9 - atomic_gaussian_width = 1.2 - - # We choose an accuracy that is lower then the default one (1e-8) - # to limit the time taken by this test. - accuracy = 1e-6 - - spliner = SoapSpliner( - cutoff=cutoff, - max_radial=max_radial, - max_angular=max_angular, - basis=GtoBasis(cutoff=cutoff, max_radial=max_radial), - density=GaussianDensity(atomic_gaussian_width=atomic_gaussian_width), - accuracy=accuracy, - ) - - hypers_spherical_expansion = { - "cutoff": cutoff, - "max_radial": max_radial, - "max_angular": max_angular, - "center_atom_weight": 1.0, - "atomic_gaussian_width": atomic_gaussian_width, - "cutoff_function": {"Step": {}}, - } - - analytic = SphericalExpansion( - radial_basis={"Gto": {}}, **hypers_spherical_expansion - ).compute(SystemForTests()) - splined = SphericalExpansion( - radial_basis=spliner.compute(), **hypers_spherical_expansion - ).compute(SystemForTests()) - - for key, block_analytic in analytic.items(): - block_splined = splined.block(key) - assert_allclose( - block_splined.values, block_analytic.values, rtol=1e-5, atol=1e-5 - ) - - -@pytest.mark.parametrize("center_atom_weight", [1.0, 0.0]) -@pytest.mark.parametrize("potential_exponent", [0, 1]) -def test_fourier_space_spliner(center_atom_weight, potential_exponent): - """Compare splined LODE spherical expansion with GTOs and a Gaussian density to - analytical implementation.""" - - cutoff = 2 - max_radial = 6 - max_angular = 4 - atomic_gaussian_width = 0.8 - k_cutoff = 1.2 * np.pi / atomic_gaussian_width - - spliner = LodeSpliner( - k_cutoff=k_cutoff, - max_radial=max_radial, - max_angular=max_angular, - basis=GtoBasis(cutoff=cutoff, max_radial=max_radial), - density=LodeDensity( - atomic_gaussian_width=atomic_gaussian_width, - potential_exponent=potential_exponent, - ), - ) - - hypers_spherical_expansion = { - "cutoff": cutoff, - "max_radial": max_radial, - "max_angular": max_angular, - "center_atom_weight": center_atom_weight, - "atomic_gaussian_width": atomic_gaussian_width, - "potential_exponent": potential_exponent, - } - - analytic = LodeSphericalExpansion( - radial_basis={"Gto": {}}, **hypers_spherical_expansion - ).compute(SystemForTests()) - splined = LodeSphericalExpansion( - radial_basis=spliner.compute(), **hypers_spherical_expansion - ).compute(SystemForTests()) - - for key, block_analytic in analytic.items(): - block_splined = splined.block(key) - assert_allclose(block_splined.values, block_analytic.values, atol=1e-14) - - -def test_center_contribution_gto_gaussian(): - cutoff = 2.0 - max_radial = 6 - max_angular = 4 - atomic_gaussian_width = 0.8 - k_cutoff = 1.2 * np.pi / atomic_gaussian_width - - # Numerical evaluation of center contributions - spliner = LodeSpliner( - k_cutoff=k_cutoff, - max_radial=max_radial, - max_angular=max_angular, - basis=GtoBasis(cutoff=cutoff, max_radial=max_radial), - density=GaussianDensity(atomic_gaussian_width=atomic_gaussian_width), - ) - - # Analytical evaluation of center contributions - center_contr_analytical = np.zeros((max_radial)) - - normalization = 1.0 / (np.pi * atomic_gaussian_width**2) ** (3 / 4) - sigma_radial = np.ones(max_radial, dtype=float) - - for n in range(1, max_radial): - sigma_radial[n] = np.sqrt(n) - sigma_radial *= cutoff / max_radial - - for n in range(max_radial): - sigmatemp_sq = 1.0 / ( - 1.0 / atomic_gaussian_width**2 + 1.0 / sigma_radial[n] ** 2 - ) - neff = 0.5 * (3 + n) - center_contr_analytical[n] = (2 * sigmatemp_sq) ** neff * gamma(neff) - - center_contr_analytical *= normalization * 2 * np.pi / np.sqrt(4 * np.pi) - - assert_allclose(spliner.center_contribution, center_contr_analytical, rtol=1e-14) - - -def test_custom_radial_integral(): - cutoff = 2.0 - max_radial = 3 - max_angular = 2 - atomic_gaussian_width = 1.2 - n_spline_points = 20 - - spliner_args = { - "max_radial": max_radial, - "max_angular": max_angular, - "cutoff": cutoff, - "basis": GtoBasis(cutoff=cutoff, max_radial=max_radial), - } - - spliner_analytical = SoapSpliner( - density=GaussianDensity(atomic_gaussian_width), - **spliner_args, - ) - - # Create a custom density that has not type "GaussianDensity" to trigger full - # numerical evaluation of the radial integral. - class mydensity(GaussianDensity): - pass - - spliner_numerical = SoapSpliner( - density=mydensity(atomic_gaussian_width), - **spliner_args, - ) - - splines_analytic = spliner_analytical.compute(n_spline_points) - splines_numerical = spliner_numerical.compute(n_spline_points) - - n_points = len(splines_analytic["TabulatedRadialIntegral"]["points"]) - - for point in range(n_points): - point_analytical = splines_analytic["TabulatedRadialIntegral"]["points"][point] - point_numerical = splines_numerical["TabulatedRadialIntegral"]["points"][point] - - assert point_numerical["position"] == point_analytical["position"] - - assert_allclose( - point_analytical["values"]["data"], - point_numerical["values"]["data"], - atol=1e-9, - ) - - # exlude r=0 because there is a Scipy bug: see comment in - # `SoapSpliner._radial_integral_gaussian_derivative` for details - if point != 0: - assert_allclose( - point_analytical["derivatives"]["data"], - point_numerical["derivatives"]["data"], - ) diff --git a/python/rascaline/tests/utils/test_utils.py b/python/rascaline/tests/utils/test_utils.py deleted file mode 100644 index 77ba7b2e5..000000000 --- a/python/rascaline/tests/utils/test_utils.py +++ /dev/null @@ -1,69 +0,0 @@ -from numpy.testing import assert_allclose - -import rascaline -from rascaline.systems import IntoSystem - - -def finite_differences_positions( - calculator: rascaline.CalculatorBase, - system: IntoSystem, - displacement: float = 1e-6, - rtol: float = 1e-5, - atol: float = 1e-16, -) -> None: - """ - Check that analytical gradients with respect to positions agree with a finite - difference calculation of the gradients. - - The implementation is simular to ``rascaline/src/calculators/tests_utils.rs``. - - :param calculator: calculator used to compute the representation - :param system: Atoms object - :param displacement: distance each atom will be displaced in each direction when - computing finite differences - :param max_relative: Maximal relative error. ``10 * displacement`` is a good - starting point - :param atol: Threshold below which all values are considered zero. This should be - very small (1e-16) to prevent false positives (if all values & gradients are - below that threshold, tests will pass even with wrong gradients) - :raises AssertionError: if the two gradients are not equal up to specified precision - """ - reference = calculator.compute(system, gradients=["positions"]) - - for atom_i in range(len(system)): - for xyz in range(3): - system_pos = system.copy() - system_pos.positions[atom_i, xyz] += displacement / 2 - updated_pos = calculator.compute(system_pos) - - system_neg = system.copy() - system_neg.positions[atom_i, xyz] -= displacement / 2 - updated_neg = calculator.compute(system_neg) - - assert updated_pos.keys == reference.keys - assert updated_neg.keys == reference.keys - - for key, block in reference.items(): - gradients = block.gradient("positions") - - block_pos = updated_pos.block(key) - block_neg = updated_neg.block(key) - - for gradient_i, (sample_i, _, atom) in enumerate(gradients.samples): - if atom != atom_i: - continue - - # check that the sample is the same in both descriptors - assert block_pos.samples[sample_i] == block.samples[sample_i] - assert block_neg.samples[sample_i] == block.samples[sample_i] - - value_pos = block_pos.values[sample_i] - value_neg = block_neg.values[sample_i] - gradient = gradients.values[gradient_i, xyz] - - assert value_pos.shape == gradient.shape - assert value_neg.shape == gradient.shape - - finite_difference = (value_pos - value_neg) / displacement - - assert_allclose(finite_difference, gradient, rtol=rtol, atol=atol) diff --git a/python/rascaline/tests/utils/utils.py b/python/rascaline/tests/utils/utils.py deleted file mode 100644 index 7e7396bb2..000000000 --- a/python/rascaline/tests/utils/utils.py +++ /dev/null @@ -1,53 +0,0 @@ -import metatensor -import numpy as np -import pytest - -import rascaline - - -# Try to import some modules -ase = pytest.importorskip("ase") -import ase.io # noqa: E402 - - -try: - import torch # noqa: F401 - - HAS_TORCH = True -except ImportError: - HAS_TORCH = False - -try: - import metatensor.operations - - HAS_METATENSOR_OPERATIONS = True -except ImportError: - HAS_METATENSOR_OPERATIONS = False - - -def spherical_expansion(): - """Returns a rascaline SphericalExpansion""" - hypers = { - "cutoff": 3.0, # Angstrom - "max_radial": 3, # Exclusive - "max_angular": 3, # Inclusive - "atomic_gaussian_width": 0.3, - "radial_basis": {"Gto": {}}, - "cutoff_function": {"ShiftedCosine": {"width": 0.5}}, - "center_atom_weight": 1.0, - } - calculator = rascaline.SphericalExpansion(**hypers) - - frames = ase.Atoms( - symbols=["O", "H", "H"], - positions=[ - [2.56633400, 2.50000000, 2.50370100], - [1.97361700, 1.73067300, 2.47063400], - [1.97361700, 3.26932700, 2.47063400], - ], - ) - density = calculator.compute(frames) - density = density.keys_to_properties( - metatensor.Labels(["neighbor_type"], np.array([1, 8]).reshape(-1, 1)) - ) - return density diff --git a/python/scripts/generate-declarations.py b/python/scripts/generate-declarations.py index 189feb7c1..22f19da7d 100755 --- a/python/scripts/generate-declarations.py +++ b/python/scripts/generate-declarations.py @@ -10,8 +10,8 @@ METATENSOR_INCLUDE = os.path.join( metatensor.utils.cmake_prefix_path, "..", "..", "include" ) -RASCALINE_HEADER = os.path.relpath( - os.path.join(ROOT, "..", "..", "rascaline-c-api", "include", "rascaline.h") +FEATOMIC_HEADER = os.path.relpath( + os.path.join(ROOT, "..", "..", "featomic", "include", "featomic.h") ) @@ -52,7 +52,7 @@ def __init__(self): self.defines = {} def visit_Decl(self, node): - if not node.name.startswith("rascal_"): + if not node.name.startswith("featomic_"): return function = Function(node.name, node.type.type) @@ -61,7 +61,7 @@ def visit_Decl(self, node): self.functions.append(function) def visit_Typedef(self, node): - if not node.name.startswith("rascal_"): + if not node.name.startswith("featomic_"): return if isinstance(node.type.type, c_ast.Enum): @@ -94,7 +94,7 @@ def parse(file): if "#define" in line: split = line.split() name = split[1] - if name == "RASCALINE_H": + if name == "FEATOMIC_H": continue value = split[2] @@ -103,9 +103,9 @@ def parse(file): def c_type_name(name): - if name.startswith("rascal_"): + if name.startswith("featomic_"): # enums are represented as int - if name == "rascal_indexes_kind": + if name == "featomic_indexes_kind": return "ctypes.c_int" else: return name @@ -213,7 +213,7 @@ def generate_structs(file, structs): def generate_functions(file, functions): file.write("\n\ndef setup_functions(lib):\n") - file.write(" from .status import _check_rascal_status_t\n") + file.write(" from .status import _check_featomic_status_t\n") for function in functions: file.write(f"\n lib.{function.name}.argtypes = [\n ") @@ -226,16 +226,16 @@ def generate_functions(file, functions): file.write("\n ]\n") restype = type_to_ctypes(function.restype) - if restype == "rascal_status_t": - restype = "_check_rascal_status_t" + if restype == "featomic_status_t": + restype = "_check_featomic_status_t" file.write(f" lib.{function.name}.restype = {restype}\n") def generate_declarations(): - data = parse(RASCALINE_HEADER) + data = parse(FEATOMIC_HEADER) - outpath = os.path.join(ROOT, "..", "rascaline", "rascaline", "_c_api.py") + outpath = os.path.join(ROOT, "..", "featomic", "featomic", "_c_api.py") with open(outpath, "w") as file: file.write( """\ diff --git a/python/scripts/rustc-manylinux2014_aarch64/Dockerfile b/python/scripts/rustc-manylinux2014_aarch64/Dockerfile new file mode 100644 index 000000000..4fa131934 --- /dev/null +++ b/python/scripts/rustc-manylinux2014_aarch64/Dockerfile @@ -0,0 +1,11 @@ +# Use manylinux docker image as a base +FROM quay.io/pypa/manylinux2014_aarch64 + +RUN yum install git -y +RUN git config --global --add safe.directory /code + +# Download rustup-init and install +RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain 1.75 + +ENV PATH="/root/.cargo/bin:${PATH}" +ENV RUST_BUILD_TARGET="aarch64-unknown-linux-gnu" diff --git a/python/scripts/rustc-manylinux2014_x86_64/Dockerfile b/python/scripts/rustc-manylinux2014_x86_64/Dockerfile index 4745f73d2..d4fe44c2f 100644 --- a/python/scripts/rustc-manylinux2014_x86_64/Dockerfile +++ b/python/scripts/rustc-manylinux2014_x86_64/Dockerfile @@ -4,7 +4,7 @@ FROM quay.io/pypa/manylinux2014_x86_64 RUN yum install git -y RUN git config --global --add safe.directory /code -# Download rustup-init asn install +# Download rustup-init and install RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain 1.75 ENV PATH="/root/.cargo/bin:${PATH}" diff --git a/python/tests/run-python-tests.rs b/python/tests/run-python-tests.rs index 6ae05d778..b09bd0e16 100644 --- a/python/tests/run-python-tests.rs +++ b/python/tests/run-python-tests.rs @@ -18,9 +18,9 @@ fn run_python_tests() { if cfg!(debug_assertions) { // assume that debug assertion means that we are building the code in // debug mode, even if that could be not true in some cases - tox.env("RASCALINE_BUILD_TYPE", "debug"); + tox.env("FEATOMIC_BUILD_TYPE", "debug"); } else { - tox.env("RASCALINE_BUILD_TYPE", "release"); + tox.env("FEATOMIC_BUILD_TYPE", "release"); } tox.current_dir(&root); let status = tox.status().expect("failed to run tox"); diff --git a/rascaline-c-api/Cargo.toml b/rascaline-c-api/Cargo.toml deleted file mode 100644 index 9b7e06bdd..000000000 --- a/rascaline-c-api/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "rascaline-c-api" -version = "0.1.0" -authors = ["Luthaf "] -edition = "2021" -rust-version = "1.74" - -[lib] -# when https://github.com/rust-lang/cargo/pull/8789 lands, use it here! -# until then, build all the crate-type we need -name = "rascaline" -crate-type = ["cdylib", "staticlib"] -bench = false - -[features] -default = ["chemfiles"] -chemfiles = ["rascaline/chemfiles"] - -[dependencies] -rascaline = {path = "../rascaline", version = "0.1.0", default-features = false} -metatensor = "0.2" - -ndarray = "0.16" -log = { version = "0.4", features = ["std"] } -once_cell = "1" -time-graph = {version = "0.3.0", features = ["table", "json"]} -libc = "0.2" - -[build-dependencies] -cbindgen = { version = "0.27", default-features = false } -fs_extra = "1" -metatensor = "0.2" - -[dev-dependencies] -which = "5" - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin)'] } diff --git a/rascaline-c-api/build.rs b/rascaline-c-api/build.rs deleted file mode 100644 index b03f6aa1e..000000000 --- a/rascaline-c-api/build.rs +++ /dev/null @@ -1,41 +0,0 @@ -#![allow(clippy::field_reassign_with_default)] - -use std::path::PathBuf; - -fn main() { - let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - - let generated_comment = "\ -/* ============ Automatically generated file, DOT NOT EDIT. ============ * - * * - * This file is automatically generated from the rascaline-c-api sources, * - * using cbindgen. If you want to make change to this file (including * - * documentation), make the corresponding changes in the rust sources. * - * =========================================================================== */"; - - let mut config: cbindgen::Config = Default::default(); - config.language = cbindgen::Language::C; - config.cpp_compat = true; - config.includes = vec!["metatensor.h".into()]; - config.include_guard = Some("RASCALINE_H".into()); - config.include_version = false; - config.documentation = true; - config.documentation_style = cbindgen::DocumentationStyle::Doxy; - config.header = Some(generated_comment.into()); - - let result = cbindgen::Builder::new() - .with_crate(crate_dir) - .with_config(config) - .generate() - .map(|data| { - let mut path = PathBuf::from("include"); - path.push("rascaline.h"); - data.write_to_file(&path); - }); - - if result.is_ok() { - println!("cargo:rerun-if-changed=src"); - } else { - // if rascaline header generation failed, we always re-run the build script - } -} diff --git a/rascaline-c-api/cmake/rascaline-config.in.cmake b/rascaline-c-api/cmake/rascaline-config.in.cmake deleted file mode 100644 index ec483597e..000000000 --- a/rascaline-c-api/cmake/rascaline-config.in.cmake +++ /dev/null @@ -1,81 +0,0 @@ -@PACKAGE_INIT@ - -cmake_minimum_required(VERSION 3.16) - -if(rascaline_FOUND) - return() -endif() - -enable_language(CXX) - -get_filename_component(RASCALINE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/@PACKAGE_RELATIVE_PATH@" ABSOLUTE) - -if (WIN32) - set(RASCALINE_SHARED_LOCATION ${RASCALINE_PREFIX_DIR}/@BIN_INSTALL_DIR@/@RASCALINE_SHARED_LIB_NAME@) - set(RASCALINE_IMPLIB_LOCATION ${RASCALINE_PREFIX_DIR}/@LIB_INSTALL_DIR@/@RASCALINE_IMPLIB_NAME@) -else() - set(RASCALINE_SHARED_LOCATION ${RASCALINE_PREFIX_DIR}/@LIB_INSTALL_DIR@/@RASCALINE_SHARED_LIB_NAME@) -endif() - -set(RASCALINE_STATIC_LOCATION ${RASCALINE_PREFIX_DIR}/@LIB_INSTALL_DIR@/@RASCALINE_STATIC_LIB_NAME@) -set(RASCALINE_INCLUDE ${RASCALINE_PREFIX_DIR}/@INCLUDE_INSTALL_DIR@/) - -if (NOT EXISTS ${RASCALINE_INCLUDE}/rascaline.h OR NOT EXISTS ${RASCALINE_INCLUDE}/rascaline.hpp) - message(FATAL_ERROR "could not find rascaline headers in '${RASCALINE_INCLUDE}', please re-install rascaline") -endif() - -find_package(metatensor @METATENSOR_REQUIRED_VERSION@ REQUIRED CONFIG) - -# Shared library target -if (@RASCALINE_INSTALL_BOTH_STATIC_SHARED@ OR @BUILD_SHARED_LIBS@) - if (NOT EXISTS ${RASCALINE_SHARED_LOCATION}) - message(FATAL_ERROR "could not find rascaline library at '${RASCALINE_SHARED_LOCATION}', please re-install rascaline") - endif() - - add_library(rascaline::shared SHARED IMPORTED GLOBAL) - set_target_properties(rascaline::shared PROPERTIES - IMPORTED_LOCATION ${RASCALINE_SHARED_LOCATION} - INTERFACE_INCLUDE_DIRECTORIES ${RASCALINE_INCLUDE} - IMPORTED_LINK_INTERFACE_LANGUAGES CXX - ) - target_link_libraries(rascaline::shared INTERFACE metatensor::shared) - - target_compile_features(rascaline::shared INTERFACE cxx_std_17) - - if (WIN32) - if (NOT EXISTS ${RASCALINE_IMPLIB_LOCATION}) - message(FATAL_ERROR "could not find rascaline library at '${RASCALINE_IMPLIB_LOCATION}', please re-install rascaline") - endif() - - set_target_properties(rascaline::shared PROPERTIES - IMPORTED_IMPLIB ${RASCALINE_IMPLIB_LOCATION} - ) - endif() -endif() - - -# Static library target -if (@RASCALINE_INSTALL_BOTH_STATIC_SHARED@ OR NOT @BUILD_SHARED_LIBS@) - if (NOT EXISTS ${RASCALINE_STATIC_LOCATION}) - message(FATAL_ERROR "could not find rascaline library at '${RASCALINE_STATIC_LOCATION}', please re-install rascaline") - endif() - - add_library(rascaline::static STATIC IMPORTED GLOBAL) - set_target_properties(rascaline::static PROPERTIES - IMPORTED_LOCATION ${RASCALINE_STATIC_LOCATION} - INTERFACE_INCLUDE_DIRECTORIES ${RASCALINE_INCLUDE} - INTERFACE_LINK_LIBRARIES "@CARGO_DEFAULT_LIBRARIES@" - IMPORTED_LINK_INTERFACE_LANGUAGES CXX - ) - target_link_libraries(rascaline::static INTERFACE metatensor::shared) - - target_compile_features(rascaline::static INTERFACE cxx_std_17) -endif() - - -# Export either the shared or static library as the rascaline target -if (@BUILD_SHARED_LIBS@) - add_library(rascaline ALIAS rascaline::shared) -else() - add_library(rascaline ALIAS rascaline::static) -endif() diff --git a/rascaline-c-api/examples/compute-soap.cpp b/rascaline-c-api/examples/compute-soap.cpp deleted file mode 100644 index 9f192dfa0..000000000 --- a/rascaline-c-api/examples/compute-soap.cpp +++ /dev/null @@ -1,46 +0,0 @@ -#include -#include - -int main(int argc, char* argv[]) { - if (argc < 2) { - std::cout << "error: expected a command line argument" << std::endl; - return 1; - } - auto systems = rascaline::BasicSystems(argv[1]); - - // pass hyper-parameters as JSON - const char* parameters = R"({ - "cutoff": 5.0, - "max_radial": 6, - "max_angular": 4, - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "gradients": false, - "radial_basis": { - "Gto": {} - }, - "cutoff_function": { - "ShiftedCosine": {"width": 0.5} - } - })"; - - // create the calculator with its name and parameters - auto calculator = rascaline::Calculator("soap_power_spectrum", parameters); - - // run the calculation - auto descriptor = calculator.compute(systems); - - // The descriptor is a metatensor `TensorMap`, containing multiple blocks. - // We can transform it to a single block containing a dense representation, - // with one sample for each atom-centered environment. - descriptor.keys_to_samples("center_type"); - descriptor.keys_to_properties(std::vector{"neighbor_1_type", "neighbor_2_type"}); - - // extract values from the descriptor in the only remaining block - auto block = descriptor.block_by_id(0); - auto values = block.values(); - - // you can now use values as the input of a machine learning algorithm - - return 0; -} diff --git a/rascaline-c-api/examples/profiling.cpp b/rascaline-c-api/examples/profiling.cpp deleted file mode 100644 index 070a6b066..000000000 --- a/rascaline-c-api/examples/profiling.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include -#include - -/// Compute SOAP power spectrum, this is the same code as the 'compute-soap' -/// example -static metatensor::TensorMap compute_soap(const std::string& path); - -int main(int argc, char* argv[]) { - if (argc < 2) { - std::cout << "error: expected a command line argument" << std::endl; - return 1; - } - - // enable collection of profiling data - rascaline::Profiler::enable(true); - // clear any existing collected data - rascaline::Profiler::clear(); - - auto descriptor = compute_soap(argv[1]); - - // Get the profiling data as a table to display it directly - std::cout << rascaline::Profiler::get("short_table") << std::endl; - // Or save this data as json for future usage - std::cout << rascaline::Profiler::get("json") << std::endl; - - return 0; -} - - -metatensor::TensorMap compute_soap(const std::string& path) { - auto systems = rascaline::BasicSystems(path); - - const char* parameters = R"({ - "cutoff": 5.0, - "max_radial": 6, - "max_angular": 4, - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "gradients": false, - "radial_basis": { - "Gto": {} - }, - "cutoff_function": { - "ShiftedCosine": {"width": 0.5} - } - })"; - - auto calculator = rascaline::Calculator("soap_power_spectrum", parameters); - auto descriptor = calculator.compute(systems); - - descriptor.keys_to_samples("center_type"); - descriptor.keys_to_properties(std::vector{"neighbor_1_type", "neighbor_2_type"}); - - return descriptor; -} diff --git a/rascaline-c-api/src/lib.rs b/rascaline-c-api/src/lib.rs deleted file mode 100644 index 5c9def8c8..000000000 --- a/rascaline-c-api/src/lib.rs +++ /dev/null @@ -1,25 +0,0 @@ -#![warn(clippy::all, clippy::pedantic)] - -// disable some style lints -#![allow(clippy::needless_return, clippy::redundant_field_names, clippy::upper_case_acronyms)] -#![allow(clippy::missing_errors_doc, clippy::missing_safety_doc, clippy::missing_panics_doc)] -#![allow(clippy::must_use_candidate, clippy::uninlined_format_args, clippy::redundant_else)] -#![allow(clippy::let_underscore_untyped, clippy::doc_markdown)] - -mod utils; -#[macro_use] -mod status; -pub use self::status::{catch_unwind, rascal_status_t}; -pub use self::status::{RASCAL_SUCCESS, RASCAL_INVALID_PARAMETER_ERROR, RASCAL_JSON_ERROR}; -pub use self::status::{RASCAL_UTF8_ERROR, RASCAL_CHEMFILES_ERROR, RASCAL_SYSTEM_ERROR}; -pub use self::status::{RASCAL_BUFFER_SIZE_ERROR, RASCAL_INTERNAL_ERROR}; - -mod logging; -pub use self::logging::{RASCAL_LOG_LEVEL_ERROR, RASCAL_LOG_LEVEL_WARN, RASCAL_LOG_LEVEL_INFO}; -pub use self::logging::{RASCAL_LOG_LEVEL_DEBUG, RASCAL_LOG_LEVEL_TRACE}; -pub use self::logging::{rascal_logging_callback_t, rascal_set_logging_callback}; - -pub mod system; -pub mod calculator; - -pub mod profiling; diff --git a/rascaline-c-api/tests/cmake-project/CMakeLists.txt b/rascaline-c-api/tests/cmake-project/CMakeLists.txt deleted file mode 100644 index 770ce80e2..000000000 --- a/rascaline-c-api/tests/cmake-project/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -cmake_minimum_required(VERSION 3.16) - -project(rascaline-test-cmake-project C CXX) - -find_package(rascaline 0.1 REQUIRED) - -add_executable(c-main src/main.c) -target_link_libraries(c-main rascaline) - -add_executable(cxx-main src/main.cpp) -target_link_libraries(cxx-main rascaline) diff --git a/rascaline-c-api/tests/cmake-project/README.md b/rascaline-c-api/tests/cmake-project/README.md deleted file mode 100644 index 3967988b4..000000000 --- a/rascaline-c-api/tests/cmake-project/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Sample CMake project using rascaline - -This is a basic cmake project linking to rascaline from C and C++ code. diff --git a/rascaline-c-api/tests/cxx/systems.cpp b/rascaline-c-api/tests/cxx/systems.cpp deleted file mode 100644 index c8685a038..000000000 --- a/rascaline-c-api/tests/cxx/systems.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include "rascaline.hpp" -#include "catch.hpp" - - -TEST_CASE("basic systems") { - auto systems = rascaline::BasicSystems( - "../../../rascaline/benches/data/silicon_bulk.xyz" - ); - - CHECK(systems.count() == 30); - - auto* system = systems.systems(); - uintptr_t size = 0; - system->size(system->user_data, &size); - CHECK(size == 54); - - const int32_t* types = nullptr; - system->types(system->user_data, &types); - for (size_t i=0; ipositions(system->user_data, &positions); - CHECK_THAT(positions[0], Catch::Matchers::WithinULP(7.8554, 10)); - CHECK_THAT(positions[1], Catch::Matchers::WithinULP(7.84887, 10)); - CHECK_THAT(positions[2], Catch::Matchers::WithinULP(0.0188612, 10)); - - double cell[9] = {0.0}; - system->cell(system->user_data, cell); - CHECK_THAT(cell[0], Catch::Matchers::WithinULP(7.84785, 10)); - CHECK_THAT(cell[1], Catch::Matchers::WithinULP(0.0, 10)); - CHECK_THAT(cell[2], Catch::Matchers::WithinULP(7.84785, 10)); - - CHECK_THAT(cell[3], Catch::Matchers::WithinULP(7.84785, 10)); - CHECK_THAT(cell[4], Catch::Matchers::WithinULP(7.84785, 10)); - CHECK_THAT(cell[5], Catch::Matchers::WithinULP(0.0, 10)); - - CHECK_THAT(cell[6], Catch::Matchers::WithinULP(0.0, 10)); - CHECK_THAT(cell[7], Catch::Matchers::WithinULP(7.84785, 10)); - CHECK_THAT(cell[8], Catch::Matchers::WithinULP(7.84785, 10)); -} - -class BadSystem: public rascaline::System { -public: - uintptr_t size() const override { - throw std::runtime_error("unimplemented function 'size'"); - } - - const int32_t* types() const override { - throw std::runtime_error("unimplemented function 'types'"); - } - - const double* positions() const override { - throw std::runtime_error("unimplemented function 'positions'"); - } - - CellMatrix cell() const override { - throw std::runtime_error("unimplemented function 'cell'"); - } - - void compute_neighbors(double cutoff) override { - throw std::runtime_error("unimplemented function 'compute_neighbors'"); - } - - const std::vector& pairs() const override { - throw std::runtime_error("unimplemented function 'pairs'"); - } - - const std::vector& pairs_containing(uintptr_t atom) const override { - throw std::runtime_error("unimplemented function 'pairs_containing'"); - } -}; - -TEST_CASE("systems errors") { - const char* HYPERS_JSON = R"({ - "cutoff": 3.0, - "delta": 4, - "name": "", - "gradients": true - })"; - - auto system = BadSystem(); - auto calculator = rascaline::Calculator("dummy_calculator", HYPERS_JSON); - - CHECK_THROWS_WITH(calculator.compute(system), "unimplemented function 'types'"); -} diff --git a/rascaline-c-api/tests/systems.cpp b/rascaline-c-api/tests/systems.cpp deleted file mode 100644 index f07c6df74..000000000 --- a/rascaline-c-api/tests/systems.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include "rascaline.h" -#include "catch.hpp" -#include "helpers.hpp" - - -TEST_CASE("basic systems") { - rascal_system_t* systems = nullptr; - uintptr_t count = 0; - - const char* path = "../../rascaline/benches/data/silicon_bulk.xyz"; - CHECK_SUCCESS(rascal_basic_systems_read(path, &systems, &count)); - CHECK(count == 30); - - auto system = systems[0]; - - uintptr_t size = 0; - system.size(system.user_data, &size); - CHECK(size == 54); - - const int32_t* types = nullptr; - system.types(system.user_data, &types); - for (size_t i=0; i - $ - $ -) - -# Create a header defining RASCALINE_TORCH_EXPORT for to export classes/functions -# in DLL on Windows. -set_target_properties(rascaline_torch PROPERTIES - # hide non-exported symbols by default, this mimics Windows behavior on Unix - CXX_VISIBILITY_PRESET hidden -) - -if (RASCALINE_TORCH_FETCH_METATENSOR_TORCH) - # If we install metatensor_torch together with rascaline_torch, we need to - # set the RPATH to $ORIGIN to make sure rascaline_torch can find - # metatensor_torch. - set_target_properties(rascaline_torch PROPERTIES INSTALL_RPATH "$ORIGIN") -endif() - -include(GenerateExportHeader) -generate_export_header(rascaline_torch - BASE_NAME RASCALINE_TORCH - EXPORT_FILE_NAME ${CMAKE_CURRENT_BINARY_DIR}/include/rascaline/torch/exports.h -) -target_compile_definitions(rascaline_torch PRIVATE rascaline_torch_EXPORTS) - -find_package(OpenMP) -if (OpenMP_CXX_FOUND) - # Torch bundles its own copy of the OpenMP runtime library, and if we - # compile and link against the system version as well this can lead to - # crashes during initialization on macOS. - # - # So on this plaftorm we instead compile the code with OpenMP flags, and - # leave the corresponding symbols undefined in `rascaline_torch`, hopping - # that when Torch is loaded we'll get these symbols in the global namespace. - # - # On other platforms, this seems to be less of an issue, maybe because torch - # adds a hash to the library name it bundles (i.e. `libgomp-de42aff.so`) - if (APPLE) - string(REPLACE " " ";" omp_cxx_flags_list ${OpenMP_CXX_FLAGS}) - target_compile_options(rascaline_torch PRIVATE ${omp_cxx_flags_list}) - target_include_directories(rascaline_torch PRIVATE SYSTEM ${OpenMP_CXX_INCLUDE_DIRS}) - target_link_libraries(rascaline_torch PRIVATE -Wl,-undefined,dynamic_lookup) - else() - target_link_libraries(rascaline_torch PRIVATE OpenMP::OpenMP_CXX) - endif() -endif() - -if (RASCALINE_TORCH_TESTS) - enable_testing() - add_subdirectory(tests) -endif() - -#------------------------------------------------------------------------------# -# Installation configuration -#------------------------------------------------------------------------------# -include(CMakePackageConfigHelpers) -write_basic_package_version_file( - rascaline_torch-config-version.cmake - VERSION ${RASCALINE_TORCH_VERSION} - COMPATIBILITY SameMinorVersion -) - -install(TARGETS rascaline_torch - EXPORT rascaline_torch-targets - ARCHIVE DESTINATION ${LIB_INSTALL_DIR} - LIBRARY DESTINATION ${LIB_INSTALL_DIR} - RUNTIME DESTINATION ${BIN_INSTALL_DIR} -) -install(EXPORT rascaline_torch-targets - DESTINATION ${LIB_INSTALL_DIR}/cmake/rascaline_torch -) - -configure_file( - ${CMAKE_CURRENT_SOURCE_DIR}/cmake/rascaline_torch-config.in.cmake - ${CMAKE_CURRENT_BINARY_DIR}/rascaline_torch-config.cmake - @ONLY -) -install(FILES - ${CMAKE_CURRENT_BINARY_DIR}/rascaline_torch-config-version.cmake - ${CMAKE_CURRENT_BINARY_DIR}/rascaline_torch-config.cmake - DESTINATION ${LIB_INSTALL_DIR}/cmake/rascaline_torch -) - -install(DIRECTORY "include/rascaline" DESTINATION ${INCLUDE_INSTALL_DIR}) -install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/include/rascaline DESTINATION ${INCLUDE_INSTALL_DIR}) diff --git a/rascaline-torch/VERSION b/rascaline-torch/VERSION deleted file mode 100644 index 6e8bf73aa..000000000 --- a/rascaline-torch/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.1.0 diff --git a/rascaline-torch/cmake/rascaline_torch-config.in.cmake b/rascaline-torch/cmake/rascaline_torch-config.in.cmake deleted file mode 100644 index d4875e9b8..000000000 --- a/rascaline-torch/cmake/rascaline_torch-config.in.cmake +++ /dev/null @@ -1,18 +0,0 @@ -include(CMakeFindDependencyMacro) - -# use the same version for rascaline as the main CMakeLists.txt -set(REQUIRED_RASCALINE_VERSION @REQUIRED_RASCALINE_VERSION@) -find_package(rascaline ${REQUIRED_RASCALINE_VERSION} CONFIG REQUIRED) - -# use the same version for metatensor_torch as the main CMakeLists.txt -set(REQUIRED_METATENSOR_TORCH_VERSION @REQUIRED_METATENSOR_TORCH_VERSION@) -find_package(metatensor_torch ${REQUIRED_METATENSOR_TORCH_VERSION} CONFIG REQUIRED) - -# We can only load rascaline_torch with the exact same version of Torch that -# was used to compile it (and is stored in BUILD_TORCH_VERSION) -set(BUILD_TORCH_VERSION @Torch_VERSION@) - -find_package(Torch ${BUILD_TORCH_VERSION} REQUIRED EXACT) - - -include(${CMAKE_CURRENT_LIST_DIR}/rascaline_torch-targets.cmake) diff --git a/rascaline-torch/include/rascaline/torch.hpp b/rascaline-torch/include/rascaline/torch.hpp deleted file mode 100644 index 17ce187dc..000000000 --- a/rascaline-torch/include/rascaline/torch.hpp +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef RASCALINE_TORCH_HPP -#define RASCALINE_TORCH_HPP - -#include "rascaline/torch/exports.h" // IWYU pragma: export - -#include "rascaline/torch/system.hpp" // IWYU pragma: export -#include "rascaline/torch/calculator.hpp" // IWYU pragma: export - - -#endif diff --git a/rascaline-torch/tests/cmake-project/CMakeLists.txt b/rascaline-torch/tests/cmake-project/CMakeLists.txt deleted file mode 100644 index 6e4681b59..000000000 --- a/rascaline-torch/tests/cmake-project/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -cmake_minimum_required(VERSION 3.16) - -project(rascaline-torch-test-cmake-project CXX) - -find_package(rascaline_torch 0.1 CONFIG REQUIRED) - -add_executable(torch-main src/main.cpp) -target_link_libraries(torch-main rascaline_torch) - -enable_testing() -add_test(NAME torch-main COMMAND torch-main) - -if(WIN32) - # We need to set the path to allow access to metatensor.dll - STRING(REPLACE ";" "\\;" PATH_STRING "$ENV{PATH}") - set_tests_properties(torch-main PROPERTIES - ENVIRONMENT "PATH=${PATH_STRING}\;$\;$\;$" - ) -endif() diff --git a/rascaline-torch/tests/cmake-project/README.md b/rascaline-torch/tests/cmake-project/README.md deleted file mode 100644 index c155aa299..000000000 --- a/rascaline-torch/tests/cmake-project/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Sample CMake project using rascaline-torch - -This is a basic cmake project linking to rascaline-torch from C++ code. diff --git a/rascaline/Cargo.toml b/rascaline/Cargo.toml deleted file mode 100644 index 95b4b0415..000000000 --- a/rascaline/Cargo.toml +++ /dev/null @@ -1,65 +0,0 @@ -[package] -name = "rascaline" -version = "0.1.0" -authors = ["Luthaf "] -edition = "2021" -rust-version = "1.74" - -[lib] -bench = false - -[features] -# We use a static version of metatensor by default since otherwise doctests can -# not find libmetatensor.so -default = ["chemfiles", "static-metatensor"] - -static-metatensor = ["metatensor/static"] - -[[bench]] -name = "spherical-harmonics" -harness = false - -[[bench]] -name = "soap-radial-integral" -harness = false - -[[bench]] -name = "lode-spherical-expansion" -harness = false - -[[bench]] -name = "soap-spherical-expansion" -harness = false - -[[bench]] -name = "soap-power-spectrum" -harness = false - -[dependencies] -metatensor = {version = "0.2", features = ["rayon"]} - -ndarray = {version = "0.16", features = ["rayon", "serde", "approx"]} -num-traits = "0.2" -rayon = "1.5" - -log = "0.4" -once_cell = "1" -indexmap = "2" -thread_local = "1.1" -time-graph = "0.3.0" - -serde = { version = "1", features = ["derive"] } -serde_json = "1" -schemars = "0.8" - -chemfiles = {version = "0.10", optional = true} - -approx = "0.5" - -[dev-dependencies] -criterion = "0.5" - -glob = "0.3" -ndarray-npy = "0.9" -flate2 = "1.0.20" -time-graph = {version = "0.3.0", features = ["table", "json"]} diff --git a/rascaline/benches/soap-radial-integral.rs b/rascaline/benches/soap-radial-integral.rs deleted file mode 100644 index c5b7ec369..000000000 --- a/rascaline/benches/soap-radial-integral.rs +++ /dev/null @@ -1,97 +0,0 @@ -#![allow(clippy::needless_return)] - -use rascaline::calculators::soap::SoapRadialIntegral; -use rascaline::calculators::soap::{SoapRadialIntegralGtoParameters, SoapRadialIntegralGto}; -use rascaline::calculators::soap::{SoapRadialIntegralSpline, SoapRadialIntegralSplineParameters}; - -use ndarray::Array2; - -use criterion::{Criterion, black_box, criterion_group, criterion_main}; -use criterion::{BenchmarkGroup, measurement::WallTime}; - -fn benchmark_radial_integral( - mut group: BenchmarkGroup<'_, WallTime>, - benchmark_gradients: bool, - create_radial_integral: impl Fn(usize, usize) -> Box, -) { - for &max_angular in black_box(&[1, 7, 15]) { - for &max_radial in black_box(&[2, 8, 14]) { - let ri = create_radial_integral(max_angular, max_radial); - - let mut values = Array2::from_elem((max_angular + 1, max_radial), 0.0); - let mut gradients = Array2::from_elem((max_angular + 1, max_radial), 0.0); - - // multiple random values spanning the whole range [0, cutoff) - let distances = [ - 0.145, 0.218, 0.585, 0.723, 1.011, 1.463, 1.560, 1.704, - 2.109, 2.266, 2.852, 2.942, 3.021, 3.247, 3.859, 4.462, - ]; - - group.bench_function(format!("n_max = {}, l_max = {}", max_radial, max_angular), |b| b.iter_custom(|repeat| { - let start = std::time::Instant::now(); - for _ in 0..repeat { - for &distance in &distances { - if benchmark_gradients { - ri.compute(distance, values.view_mut(), Some(gradients.view_mut())) - } else { - ri.compute(distance, values.view_mut(), None) - } - } - } - start.elapsed() / distances.len() as u32 - })); - } - } -} - -fn gto_radial_integral(c: &mut Criterion) { - let create_radial_integral = |max_angular, max_radial| { - let parameters = SoapRadialIntegralGtoParameters { - max_radial, - max_angular, - cutoff: 4.5, - atomic_gaussian_width: 0.5, - }; - return Box::new(SoapRadialIntegralGto::new(parameters).unwrap()) as Box; - }; - - let mut group = c.benchmark_group("GTO (per neighbor)"); - group.noise_threshold(0.05); - benchmark_radial_integral(group, false, create_radial_integral); - - let mut group = c.benchmark_group("GTO with gradients (per neighbor)"); - group.noise_threshold(0.05); - benchmark_radial_integral(group, true, create_radial_integral); -} - -fn splined_gto_radial_integral(c: &mut Criterion) { - let create_radial_integral = |max_angular, max_radial| { - let cutoff = 4.5; - let parameters = SoapRadialIntegralGtoParameters { - max_radial, - max_angular, - cutoff, - atomic_gaussian_width: 0.5, - }; - let gto = SoapRadialIntegralGto::new(parameters).unwrap(); - - let parameters = SoapRadialIntegralSplineParameters { - max_radial, - max_angular, - cutoff, - }; - let accuracy = 1e-8; - return Box::new(SoapRadialIntegralSpline::with_accuracy(parameters, accuracy, gto).unwrap()) as Box; - }; - - let mut group = c.benchmark_group("Splined GTO (per neighbor)"); - group.noise_threshold(0.05); - benchmark_radial_integral(group, false, create_radial_integral); - - let mut group = c.benchmark_group("Splined GTO with gradients (per neighbor)"); - group.noise_threshold(0.05); - benchmark_radial_integral(group, true, create_radial_integral); -} - -criterion_group!(gto, gto_radial_integral, splined_gto_radial_integral); -criterion_main!(gto); diff --git a/rascaline/src/calculators/lode/mod.rs b/rascaline/src/calculators/lode/mod.rs deleted file mode 100644 index e8772b925..000000000 --- a/rascaline/src/calculators/lode/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod radial_integral; -pub use self::radial_integral::{LodeRadialIntegral, LodeRadialIntegralParameters}; -pub use self::radial_integral::{LodeRadialIntegralGto, LodeRadialIntegralGtoParameters}; -pub use self::radial_integral::{LodeRadialIntegralSpline, LodeRadialIntegralSplineParameters}; - - -mod spherical_expansion; -pub use self::spherical_expansion::{LodeSphericalExpansion, LodeSphericalExpansionParameters}; diff --git a/rascaline/src/calculators/lode/radial_integral/gto.rs b/rascaline/src/calculators/lode/radial_integral/gto.rs deleted file mode 100644 index ca786f102..000000000 --- a/rascaline/src/calculators/lode/radial_integral/gto.rs +++ /dev/null @@ -1,297 +0,0 @@ -use std::f64; - -use ndarray::{Array1, Array2, ArrayViewMut2}; - -use crate::math::{hyp2f1, gamma, DoubleRegularized1F1}; -use crate::Error; - -use super::LodeRadialIntegral; -use crate::calculators::radial_basis::GtoRadialBasis; - -/// Parameters controlling the LODE radial integral with GTO radial basis -#[derive(Debug, Clone, Copy)] -pub struct LodeRadialIntegralGtoParameters { - /// Number of radial components - pub max_radial: usize, - /// Number of angular components - pub max_angular: usize, - /// atomic density gaussian width - pub atomic_gaussian_width: f64, - /// potential exponent - pub potential_exponent: usize, - /// cutoff radius - pub cutoff: f64, -} - -impl LodeRadialIntegralGtoParameters { - pub(crate) fn validate(&self) -> Result<(), Error> { - if self.max_radial == 0 { - return Err(Error::InvalidParameter( - "max_radial must be at least 1 for GTO radial integral".into() - )); - } - - if self.cutoff <= 1e-16 || !self.cutoff.is_finite() { - return Err(Error::InvalidParameter( - "cutoff must be a positive number for GTO radial integral".into() - )); - } - - if self.atomic_gaussian_width <= 1e-16 || !self.atomic_gaussian_width.is_finite() { - return Err(Error::InvalidParameter( - "atomic_gaussian_width must be a positive number for GTO radial integral".into() - )); - } - - Ok(()) - } -} - -/// Implementation of the LODE radial integral for GTO radial basis and Gaussian -/// atomic density. -#[derive(Debug, Clone)] -pub struct LodeRadialIntegralGto { - parameters: LodeRadialIntegralGtoParameters, - /// `sigma_n` GTO gaussian width, i.e. `cutoff * max(√n, 1) / n_max` - gto_gaussian_widths: Vec, - /// `n_max * n_max` matrix to orthonormalize the GTO - gto_orthonormalization: Array2, - /// Implementation of `Gamma(a) / Gamma(b) 1F1(a, b, z)` - double_regularized_1f1: DoubleRegularized1F1, -} - -impl LodeRadialIntegralGto { - pub fn new(parameters: LodeRadialIntegralGtoParameters) -> Result { - parameters.validate()?; - - let basis = GtoRadialBasis { - max_radial: parameters.max_radial, - cutoff: parameters.cutoff, - }; - let gto_gaussian_widths = basis.gaussian_widths(); - let gto_orthonormalization = basis.orthonormalization_matrix(); - - return Ok(LodeRadialIntegralGto { - parameters: parameters, - double_regularized_1f1: DoubleRegularized1F1 { - max_angular: parameters.max_angular, - }, - gto_gaussian_widths: gto_gaussian_widths, - gto_orthonormalization: gto_orthonormalization.t().to_owned(), - }) - } -} - -impl LodeRadialIntegral for LodeRadialIntegralGto { - #[time_graph::instrument(name = "LodeRadialIntegralGto::compute")] - fn compute( - &self, - k_norm: f64, - mut values: ArrayViewMut2, - mut gradients: Option> - ) { - let expected_shape = [self.parameters.max_angular + 1, self.parameters.max_radial]; - assert_eq!( - values.shape(), expected_shape, - "wrong size for values array, expected [{}, {}] but got [{}, {}]", - expected_shape[0], expected_shape[1], values.shape()[0], values.shape()[1] - ); - - if let Some(ref gradients) = gradients { - assert_eq!( - gradients.shape(), expected_shape, - "wrong size for gradients array, expected [{}, {}] but got [{}, {}]", - expected_shape[0], expected_shape[1], gradients.shape()[0], gradients.shape()[1] - ); - } - - let global_factor = std::f64::consts::PI.sqrt() / std::f64::consts::SQRT_2; - - for n in 0..self.parameters.max_radial { - let sigma_n = self.gto_gaussian_widths[n]; - let k_sigma_n_sqrt2 = k_norm * sigma_n / std::f64::consts::SQRT_2; - // `global_factor * sqrt(2)^{n} * sigma_n^{n + 3} * (k * sigma_n / sqrt(2))^l` - let mut factor = global_factor * sigma_n.powi(n as i32 + 3) * std::f64::consts::SQRT_2.powi(n as i32); - - let k_norm_sigma_n_2 = - k_norm * sigma_n * sigma_n; - let z = 0.5 * k_norm * k_norm_sigma_n_2; - self.double_regularized_1f1.compute( - z, n, - values.index_axis_mut(ndarray::Axis(1), n), - gradients.as_mut().map(|g| g.index_axis_mut(ndarray::Axis(1), n)) - ); - - for l in 0..(self.parameters.max_angular + 1) { - assert!(values[[l, n]].is_finite()); - values[[l, n]] *= factor; - if let Some(ref mut gradients) = gradients { - gradients[[l, n]] *= k_norm_sigma_n_2 * factor; - gradients[[l, n]] += l as f64 / k_norm * values[[l, n]]; - } - - factor *= k_sigma_n_sqrt2; - } - } - - // for k_norm = 0, the formula used in the calculations above yield NaN, - // which in turns breaks the SplinedGto radial integral. From the - // analytical formula, the gradient is 0 everywhere expect for l=1 - if k_norm == 0.0 { - if let Some(ref mut gradients) = gradients { - gradients.fill(0.0); - - if self.parameters.max_angular >= 1 { - let l = 1; - for n in 0..self.parameters.max_radial { - let sigma_n = self.gto_gaussian_widths[n]; - let a = 0.5 * (n + l) as f64 + 1.5; - let b = 2.5; - let factor = global_factor * sigma_n.powi((n + l) as i32 + 3) * std::f64::consts::SQRT_2.powi(n as i32 - l as i32); - - gradients[[l, n]] = gamma(a) / gamma(b) * factor; - } - } - } - } - - values.assign(&values.dot(&self.gto_orthonormalization)); - if let Some(ref mut gradients) = gradients { - gradients.assign(&gradients.dot(&self.gto_orthonormalization)); - } - } - - fn compute_center_contribution(&self) -> Array1 { - let max_radial = self.parameters.max_radial; - let atomic_gaussian_width = self.parameters.atomic_gaussian_width; - let potential_exponent = self.parameters.potential_exponent as f64; - - let mut contrib = Array1::from_elem(max_radial, 0.0); - - let basis = GtoRadialBasis { - max_radial, - cutoff: self.parameters.cutoff, - }; - let gto_gaussian_widths = basis.gaussian_widths(); - let n_eff: Vec = (0..max_radial) - .map(|n| 0.5 * (3. + n as f64)) - .collect(); - - if potential_exponent == 0. { - let factor = std::f64::consts::PI.powf(-0.25) - / (atomic_gaussian_width * atomic_gaussian_width).powf(0.75); - - for n in 0..max_radial { - let alpha = 0.5 - * (1. / (atomic_gaussian_width * atomic_gaussian_width) - + 1. / (gto_gaussian_widths[n] * gto_gaussian_widths[n])); - contrib[n] = factor * gamma(n_eff[n]) / alpha.powf(n_eff[n]); - } - } else { - let factor = 2. * f64::sqrt(4. * std::f64::consts::PI) - / gamma(potential_exponent / 2.) - / potential_exponent; - - for n in 0..max_radial { - let s = atomic_gaussian_width / gto_gaussian_widths[n]; - let hyparg = 1. / (1. + s * s); - - contrib[n] = factor - * 2_f64.powf((1. + n as f64 - potential_exponent) / 2.) - * atomic_gaussian_width.powi(3 + n as i32 - potential_exponent as i32) - * gamma(n_eff[n]) - * hyp2f1(1., n_eff[n], (potential_exponent + 2.) / 2., hyparg) - * hyparg.powf(n_eff[n]); - } - } - - let gto_orthonormalization = basis.orthonormalization_matrix(); - - return gto_orthonormalization.dot(&(contrib)); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use ndarray::Array2; - use approx::assert_relative_eq; - - #[test] - fn gradients_near_zero() { - let max_radial = 8; - let max_angular = 8; - let gto = LodeRadialIntegralGto::new(LodeRadialIntegralGtoParameters { - max_radial: max_radial, - max_angular: max_angular, - cutoff: 5.0, - atomic_gaussian_width: 0.5, - potential_exponent: 1, - }).unwrap(); - - let shape = (max_angular + 1, max_radial); - let mut values = Array2::from_elem(shape, 0.0); - let mut gradients = Array2::from_elem(shape, 0.0); - let mut gradients_plus = Array2::from_elem(shape, 0.0); - gto.compute(0.0, values.view_mut(), Some(gradients.view_mut())); - gto.compute(1e-12, values.view_mut(), Some(gradients_plus.view_mut())); - - assert_relative_eq!( - gradients, gradients_plus, epsilon=1e-9, max_relative=1e-6, - ); - } - - #[test] - fn finite_differences() { - let max_radial = 8; - let max_angular = 8; - let gto = LodeRadialIntegralGto::new(LodeRadialIntegralGtoParameters { - max_radial: max_radial, - max_angular: max_angular, - cutoff: 5.0, - atomic_gaussian_width: 0.5, - potential_exponent: 1, - }).unwrap(); - - let k = 3.4; - let delta = 1e-6; - - let shape = (max_angular + 1, max_radial); - let mut values = Array2::from_elem(shape, 0.0); - let mut values_delta = Array2::from_elem(shape, 0.0); - let mut gradients = Array2::from_elem(shape, 0.0); - gto.compute(k, values.view_mut(), Some(gradients.view_mut())); - gto.compute(k + delta, values_delta.view_mut(), None); - - let finite_differences = (&values_delta - &values) / delta; - - assert_relative_eq!( - finite_differences, gradients, max_relative=1e-4 - ); - } - - #[test] - fn central_atom_contribution() { - let potential_exponents = [0, 1, 2, 6]; - - // Reference values taken from pyLODE - let reference_vals = [ - [7.09990773e-01, 6.13767550e-01, 3.34161655e-01, 8.35301652e-02, 1.78439072e-02, -3.44944648e-05], - [1.69193719, 2.02389574, 2.85086136, 3.84013091, 1.62869125, 7.03338899], - [1.00532822, 1.10024472, 1.34843326, 1.19816598, 0.69150744, 1.2765415], - [0.03811939, 0.03741200, 0.03115835, 0.01364843, 0.00534184, 0.00205973]]; - - for (i, &p) in potential_exponents.iter().enumerate(){ - let gto = LodeRadialIntegralGto::new(LodeRadialIntegralGtoParameters { - cutoff: 5.0, - max_radial: 6, - max_angular: 2, - atomic_gaussian_width: 1.0, - potential_exponent: p, - }).unwrap(); - - let center_contrib = gto.compute_center_contribution(); - assert_relative_eq!(center_contrib, ndarray::arr1(&reference_vals[i]), max_relative=3e-6); - }; - } -} diff --git a/rascaline/src/calculators/lode/radial_integral/mod.rs b/rascaline/src/calculators/lode/radial_integral/mod.rs deleted file mode 100644 index c76b626df..000000000 --- a/rascaline/src/calculators/lode/radial_integral/mod.rs +++ /dev/null @@ -1,141 +0,0 @@ -use ndarray::{ArrayViewMut2, Array1, Array2}; - -use crate::Error; -use crate::calculators::radial_basis::RadialBasis; - -/// A `LodeRadialIntegral` computes the LODE radial integral on a given radial basis. -/// -/// See equations 5 to 8 of [this paper](https://doi.org/10.1063/5.0044689) for -/// mor information on the radial integral. -/// -/// `std::panic::RefUnwindSafe` is a required super-trait to enable passing -/// radial integrals across the C API. `Send` is a required super-trait to -/// enable passing radial integrals between threads. -pub trait LodeRadialIntegral: std::panic::RefUnwindSafe + Send { - /// Compute the LODE radial integral for a single k-vector `norm` and store - /// the resulting data in the `(max_angular + 1) x max_radial` array - /// `values`. If `gradients` is `Some`, also compute and store gradients - /// there. - fn compute(&self, k_norm: f64, values: ArrayViewMut2, gradients: Option>); - - /// Compute the contribution of the central atom to the final `` - /// coefficients. By symmetry, only l=0 is non-zero, so this function - /// returns a 1-D array containing the different `` coefficients. - /// - /// This function differs from the rest of LODE calculation because it goes - /// straight from atom => n l m, without using k-space projection in the - /// middle. - fn compute_center_contribution(&self) -> Array1; -} - -mod gto; -pub use self::gto::{LodeRadialIntegralGto, LodeRadialIntegralGtoParameters}; - -mod spline; -pub use self::spline::{LodeRadialIntegralSpline, LodeRadialIntegralSplineParameters}; - -/// Parameters controlling the radial integral for LODE -#[derive(Debug, Clone, Copy)] -pub struct LodeRadialIntegralParameters { - pub max_radial: usize, - pub max_angular: usize, - pub atomic_gaussian_width: f64, - pub potential_exponent: usize, - pub cutoff: f64, - pub k_cutoff: f64, -} - -/// Store together a Radial integral implementation and cached allocation for -/// values/gradients. -pub struct LodeRadialIntegralCache { - /// Implementation of the radial integral - code: Box, - /// Cache for the radial integral values - pub(crate) values: Array2, - /// Cache for the radial integral gradient - pub(crate) gradients: Array2, - - /// Cache for the central atom contribution - pub(crate) center_contribution: Array1, -} - -impl LodeRadialIntegralCache { - /// Create a new `RadialIntegralCache` for the given radial basis & parameters - #[allow(clippy::needless_pass_by_value)] - pub fn new(radial_basis: RadialBasis, parameters: LodeRadialIntegralParameters) -> Result { - let code = match radial_basis { - RadialBasis::Gto {splined_radial_integral, spline_accuracy} => { - let gto_parameters = LodeRadialIntegralGtoParameters { - max_radial: parameters.max_radial, - max_angular: parameters.max_angular, - atomic_gaussian_width: parameters.atomic_gaussian_width, - potential_exponent: parameters.potential_exponent, - cutoff: parameters.cutoff, - }; - let gto = LodeRadialIntegralGto::new(gto_parameters)?; - - if splined_radial_integral { - let parameters = LodeRadialIntegralSplineParameters { - max_radial: parameters.max_radial, - max_angular: parameters.max_angular, - // the largest value the spline should interpolate is - // the k-space cutoff, not the real-space cutoff - // associated with the GTO basis - k_cutoff: parameters.k_cutoff, - }; - - Box::new(LodeRadialIntegralSpline::with_accuracy( - parameters, spline_accuracy, gto - )?) - } else { - Box::new(gto) as Box - } - } - RadialBasis::TabulatedRadialIntegral {points, center_contribution} => { - let parameters = LodeRadialIntegralSplineParameters { - max_radial: parameters.max_radial, - max_angular: parameters.max_angular, - k_cutoff: parameters.k_cutoff, - }; - - let center_contribution = center_contribution.ok_or(Error::InvalidParameter( - "For a tabulated radial integral with LODE please provide the - `center_contribution`.".into()))?; - - Box::new(LodeRadialIntegralSpline::from_tabulated( - parameters, points, center_contribution - )?) - } - }; - let shape = (parameters.max_angular + 1, parameters.max_radial); - let values = Array2::from_elem(shape, 0.0); - let gradients = Array2::from_elem(shape, 0.0); - let center_contribution = Array1::from_elem(parameters.max_radial, 0.0); - - return Ok(LodeRadialIntegralCache { code, values, gradients, center_contribution }); - } - - /// Run the calculation, the results are stored inside `self.values` and - /// `self.gradients` - pub fn compute(&mut self, k_norm: f64, gradients: bool) { - if gradients { - self.code.compute( - k_norm, - self.values.view_mut(), - Some(self.gradients.view_mut()), - ); - } else { - self.code.compute( - k_norm, - self.values.view_mut(), - None, - ); - } - } - - /// Run `compute_center_contribution`, and store the results in - /// `self.center_contributions` - pub fn compute_center_contribution(&mut self) { - self.center_contribution = self.code.compute_center_contribution(); - } -} diff --git a/rascaline/src/calculators/lode/radial_integral/spline.rs b/rascaline/src/calculators/lode/radial_integral/spline.rs deleted file mode 100644 index 7597de15f..000000000 --- a/rascaline/src/calculators/lode/radial_integral/spline.rs +++ /dev/null @@ -1,257 +0,0 @@ -use ndarray::{Array1, Array2, ArrayViewMut2}; - -use super::LodeRadialIntegral; -use crate::math::{HermitCubicSpline, SplineParameters, HermitSplinePoint}; -use crate::calculators::radial_basis::SplinePoint; -use crate::Error; - -/// `LodeRadialIntegralSpline` allows to evaluate another radial integral -/// implementation using [cubic Hermit spline][splines-wiki]. -/// -/// This can be much faster than using the actual radial integral -/// implementation. -/// -/// [splines-wiki]: https://en.wikipedia.org/wiki/Cubic_Hermite_spline -pub struct LodeRadialIntegralSpline { - spline: HermitCubicSpline, - center_contribution: ndarray::Array1, -} - -/// Parameters for computing the radial integral using Hermit cubic splines -#[derive(Debug, Clone, Copy)] -pub struct LodeRadialIntegralSplineParameters { - /// Number of radial components - pub max_radial: usize, - /// Number of angular components - pub max_angular: usize, - /// k-space cutoff radius, this is also the maximal value that can be interpolated - pub k_cutoff: f64, -} - -impl LodeRadialIntegralSpline { - /// Create a new `LodeRadialIntegralSpline` taking values from the given - /// `radial_integral`. Points are added to the spline until the requested - /// accuracy is reached. We consider that the accuracy is reached when - /// either the mean absolute error or the mean relative error gets below the - /// `accuracy` threshold. - #[time_graph::instrument(name = "LodeRadialIntegralSpline::with_accuracy")] - pub fn with_accuracy( - parameters: LodeRadialIntegralSplineParameters, - accuracy: f64, - radial_integral: impl LodeRadialIntegral - ) -> Result { - let shape_tuple = (parameters.max_angular + 1, parameters.max_radial); - - let parameters = SplineParameters { - start: 0.0, - stop: parameters.k_cutoff, - shape: vec![parameters.max_angular + 1, parameters.max_radial], - }; - - let spline = HermitCubicSpline::with_accuracy( - accuracy, - parameters, - |x| { - let mut values = Array2::from_elem(shape_tuple, 0.0); - let mut gradients = Array2::from_elem(shape_tuple, 0.0); - radial_integral.compute(x, values.view_mut(), Some(gradients.view_mut())); - (values, gradients) - }, - )?; - - return Ok(LodeRadialIntegralSpline { - spline, - center_contribution: radial_integral.compute_center_contribution() - }); - } - - /// Create a new `LodeRadialIntegralSpline` with user-defined spline points. - pub fn from_tabulated( - parameters: LodeRadialIntegralSplineParameters, - spline_points: Vec, - center_contribution: Vec, - ) -> Result { - - let spline_parameters = SplineParameters { - start: 0.0, - stop: parameters.k_cutoff, - shape: vec![parameters.max_angular + 1, parameters.max_radial], - }; - - let mut new_spline_points = Vec::new(); - for spline_point in spline_points { - new_spline_points.push( - HermitSplinePoint{ - position: spline_point.position, - values: spline_point.values.0.clone(), - derivatives: spline_point.derivatives.0.clone(), - } - ); - } - - if center_contribution.len() != parameters.max_radial { - return Err(Error::InvalidParameter(format!( - "wrong length of center_contribution, expected {} elements but got {}", - parameters.max_radial, center_contribution.len() - ))) - } - - let spline = HermitCubicSpline::new(spline_parameters, new_spline_points); - return Ok(LodeRadialIntegralSpline{ - spline: spline, center_contribution: Array1::from_vec(center_contribution)}); - } -} - -impl LodeRadialIntegral for LodeRadialIntegralSpline { - #[time_graph::instrument(name = "SplinedRadialIntegral::compute")] - fn compute(&self, x: f64, values: ArrayViewMut2, gradients: Option>) { - self.spline.compute(x, values, gradients); - } - - fn compute_center_contribution(&self) -> Array1 { - return self.center_contribution.clone(); - } -} - -#[cfg(test)] -mod tests { - use approx::assert_relative_eq; - use ndarray::Array; - - use super::*; - use super::super::{LodeRadialIntegralGto, LodeRadialIntegralGtoParameters}; - - #[test] - fn high_accuracy() { - // Check that even with high accuracy and large domain MAX_SPLINE_SIZE is enough - let parameters = LodeRadialIntegralSplineParameters { - max_radial: 15, - max_angular: 10, - k_cutoff: 10.0, - }; - - let gto = LodeRadialIntegralGto::new(LodeRadialIntegralGtoParameters { - max_radial: parameters.max_radial, - max_angular: parameters.max_angular, - cutoff: 5.0, - atomic_gaussian_width: 0.5, - potential_exponent: 1, - }).unwrap(); - - // this test only check that this code runs without crashing - LodeRadialIntegralSpline::with_accuracy(parameters, 5e-10, gto).unwrap(); - } - - #[test] - fn finite_difference() { - let max_radial = 8; - let max_angular = 8; - let parameters = LodeRadialIntegralSplineParameters { - max_radial: max_radial, - max_angular: max_angular, - k_cutoff: 10.0, - }; - - let gto = LodeRadialIntegralGto::new(LodeRadialIntegralGtoParameters { - max_radial: parameters.max_radial, - max_angular: parameters.max_angular, - cutoff: 5.0, - atomic_gaussian_width: 0.5, - potential_exponent: 1, - }).unwrap(); - - // even with very bad accuracy, we want the gradients of the spline to match the - // values produces by the spline, and not necessarily the actual GTO gradients. - let spline = LodeRadialIntegralSpline::with_accuracy(parameters, 1e-2, gto).unwrap(); - - let rij = 3.4; - let delta = 1e-9; - - let shape = (max_angular + 1, max_radial); - let mut values = Array2::from_elem(shape, 0.0); - let mut values_delta = Array2::from_elem(shape, 0.0); - let mut gradients = Array2::from_elem(shape, 0.0); - spline.compute(rij, values.view_mut(), Some(gradients.view_mut())); - spline.compute(rij + delta, values_delta.view_mut(), None); - - let finite_differences = (&values_delta - &values) / delta; - assert_relative_eq!( - finite_differences, gradients, - epsilon=delta, max_relative=5e-6 - ); - } - - #[derive(serde::Serialize)] - /// Helper struct for testing de- and serialization of spline points - struct HelperSplinePoint { - /// Position of the point - pub(crate) position: f64, - /// Values of the function to interpolate at the position - pub(crate) values: Array, - /// Derivatives of the function to interpolate at the position - pub(crate) derivatives: Array, - } - - - /// Check that the `with_accuracy` spline can be directly loaded into - /// `from_tabulated` and that both give the same result. - #[test] - fn accuracy_tabulated() { - let max_radial = 8; - let max_angular = 8; - let parameters = LodeRadialIntegralSplineParameters { - max_radial: max_radial, - max_angular: max_angular, - k_cutoff: 10.0, - }; - - let gto = LodeRadialIntegralGto::new(LodeRadialIntegralGtoParameters { - max_radial: parameters.max_radial, - max_angular: parameters.max_angular, - cutoff: 5.0, - atomic_gaussian_width: 0.5, - potential_exponent: 1, - }).unwrap(); - - let spline_accuracy: LodeRadialIntegralSpline = LodeRadialIntegralSpline::with_accuracy(parameters, 1e-2, gto).unwrap(); - - let mut new_spline_points = Vec::new(); - for spline_point in &spline_accuracy.spline.points { - new_spline_points.push( - HelperSplinePoint{ - position: spline_point.position, - values: spline_point.values.clone(), - derivatives: spline_point.derivatives.clone(), - } - ); - } - - // Serialize and Deserialize spline points - let spline_str = serde_json::to_string(&new_spline_points).unwrap(); - let spline_points: Vec = serde_json::from_str(&spline_str).unwrap(); - - let spline_tabulated = LodeRadialIntegralSpline::from_tabulated(parameters,spline_points, vec![0.0; max_radial]).unwrap(); - - let rij = 3.4; - let shape = (max_angular + 1, max_radial); - - let mut values_accuracy = Array2::from_elem(shape, 0.0); - let mut gradients_accuracy = Array2::from_elem(shape, 0.0); - spline_accuracy.compute(rij, values_accuracy.view_mut(), Some(gradients_accuracy.view_mut())); - - let mut values_tabulated = Array2::from_elem(shape, 0.0); - let mut gradients_tabulated = Array2::from_elem(shape, 0.0); - spline_tabulated.compute(rij, values_tabulated.view_mut(), Some(gradients_tabulated.view_mut())); - - assert_relative_eq!( - values_accuracy, values_tabulated, - epsilon=1e-15, max_relative=1e-16 - ); - - assert_relative_eq!( - gradients_accuracy, gradients_tabulated, - epsilon=1e-15, max_relative=1e-16 - ); - - } -} diff --git a/rascaline/src/calculators/radial_basis/mod.rs b/rascaline/src/calculators/radial_basis/mod.rs deleted file mode 100644 index 85cd94813..000000000 --- a/rascaline/src/calculators/radial_basis/mod.rs +++ /dev/null @@ -1,61 +0,0 @@ -mod gto; -pub use self::gto::GtoRadialBasis; - -mod tabulated; -pub use self::tabulated::SplinePoint; - -#[derive(Debug, Clone)] -#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] -/// Radial basis that can be used in the SOAP or LODE spherical expansion -pub enum RadialBasis { - /// Use a radial basis similar to Gaussian-Type Orbitals. - /// - /// The basis is defined as `R_n(r) ∝ r^n e^{- r^2 / (2 σ_n^2)}`, where `σ_n - /// = cutoff * \sqrt{n} / n_max` - Gto { - /// compute the radial integral using splines. This is much faster than - /// the base GTO implementation. - #[serde(default = "serde_default_splined_radial_integral")] - splined_radial_integral: bool, - /// Accuracy for the spline. The number of control points in the spline - /// is automatically determined to ensure the average absolute error is - /// close to the requested accuracy. - #[serde(default = "serde_default_spline_accuracy")] - spline_accuracy: f64, - }, - /// Compute the radial integral with user-defined splines. - /// - /// The easiest way to create a set of spline points is the - /// `rascaline.generate_splines` Python function. - /// - /// For LODE calculations also the contribution of the central atom have to be - /// provided. The `center_contribution` is defined as `c_n = - /// \sqrt{4π} \int dr r^2 R_n(r) g(r)` where `g(r)` is a radially symmetric density - /// function, `R_n(r)` the radial basis function and `n` the current radial channel. - /// Note that the integration range was deliberately left ambiguous since it depends - /// on the radial basis, i.e. for the GTO basis, `r \in R^+` is used, while `r \in - /// [0, cutoff]` for the monomial basis. - TabulatedRadialIntegral { - points: Vec, - center_contribution: Option>, - } -} - -fn serde_default_splined_radial_integral() -> bool { true } -fn serde_default_spline_accuracy() -> f64 { 1e-8 } - -impl RadialBasis { - /// Use GTO as the radial basis, and do not spline the radial integral - pub fn gto() -> RadialBasis { - return RadialBasis::Gto { - splined_radial_integral: false, spline_accuracy: 0.0 - }; - } - - /// Use GTO as the radial basis, and spline the radial integral - pub fn splined_gto(accuracy: f64) -> RadialBasis { - return RadialBasis::Gto { - splined_radial_integral: true, spline_accuracy: accuracy - }; - } -} diff --git a/rascaline/src/calculators/radial_basis/tabulated.rs b/rascaline/src/calculators/radial_basis/tabulated.rs deleted file mode 100644 index de74af99a..000000000 --- a/rascaline/src/calculators/radial_basis/tabulated.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::collections::BTreeMap; - -use ndarray::Array2; - -use schemars::schema::{SchemaObject, Schema, SingleOrVec, InstanceType, ObjectValidation, Metadata}; - -/// A single point entering a spline used for the tabulated radial integrals. -#[derive(Debug, Clone)] -#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] -pub struct SplinePoint { - /// Position of the point - pub position: f64, - /// Array of values for the tabulated radial integral (the shape should be - /// `(max_angular + 1) x max_radial`) - pub values: JsonArray2, - /// Array of values for the tabulated radial integral (the shape should be - /// `(max_angular + 1) x max_radial`) - pub derivatives: JsonArray2, -} - -/// A simple wrapper around `ndarray::Array2` implementing -/// `schemars::JsonSchema` -#[derive(Debug, Clone)] -#[derive(serde::Serialize, serde::Deserialize)] -pub struct JsonArray2(pub Array2); - -impl std::ops::Deref for JsonArray2 { - type Target = Array2; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl std::ops::DerefMut for JsonArray2 { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - - -impl schemars::JsonSchema for JsonArray2 { - fn schema_name() -> String { - "ndarray::Array".into() - } - - fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> Schema { - let mut v = schemars::schema_for_value!(1).schema; - v.metadata().description = Some("version of the ndarray serialization scheme, should be 1".into()); - - let mut dim = schemars::schema_for!(Vec).schema; - dim.metadata().description = Some("shape of the array".into()); - - let mut data = schemars::schema_for!(Vec).schema; - data.metadata().description = Some("data of the array, in row-major order".into()); - - let properties = [ - ("v".to_string(), Schema::Object(v)), - ("dim".to_string(), Schema::Object(dim)), - ("data".to_string(), Schema::Object(data)), - ]; - - return Schema::Object(SchemaObject { - metadata: Some(Box::new(Metadata { - id: None, - title: Some("ndarray::Array".into()), - description: Some("Serialization format used by ndarray".into()), - default: None, - deprecated: false, - read_only: false, - write_only: false, - examples: vec![], - })), - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))), - format: None, - enum_values: None, - const_value: None, - subschemas: None, - number: None, - string: None, - array: None, - object: Some(Box::new(ObjectValidation { - max_properties: None, - min_properties: None, - required: properties.iter().map(|(p, _)| p.clone()).collect(), - properties: properties.into_iter().collect(), - pattern_properties: BTreeMap::new(), - additional_properties: None, - property_names: None, - })), - reference: None, - extensions: BTreeMap::new(), - }); - } - } diff --git a/rascaline/src/calculators/soap/cutoff.rs b/rascaline/src/calculators/soap/cutoff.rs deleted file mode 100644 index cc123292a..000000000 --- a/rascaline/src/calculators/soap/cutoff.rs +++ /dev/null @@ -1,191 +0,0 @@ -use crate::Error; - -/// Possible values for the smoothing cutoff function -#[derive(Debug, Clone, Copy)] -#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] -pub enum CutoffFunction { - /// Step function, 1 if `r < cutoff` and 0 if `r >= cutoff` - Step{}, - /// Shifted cosine switching function - /// `f(r) = 1/2 * (1 + cos(π (r - cutoff + width) / width ))` - ShiftedCosine { - width: f64, - }, -} - -impl CutoffFunction { - pub fn validate(&self) -> Result<(), Error> { - match self { - CutoffFunction::Step {} => {}, - CutoffFunction::ShiftedCosine { width } => { - if *width <= 0.0 { - return Err(Error::InvalidParameter(format!( - "expected positive width for shifted cosine cutoff function, got {}", - width - ))); - } - } - } - return Ok(()); - } - - /// Evaluate the cutoff function at the distance `r` for the given `cutoff` - pub fn compute(&self, r: f64, cutoff: f64) -> f64 { - match self { - CutoffFunction::Step{} => { - if r >= cutoff { 0.0 } else { 1.0 } - }, - CutoffFunction::ShiftedCosine { width } => { - if r <= (cutoff - width) { - 1.0 - } else if r >= cutoff { - 0.0 - } else { - let s = std::f64::consts::PI * (r - cutoff + width) / width; - 0.5 * (1. + f64::cos(s)) - } - } - } - } - - /// Evaluate the derivative of the cutoff function at the distance `r` for the - /// given `cutoff` - pub fn derivative(&self, r: f64, cutoff: f64) -> f64 { - match self { - CutoffFunction::Step{} => 0.0, - CutoffFunction::ShiftedCosine { width } => { - if r <= (cutoff - width) || r >= cutoff { - 0.0 - } else { - let s = std::f64::consts::PI * (r - cutoff + width) / width; - return -0.5 * std::f64::consts::PI * f64::sin(s) / width; - } - } - } - } -} - -/// Implemented options for radial scaling of the atomic density around an atom -#[derive(Debug, Clone, Copy)] -#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema)] -pub enum RadialScaling { - /// No radial scaling - None {}, - /// Use a long-range algebraic decay and smooth behavior at $r \rightarrow 0$ - /// as introduced in : - /// `f(r) = rate / (rate + (r / scale) ^ exponent)` - Willatt2018 { - scale: f64, - rate: f64, - exponent: f64, - }, -} - -impl Default for RadialScaling { - fn default() -> RadialScaling { - RadialScaling::None {} - } -} - -impl RadialScaling { - pub fn validate(&self) -> Result<(), Error> { - match self { - RadialScaling::None {} => {}, - RadialScaling::Willatt2018 { scale, rate, exponent } => { - if *scale <= 0.0 { - return Err(Error::InvalidParameter(format!( - "expected positive scale for Willatt2018 radial scaling function, got {}", - scale - ))); - } - - if *rate <= 0.0 { - return Err(Error::InvalidParameter(format!( - "expected positive rate for Willatt2018 radial scaling function, got {}", - rate - ))); - } - - if *exponent <= 0.0 { - return Err(Error::InvalidParameter(format!( - "expected positive exponent for Willatt2018 radial scaling function, got {}", - exponent - ))); - } - } - } - return Ok(()); - } - - /// Evaluate the radial scaling function at the distance `r` - pub fn compute(&self, r: f64) -> f64 { - match self { - RadialScaling::None {} => 1.0, - RadialScaling::Willatt2018 { rate, scale, exponent } => { - rate / (rate + (r / scale).powf(*exponent)) - } - } - } - - /// Evaluate the derivative of the radial scaling function at the distance `r` - pub fn derivative(&self, r: f64) -> f64 { - match self { - RadialScaling::None {} => 0.0, - RadialScaling::Willatt2018 { scale, rate, exponent } => { - let rs = r / scale; - let rs_m1 = rs.powf(exponent - 1.0); - let rs_m = rs * rs_m1; - let factor = - rate * exponent / scale; - - factor * rs_m1 / ((rate + rs_m) * (rate + rs_m)) - } - } - } -} - - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn step() { - let function = CutoffFunction::Step{}; - let cutoff = 4.0; - - assert_eq!(function.compute(2.0, cutoff), 1.0); - assert_eq!(function.compute(5.0, cutoff), 0.0); - } - - #[test] - fn step_gradient() { - let function = CutoffFunction::Step{}; - let cutoff = 4.0; - - assert_eq!(function.derivative(2.0, cutoff), 0.0); - assert_eq!(function.derivative(5.0, cutoff), 0.0); - } - - #[test] - fn shifted_cosine() { - let function = CutoffFunction::ShiftedCosine { width: 0.5 }; - let cutoff = 4.0; - - assert_eq!(function.compute(2.0, cutoff), 1.0); - assert_eq!(function.compute(3.5, cutoff), 1.0); - assert_eq!(function.compute(3.8, cutoff), 0.34549150281252683); - assert_eq!(function.compute(4.0, cutoff), 0.0); - assert_eq!(function.compute(5.0, cutoff), 0.0); - } - - #[test] - fn shifted_cosine_gradient() { - let function = CutoffFunction::ShiftedCosine { width: 0.5 }; - let cutoff = 4.0; - - assert_eq!(function.derivative(2.0, cutoff), 0.0); - assert_eq!(function.derivative(3.5, cutoff), 0.0); - assert_eq!(function.derivative(3.8, cutoff), -2.987832164741557); - assert_eq!(function.derivative(4.0, cutoff), 0.0); - assert_eq!(function.derivative(5.0, cutoff), 0.0); - } -} diff --git a/rascaline/src/calculators/soap/radial_integral/gto.rs b/rascaline/src/calculators/soap/radial_integral/gto.rs deleted file mode 100644 index ad8e188bb..000000000 --- a/rascaline/src/calculators/soap/radial_integral/gto.rs +++ /dev/null @@ -1,353 +0,0 @@ -use std::f64; - -use ndarray::{Array2, ArrayViewMut2}; - -use crate::calculators::radial_basis::GtoRadialBasis; -use crate::math::{gamma, DoubleRegularized1F1}; -use crate::Error; - -use super::SoapRadialIntegral; - -/// Parameters controlling the SOAP radial integral with GTO radial basis -#[derive(Debug, Clone, Copy)] -pub struct SoapRadialIntegralGtoParameters { - /// Number of radial components - pub max_radial: usize, - /// Number of angular components - pub max_angular: usize, - /// atomic density gaussian width - pub atomic_gaussian_width: f64, - /// cutoff radius - pub cutoff: f64, -} - -impl SoapRadialIntegralGtoParameters { - pub(crate) fn validate(&self) -> Result<(), Error> { - if self.max_radial == 0 { - return Err(Error::InvalidParameter( - "max_radial must be at least 1 for GTO radial integral".into() - )); - } - - if self.cutoff <= 1e-16 || !self.cutoff.is_finite() { - return Err(Error::InvalidParameter( - "cutoff must be a positive number for GTO radial integral".into() - )); - } - - if self.atomic_gaussian_width <= 1e-16 || !self.atomic_gaussian_width.is_finite() { - return Err(Error::InvalidParameter( - "atomic_gaussian_width must be a positive number for GTO radial integral".into() - )); - } - - Ok(()) - } -} - -/// Implementation of the radial integral for GTO radial basis and gaussian -/// atomic density. -#[derive(Debug, Clone)] -pub struct SoapRadialIntegralGto { - parameters: SoapRadialIntegralGtoParameters, - /// `σ^2`, with σ the atomic density gaussian width - atomic_gaussian_width_2: f64, - /// `1/2σ^2`, with σ the atomic density gaussian width - atomic_gaussian_constant: f64, - /// `1/2σ_n^2`, with `σ_n` the GTO gaussian width, i.e. `cutoff * max(√n, 1) - /// / n_max` - gto_gaussian_constants: Vec, - /// `n_max * n_max` matrix to orthonormalize the GTO - gto_orthonormalization: Array2, - /// Implementation of `Gamma(a) / Gamma(b) 1F1(a, b, z)` - double_regularized_1f1: DoubleRegularized1F1, -} - - -impl SoapRadialIntegralGto { - pub fn new(parameters: SoapRadialIntegralGtoParameters) -> Result { - parameters.validate()?; - - let basis = GtoRadialBasis { - max_radial: parameters.max_radial, - cutoff: parameters.cutoff, - }; - let gto_gaussian_widths = basis.gaussian_widths(); - let gto_orthonormalization = basis.orthonormalization_matrix(); - - let gto_gaussian_constants = gto_gaussian_widths.iter() - .map(|&sigma| 1.0 / (2.0 * sigma * sigma)) - .collect::>(); - - let atomic_gaussian_width_2 = parameters.atomic_gaussian_width * parameters.atomic_gaussian_width; - let atomic_gaussian_constant = 1.0 / (2.0 * atomic_gaussian_width_2); - - return Ok(SoapRadialIntegralGto { - parameters: parameters, - double_regularized_1f1: DoubleRegularized1F1 { - max_angular: parameters.max_angular, - }, - atomic_gaussian_width_2: atomic_gaussian_width_2, - atomic_gaussian_constant: atomic_gaussian_constant, - gto_gaussian_constants: gto_gaussian_constants, - gto_orthonormalization: gto_orthonormalization.t().to_owned(), - }) - } -} - -impl SoapRadialIntegral for SoapRadialIntegralGto { - #[time_graph::instrument(name = "GtoRadialIntegral::compute")] - fn compute( - &self, - distance: f64, - mut values: ArrayViewMut2, - mut gradients: Option> - ) { - let expected_shape = [self.parameters.max_angular + 1, self.parameters.max_radial]; - assert_eq!( - values.shape(), expected_shape, - "wrong size for values array, expected [{}, {}] but got [{}, {}]", - expected_shape[0], expected_shape[1], values.shape()[0], values.shape()[1] - ); - - if let Some(ref gradients) = gradients { - assert_eq!( - gradients.shape(), expected_shape, - "wrong size for gradients array, expected [{}, {}] but got [{}, {}]", - expected_shape[0], expected_shape[1], gradients.shape()[0], gradients.shape()[1] - ); - } - - // Define global factor of radial integral arising from three parts: - // - a global 4 pi factor coming from integration of the angular part of - // the radial integral (see the docs for `SoapRadialIntegral`) - // - a global factor of sqrt(pi)/4 from the calculation of the integral - // of GTO basis functions and gaussian density - // - the normalization constant of the atomic Gaussian density. We use a - // factor of 1/(pi*sigma^2)^0.75 which leads to Gaussian densities - // that are normalized in the L2-sense, i.e. integral_{R^3} |g(r)|^2 - // d^3r = 1. - // - // These three factors simplify to (pi/sigma^2)^3/4 - let global_factor = (std::f64::consts::PI / self.atomic_gaussian_width_2).powf(0.75); - - let c = self.atomic_gaussian_constant; - let c_rij = c * distance; - let exp_c_rij = f64::exp(-distance * c_rij); - - for n in 0..self.parameters.max_radial { - let gto_constant = self.gto_gaussian_constants[n]; - // `global_factor * exp(-c rij^2) * (c * rij)^l` - let mut factor = global_factor * exp_c_rij; - - let z = c_rij * c_rij / (self.atomic_gaussian_constant + gto_constant); - // Calculate Gamma(a) / Gamma(b) 1F1(a, b, z) - self.double_regularized_1f1.compute( - z, n, - values.index_axis_mut(ndarray::Axis(1), n), - gradients.as_mut().map(|g| g.index_axis_mut(ndarray::Axis(1), n)) - ); - - for l in 0..(self.parameters.max_angular + 1) { - let n_l_3_over_2 = 0.5 * (n + l) as f64 + 1.5; - let c_dn = (c + gto_constant).powf(-n_l_3_over_2); - - if !values[[l, n]].is_finite() { - panic!( - "Failed to compute radial integral with GTO basis. \ - Try increasing decreasing the `cutoff`, or increasing `atomic_gaussian_width`." - ); - } - - values[[l, n]] *= c_dn * factor; - if let Some(ref mut gradients) = gradients { - gradients[[l, n]] *= c_dn * factor * 2.0 * z / distance; - gradients[[l, n]] += values[[l, n]] * (l as f64 / distance - 2.0 * c_rij); - } - - factor *= c_rij; - } - } - - // for r = 0, the formula used in the calculations above yield NaN, - // which in turns breaks the SplinedGto radial integral. From the - // analytical formula, the gradient is 0 everywhere expect for l=1 - if distance == 0.0 { - if let Some(ref mut gradients) = gradients { - gradients.fill(0.0); - - if self.parameters.max_angular >= 1 { - let l = 1; - for n in 0..self.parameters.max_radial { - let gto_constant = self.gto_gaussian_constants[n]; - let a = 0.5 * (n + l) as f64 + 1.5; - let b = 2.5; - let c_dn = (c + gto_constant).powf(-a); - let factor = global_factor * c * c_dn; - - gradients[[l, n]] = gamma(a) / gamma(b) * factor; - } - } - } - } - - values.assign(&values.dot(&self.gto_orthonormalization)); - if let Some(ref mut gradients) = gradients { - gradients.assign(&gradients.dot(&self.gto_orthonormalization)); - } - } -} - -#[cfg(test)] -mod tests { - use approx::assert_relative_eq; - - use super::super::{SoapRadialIntegralGto, SoapRadialIntegralGtoParameters, SoapRadialIntegral}; - use ndarray::Array2; - - #[test] - #[should_panic = "max_radial must be at least 1"] - fn invalid_max_radial() { - SoapRadialIntegralGto::new(SoapRadialIntegralGtoParameters { - max_radial: 0, - max_angular: 4, - cutoff: 3.0, - atomic_gaussian_width: 0.5 - }).unwrap(); - } - - #[test] - #[should_panic = "cutoff must be a positive number"] - fn negative_cutoff() { - SoapRadialIntegralGto::new(SoapRadialIntegralGtoParameters { - max_radial: 10, - max_angular: 4, - cutoff: -3.0, - atomic_gaussian_width: 0.5 - }).unwrap(); - } - - #[test] - #[should_panic = "cutoff must be a positive number"] - fn infinite_cutoff() { - SoapRadialIntegralGto::new(SoapRadialIntegralGtoParameters { - max_radial: 10, - max_angular: 4, - cutoff: f64::INFINITY, - atomic_gaussian_width: 0.5 - }).unwrap(); - } - - #[test] - #[should_panic = "atomic_gaussian_width must be a positive number"] - fn negative_atomic_gaussian_width() { - SoapRadialIntegralGto::new(SoapRadialIntegralGtoParameters { - max_radial: 10, - max_angular: 4, - cutoff: 3.0, - atomic_gaussian_width: -0.5 - }).unwrap(); - } - - #[test] - #[should_panic = "atomic_gaussian_width must be a positive number"] - fn infinite_atomic_gaussian_width() { - SoapRadialIntegralGto::new(SoapRadialIntegralGtoParameters { - max_radial: 10, - max_angular: 4, - cutoff: 3.0, - atomic_gaussian_width: f64::INFINITY, - }).unwrap(); - } - - #[test] - #[should_panic = "radial overlap matrix is singular, try with a lower max_radial (current value is 30)"] - fn ill_conditioned_orthonormalization() { - SoapRadialIntegralGto::new(SoapRadialIntegralGtoParameters { - max_radial: 30, - max_angular: 3, - cutoff: 5.0, - atomic_gaussian_width: 0.5, - }).unwrap(); - } - - #[test] - #[should_panic = "wrong size for values array, expected [4, 2] but got [3, 2]"] - fn values_array_size() { - let gto = SoapRadialIntegralGto::new(SoapRadialIntegralGtoParameters { - max_radial: 2, - max_angular: 3, - cutoff: 5.0, - atomic_gaussian_width: 0.5, - }).unwrap(); - let mut values = Array2::from_elem((3, 2), 0.0); - - gto.compute(1.0, values.view_mut(), None); - } - - #[test] - #[should_panic = "wrong size for gradients array, expected [4, 2] but got [3, 2]"] - fn gradient_array_size() { - let gto = SoapRadialIntegralGto::new(SoapRadialIntegralGtoParameters { - max_radial: 2, - max_angular: 3, - cutoff: 5.0, - atomic_gaussian_width: 0.5, - }).unwrap(); - let mut values = Array2::from_elem((4, 2), 0.0); - let mut gradients = Array2::from_elem((3, 2), 0.0); - - gto.compute(1.0, values.view_mut(), Some(gradients.view_mut())); - } - - #[test] - fn gradients_near_zero() { - let max_radial = 8; - let max_angular = 8; - let gto = SoapRadialIntegralGto::new(SoapRadialIntegralGtoParameters { - max_radial: max_radial, - max_angular: max_angular, - cutoff: 5.0, - atomic_gaussian_width: 0.5, - }).unwrap(); - - let shape = (max_angular + 1, max_radial); - let mut values = Array2::from_elem(shape, 0.0); - let mut gradients = Array2::from_elem(shape, 0.0); - let mut gradients_plus = Array2::from_elem(shape, 0.0); - gto.compute(0.0, values.view_mut(), Some(gradients.view_mut())); - gto.compute(1e-12, values.view_mut(), Some(gradients_plus.view_mut())); - - assert_relative_eq!( - gradients, gradients_plus, epsilon=1e-11, max_relative=1e-6, - ); - } - - #[test] - fn finite_differences() { - let max_radial = 8; - let max_angular = 8; - let gto = SoapRadialIntegralGto::new(SoapRadialIntegralGtoParameters { - max_radial: max_radial, - max_angular: max_angular, - cutoff: 5.0, - atomic_gaussian_width: 0.5, - }).unwrap(); - - let rij = 3.4; - let delta = 1e-9; - - let shape = (max_angular + 1, max_radial); - let mut values = Array2::from_elem(shape, 0.0); - let mut values_delta = Array2::from_elem(shape, 0.0); - let mut gradients = Array2::from_elem(shape, 0.0); - gto.compute(rij, values.view_mut(), Some(gradients.view_mut())); - gto.compute(rij + delta, values_delta.view_mut(), None); - - let finite_differences = (&values_delta - &values) / delta; - - assert_relative_eq!( - finite_differences, gradients, max_relative=1e-4 - ); - } -} diff --git a/rascaline/src/calculators/soap/radial_integral/mod.rs b/rascaline/src/calculators/soap/radial_integral/mod.rs deleted file mode 100644 index afdbe31c7..000000000 --- a/rascaline/src/calculators/soap/radial_integral/mod.rs +++ /dev/null @@ -1,153 +0,0 @@ -use ndarray::{ArrayViewMut2, Array2}; - -use log::warn; - -use crate::Error; -use crate::calculators::radial_basis::RadialBasis; - -/// A `SoapRadialIntegral` computes the SOAP radial integral on a given radial -/// basis. -/// -/// See equations 5 to 8 of [this paper](https://doi.org/10.1063/5.0044689) for -/// mor information on the radial integral. -/// -/// `std::panic::RefUnwindSafe` is a required super-trait to enable passing -/// radial integrals across the C API. `Send` is a required super-trait to -/// enable passing radial integrals between threads. -#[allow(clippy::doc_markdown)] -pub trait SoapRadialIntegral: std::panic::RefUnwindSafe + Send { - /// Compute the radial integral for a single `distance` between two atoms - /// and store the resulting data in the `(max_angular + 1) x max_radial` - /// array `values`. If `gradients` is `Some`, also compute and store - /// gradients there. - /// - /// The radial integral $I_{nl}$ is defined as "the non-spherical harmonics - /// part of the spherical expansion". Depending on the atomic density, - /// different expressions can be used. - /// - /// For a delta density, the radial integral is simply the radial basis - /// function $R_{nl}$ evaluated at the pair distance: - /// - /// $$ I_{nl}(r_{ij}) = R_{nl}(r_{ij}) $$ - /// - /// For a Gaussian atomic density with a width of $\sigma$, the radial - /// integral reduces to: - /// - /// $$ - /// I_{nl}(r_{ij}) = \frac{4\pi}{(\pi \sigma^2)^{3/4}} e^{-\frac{r_{ij}^2}{2\sigma^2}} - /// \int_0^\infty \mathrm{d}r r^2 R_{nl}(r) e^{-\frac{r^2}{2\sigma^2}} i_l\left(\frac{rr_{ij}}{\sigma^2}\right) - /// $$ - /// - /// where $i_l$ is the modified spherical Bessel function of the first kind - /// of order $l$. - /// - /// Finally, for an arbitrary spherically symmetric atomic density `g(r)`, - /// the radial integral is - /// - /// $$ - /// I_{nl}(r_{ij}) = 2\pi \int_0^\infty \mathrm{d}r r^2 R_{nl}(r) - /// \int_{-1}^1 \mathrm{d}u P_l(u) g(\sqrt{r^2+r_{ij}^2-2rr_{ij}u}) - /// $$ - /// - /// where $P_l$ is the l-th Legendre polynomial. - fn compute(&self, rij: f64, values: ArrayViewMut2, gradients: Option>); -} - -mod gto; -pub use self::gto::{SoapRadialIntegralGto, SoapRadialIntegralGtoParameters}; - -mod spline; -pub use self::spline::{SoapRadialIntegralSpline, SoapRadialIntegralSplineParameters}; - -/// Parameters controlling the radial integral for SOAP -#[derive(Debug, Clone, Copy)] -pub struct SoapRadialIntegralParameters { - pub max_radial: usize, - pub max_angular: usize, - pub atomic_gaussian_width: f64, - pub cutoff: f64, -} - -/// Store together a Radial integral implementation and cached allocation for -/// values/gradients. -pub struct SoapRadialIntegralCache { - /// Implementation of the radial integral - code: Box, - /// Cache for the radial integral values - pub(crate) values: Array2, - /// Cache for the radial integral gradient - pub(crate) gradients: Array2, -} - -impl SoapRadialIntegralCache { - /// Create a new `RadialIntegralCache` for the given radial basis & parameters - pub fn new(radial_basis: RadialBasis, parameters: SoapRadialIntegralParameters) -> Result { - let code = match radial_basis { - RadialBasis::Gto {splined_radial_integral, spline_accuracy} => { - let parameters = SoapRadialIntegralGtoParameters { - max_radial: parameters.max_radial, - max_angular: parameters.max_angular, - atomic_gaussian_width: parameters.atomic_gaussian_width, - cutoff: parameters.cutoff, - }; - let gto = SoapRadialIntegralGto::new(parameters)?; - - if splined_radial_integral { - let parameters = SoapRadialIntegralSplineParameters { - max_radial: parameters.max_radial, - max_angular: parameters.max_angular, - cutoff: parameters.cutoff, - }; - - Box::new(SoapRadialIntegralSpline::with_accuracy( - parameters, spline_accuracy, gto - )?) - } else { - Box::new(gto) as Box - } - } - RadialBasis::TabulatedRadialIntegral {points, center_contribution} => { - let parameters = SoapRadialIntegralSplineParameters { - max_radial: parameters.max_radial, - max_angular: parameters.max_angular, - cutoff: parameters.cutoff, - }; - - if center_contribution.is_some() { - warn!( - "`center_contribution` is not used in SOAP radial \ - integral and will be ignored" - ); - } - - Box::new(SoapRadialIntegralSpline::from_tabulated( - parameters, points - )?) - } - }; - - let shape = (parameters.max_angular + 1, parameters.max_radial); - let values = Array2::from_elem(shape, 0.0); - let gradients = Array2::from_elem(shape, 0.0); - - return Ok(SoapRadialIntegralCache { code, values, gradients }); - } - - /// Run the calculation, the results are stored inside `self.values` and - /// `self.gradients` - pub fn compute(&mut self, distance: f64, gradients: bool) { - if gradients { - self.code.compute( - distance, - self.values.view_mut(), - Some(self.gradients.view_mut()), - ); - } else { - self.code.compute( - distance, - self.values.view_mut(), - None, - ); - } - } -} diff --git a/rascaline/src/calculators/soap/radial_integral/spline.rs b/rascaline/src/calculators/soap/radial_integral/spline.rs deleted file mode 100644 index 372ae16b5..000000000 --- a/rascaline/src/calculators/soap/radial_integral/spline.rs +++ /dev/null @@ -1,240 +0,0 @@ -use ndarray::{Array2, ArrayViewMut2}; - -use super::SoapRadialIntegral; -use crate::math::{HermitCubicSpline, SplineParameters, HermitSplinePoint}; -use crate::calculators::radial_basis::SplinePoint; -use crate::Error; - -/// `SoapRadialIntegralSpline` allows to evaluate another radial integral -/// implementation using [cubic Hermit spline][splines-wiki]. -/// -/// This can be much faster than using the actual radial integral -/// implementation. -/// -/// [splines-wiki]: https://en.wikipedia.org/wiki/Cubic_Hermite_spline -pub struct SoapRadialIntegralSpline { - spline: HermitCubicSpline, -} - -/// Parameters for computing the radial integral using Hermit cubic splines -#[derive(Debug, Clone, Copy)] -pub struct SoapRadialIntegralSplineParameters { - /// Number of radial components - pub max_radial: usize, - /// Number of angular components - pub max_angular: usize, - /// cutoff radius, this is also the maximal value that can be interpolated - pub cutoff: f64, -} - -impl SoapRadialIntegralSpline { - /// Create a new `SoapRadialIntegralSpline` taking values from the given - /// `radial_integral`. Points are added to the spline until the requested - /// accuracy is reached. We consider that the accuracy is reached when - /// either the mean absolute error or the mean relative error gets below the - /// `accuracy` threshold. - #[time_graph::instrument(name = "SoapRadialIntegralSpline::with_accuracy")] - pub fn with_accuracy( - parameters: SoapRadialIntegralSplineParameters, - accuracy: f64, - radial_integral: impl SoapRadialIntegral - ) -> Result { - let shape_tuple = (parameters.max_angular + 1, parameters.max_radial); - - let parameters = SplineParameters { - start: 0.0, - stop: parameters.cutoff, - shape: vec![parameters.max_angular + 1, parameters.max_radial], - }; - - let spline = HermitCubicSpline::with_accuracy( - accuracy, - parameters, - |x| { - let mut values = Array2::from_elem(shape_tuple, 0.0); - let mut derivatives = Array2::from_elem(shape_tuple, 0.0); - radial_integral.compute(x, values.view_mut(), Some(derivatives.view_mut())); - (values, derivatives) - }, - )?; - - return Ok(SoapRadialIntegralSpline { spline }); - } - - /// Create a new `SoapRadialIntegralSpline` with user-defined spline points. - pub fn from_tabulated( - parameters: SoapRadialIntegralSplineParameters, - spline_points: Vec - ) -> Result { - - let spline_parameters = SplineParameters { - start: 0.0, - stop: parameters.cutoff, - shape: vec![parameters.max_angular + 1, parameters.max_radial], - }; - - let mut new_spline_points = Vec::new(); - for spline_point in spline_points { - new_spline_points.push( - HermitSplinePoint{ - position: spline_point.position, - values: spline_point.values.0.clone(), - derivatives: spline_point.derivatives.0.clone(), - } - ); - } - - let spline = HermitCubicSpline::new(spline_parameters, new_spline_points); - return Ok(SoapRadialIntegralSpline{spline}); - } -} - -impl SoapRadialIntegral for SoapRadialIntegralSpline { - #[time_graph::instrument(name = "SplinedRadialIntegral::compute")] - fn compute(&self, x: f64, values: ArrayViewMut2, gradients: Option>) { - self.spline.compute(x, values, gradients); - } -} - -#[cfg(test)] -mod tests { - use approx::assert_relative_eq; - use ndarray::Array; - - use super::*; - use super::super::{SoapRadialIntegralGto, SoapRadialIntegralGtoParameters}; - - #[test] - fn high_accuracy() { - // Check that even with high accuracy and large domain MAX_SPLINE_SIZE - // is enough - let parameters = SoapRadialIntegralSplineParameters { - max_radial: 15, - max_angular: 10, - cutoff: 12.0, - }; - - let gto = SoapRadialIntegralGto::new(SoapRadialIntegralGtoParameters { - max_radial: parameters.max_radial, - max_angular: parameters.max_angular, - cutoff: parameters.cutoff, - atomic_gaussian_width: 0.5, - }).unwrap(); - - // this test only check that this code runs without crashing - SoapRadialIntegralSpline::with_accuracy(parameters, 1e-10, gto).unwrap(); - } - - #[test] - fn finite_difference() { - let max_radial = 8; - let max_angular = 8; - let parameters = SoapRadialIntegralSplineParameters { - max_radial: max_radial, - max_angular: max_angular, - cutoff: 5.0, - }; - - let gto = SoapRadialIntegralGto::new(SoapRadialIntegralGtoParameters { - max_radial: parameters.max_radial, - max_angular: parameters.max_angular, - cutoff: parameters.cutoff, - atomic_gaussian_width: 0.5, - }).unwrap(); - - // even with very bad accuracy, we want the gradients of the spline to - // match the values produces by the spline, and not necessarily the - // actual GTO gradients. - let spline = SoapRadialIntegralSpline::with_accuracy(parameters, 1e-2, gto).unwrap(); - - let rij = 3.4; - let delta = 1e-9; - - let shape = (max_angular + 1, max_radial); - let mut values = Array2::from_elem(shape, 0.0); - let mut values_delta = Array2::from_elem(shape, 0.0); - let mut gradients = Array2::from_elem(shape, 0.0); - spline.compute(rij, values.view_mut(), Some(gradients.view_mut())); - spline.compute(rij + delta, values_delta.view_mut(), None); - - let finite_differences = (&values_delta - &values) / delta; - assert_relative_eq!( - finite_differences, gradients, - epsilon=delta, max_relative=1e-6 - ); - } - - - #[derive(serde::Serialize)] - /// Helper struct for testing de- and serialization of spline points - struct HelperSplinePoint { - /// Position of the point - pub(crate) position: f64, - /// Values of the function to interpolate at the position - pub(crate) values: Array, - /// Derivatives of the function to interpolate at the position - pub(crate) derivatives: Array, - } - - - /// Check that the `with_accuracy` spline can be directly loaded into - /// `from_tabulated` and that both give the same result. - #[test] - fn accuracy_tabulated() { - let max_radial = 8; - let max_angular = 8; - let parameters = SoapRadialIntegralSplineParameters { - max_radial: max_radial, - max_angular: max_angular, - cutoff: 5.0, - }; - - let gto = SoapRadialIntegralGto::new(SoapRadialIntegralGtoParameters { - max_radial: parameters.max_radial, - max_angular: parameters.max_angular, - cutoff: parameters.cutoff, - atomic_gaussian_width: 0.5, - }).unwrap(); - - let spline_accuracy: SoapRadialIntegralSpline = SoapRadialIntegralSpline::with_accuracy(parameters, 1e-2, gto).unwrap(); - - let mut new_spline_points = Vec::new(); - for spline_point in &spline_accuracy.spline.points { - new_spline_points.push( - HelperSplinePoint{ - position: spline_point.position, - values: spline_point.values.clone(), - derivatives: spline_point.derivatives.clone(), - } - ); - } - - // Serialize and Deserialize spline points - let spline_str = serde_json::to_string(&new_spline_points).unwrap(); - let spline_points: Vec = serde_json::from_str(&spline_str).unwrap(); - - let spline_tabulated = SoapRadialIntegralSpline::from_tabulated(parameters, spline_points).unwrap(); - - let rij = 3.4; - let shape = (max_angular + 1, max_radial); - - let mut values_accuracy = Array2::from_elem(shape, 0.0); - let mut gradients_accuracy = Array2::from_elem(shape, 0.0); - spline_accuracy.compute(rij, values_accuracy.view_mut(), Some(gradients_accuracy.view_mut())); - - let mut values_tabulated = Array2::from_elem(shape, 0.0); - let mut gradients_tabulated = Array2::from_elem(shape, 0.0); - spline_tabulated.compute(rij, values_tabulated.view_mut(), Some(gradients_tabulated.view_mut())); - - assert_relative_eq!( - values_accuracy, values_tabulated, - epsilon=1e-15, max_relative=1e-16 - ); - - assert_relative_eq!( - gradients_accuracy, gradients_tabulated, - epsilon=1e-15, max_relative=1e-16 - ); - - } -} diff --git a/rascaline/src/math/double_regularized_1f1.rs b/rascaline/src/math/double_regularized_1f1.rs deleted file mode 100644 index 64953667a..000000000 --- a/rascaline/src/math/double_regularized_1f1.rs +++ /dev/null @@ -1,199 +0,0 @@ -use ndarray::ArrayViewMut1; - -use super::{hyp1f1, gamma}; - -/// Compute `G(a, b, z) = Gamma(a) / Gamma(b) 1F1(a, b, z)` for -/// `a = 1/2 (n + l + 3)` and `b = l + 3/2` using recursion relations between -/// the value/gradients of this function for `l` and `l + 2`. -/// -/// This is similar (but not the exact same) to the G function defined in -/// appendix A in . -/// -/// The function is called "double regularized 1F1" by reference to the -/// "regularized 1F1" function (i.e. `1F1(a, b, z) / Gamma(b)`) -#[derive(Clone, Copy, Debug)] -pub struct DoubleRegularized1F1 { - pub max_angular: usize, -} - -impl DoubleRegularized1F1 { - /// Compute `Gamma(a) / Gamma(b) 1F1(a, b, z)` for all l and a given `n` - pub fn compute(self, z: f64, n: usize, values: ArrayViewMut1, gradients: Option>) { - debug_assert_eq!(values.len(), self.max_angular + 1); - if let Some(ref gradients) = gradients { - debug_assert_eq!(gradients.len(), self.max_angular + 1); - } - - if self.max_angular < 3 { - self.direct(z, n, values, gradients); - } else { - self.recursive(z, n, values, gradients); - } - } - - /// Direct evaluation of the G function - fn direct(self, z: f64, n: usize, mut values: ArrayViewMut1, mut gradients: Option>) { - for l in 0..=self.max_angular { - let (a, b) = get_ab(l, n); - let ratio = gamma(a) / gamma(b); - - values[l] = ratio * hyp1f1(a, b, z); - if let Some(ref mut gradients) = gradients { - gradients[l] = ratio * hyp1f1_derivative(a, b, z); - } - } - } - - /// Recursive evaluation of the G function - /// - /// The recursion relations are derived from "Abramowitz and Stegun", - /// rewriting equations 13.4.8, 13.4.10, 13.4.12, and 13.4.14 for G instead - /// of M/1F1; and realizing that if G(a, b) is G(l); then G(a + 1, b + 2) is - /// G(l+2). - /// - /// We end up with the following recurrence relations: - /// - /// - G'(l) = (b + 1) G(l + 2) + z G'(l + 2) - /// - G(l) = b/a G'(l) + z (a - b)/a G(l + 2) - /// - /// Since the relation have a step of 2 for l, we initialize the recurrence - /// by evaluating G for `l_max` and `l_max - 1`, and then propagate the - /// values downward. - #[allow(clippy::many_single_char_names)] - fn recursive(self, z: f64, n: usize, mut values: ArrayViewMut1, mut gradients: Option>) { - debug_assert!(self.max_angular >= 3); - - // initialize the values at l_max - let mut l = self.max_angular; - let (a, b) = get_ab(l, n); - let ratio = gamma(a) / gamma(b); - - let mut g_l2 = ratio * hyp1f1(a, b, z); - let mut grad_g_l2 = ratio * hyp1f1_derivative(a, b, z); - values[l] = g_l2; - if let Some(ref mut gradients) = gradients { - gradients[l] = grad_g_l2; - } - - // initialize the values at (l_max - 1) - l -= 1; - let (a, b) = get_ab(l, n); - let ratio = gamma(a) / gamma(b); - - let mut g_l1 = ratio * hyp1f1(a, b, z); - let mut grad_g_l1 = ratio * hyp1f1_derivative(a, b, z); - values[l] = g_l1; - if let Some(ref mut gradients) = gradients { - gradients[l] = grad_g_l1; - } - - let g_recursive_step = |a, b, g_l2, grad_g_l2| { - let grad_g_l = (b + 1.0) * g_l2 + z * grad_g_l2; - let g_l = (a - b) / a * z * g_l2 + b / a * grad_g_l; - return (g_l, grad_g_l); - }; - - // do the recursion for all other l values - l = self.max_angular; - while l > 2 { - l -= 2; - let (a, b) = get_ab(l, n); - let (new_value, new_grad) = g_recursive_step(a, b, g_l2, grad_g_l2); - g_l2 = new_value; - grad_g_l2 = new_grad; - - values[l] = g_l2; - if let Some(ref mut gradients) = gradients { - gradients[l] = grad_g_l2; - } - - let (a, b) = get_ab(l - 1, n); - let (new_value, new_grad) = g_recursive_step(a, b, g_l1, grad_g_l1); - g_l1 = new_value; - grad_g_l1 = new_grad; - - values[l - 1] = g_l1; - if let Some(ref mut gradients) = gradients { - gradients[l - 1] = grad_g_l1; - } - } - - // makes sure l == 0 is taken care of - if self.max_angular % 2 == 0 { - let (a, b) = get_ab(0, n); - let (new_value, new_grad) = g_recursive_step(a, b, g_l2, grad_g_l2); - g_l2 = new_value; - grad_g_l2 = new_grad; - - values[0] = g_l2; - if let Some(ref mut gradients) = gradients { - gradients[0] = grad_g_l2; - } - } - } -} - -#[inline] -fn get_ab(l: usize, n: usize) -> (f64, f64) { - return (0.5 * (n + l + 3) as f64, l as f64 + 1.5); -} - - -fn hyp1f1_derivative(a: f64, b: f64, x: f64) -> f64 { - a / b * hyp1f1(a + 1.0, b + 1.0, x) -} - - -#[cfg(test)] -mod tests { - use super::*; - use ndarray::Array1; - use approx::assert_relative_eq; - - #[test] - fn direct_recursive_agree() { - for &n in &[1, 5, 10, 18] { - for &max_angular in &[8, 15] { //&[3, 8, 15] { - - let dr_1f1 = DoubleRegularized1F1{ max_angular }; - let mut direct = Array1::from_elem(max_angular + 1, 0.0); - let mut recursive = Array1::from_elem(max_angular + 1, 0.0); - - for &z in &[-200.0, -10.0, -1.1, -1e-2, 0.2, 1.5, 10.0, 40.0, 523.0] { - dr_1f1.direct(z, n, direct.view_mut(), None); - dr_1f1.recursive(z, n, recursive.view_mut(), None); - - assert_relative_eq!( - direct, recursive, max_relative=1e-9, - ); - } - } - } - } - - #[test] - fn finite_differences() { - let delta = 1e-6; - - for &n in &[1, 5, 10, 18] { - for &max_angular in &[0, 2, 8, 15] { - - let dr_1f1 = DoubleRegularized1F1{ max_angular }; - let mut values = Array1::from_elem(max_angular + 1, 0.0); - let mut values_delta = Array1::from_elem(max_angular + 1, 0.0); - let mut gradients = Array1::from_elem(max_angular + 1, 0.0); - - for &z in &[-200.0, -10.0, -1.1, -1e-2, 0.2, 1.5, 10.0, 40.0, 523.0] { - dr_1f1.compute(z, n, values.view_mut(), Some(gradients.view_mut())); - dr_1f1.compute(z + delta, n, values_delta.view_mut(), None); - - let finite_difference = (&values_delta - &values) / delta; - - assert_relative_eq!( - gradients, finite_difference, epsilon=delta, max_relative=1e-4, - ); - } - } - } - } -} diff --git a/rascaline/src/systems/chemfiles.rs b/rascaline/src/systems/chemfiles.rs deleted file mode 100644 index 35cb99b73..000000000 --- a/rascaline/src/systems/chemfiles.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::path::Path; - -use super::SimpleSystem; -use crate::Error; - -#[cfg(feature = "chemfiles")] -impl From for Error { - fn from(error: chemfiles::Error) -> Error { - Error::Chemfiles(error.message) - } -} - -/// Read all structures in the file at the given `path` using -/// [chemfiles](https://chemfiles.org/), and convert them to `SimpleSystem`s. -/// -/// This function can read all [formats supported by -/// chemfiles](https://chemfiles.org/chemfiles/latest/formats.html). -#[cfg(feature = "chemfiles")] -#[allow(clippy::needless_range_loop)] -pub fn read_from_file(path: impl AsRef) -> Result, Error> { - use std::collections::HashMap; - use crate::Matrix3; - use crate::systems::UnitCell; - - let mut systems = Vec::new(); - - let mut trajectory = chemfiles::Trajectory::open(path, 'r')?; - let mut frame = chemfiles::Frame::new(); - - let mut assigned_types = HashMap::new(); - let mut get_atomic_type = |atom: chemfiles::AtomRef| { - let atomic_number = atom.atomic_number(); - if atomic_number == 0 { - // use number assigned from the the atomic type, starting at 120 - // since that's larger than the number of elements in the periodic - // table - let new_type = 120 + assigned_types.len() as i32; - *assigned_types.entry(atom.atomic_type()).or_insert(new_type) - } else { - atomic_number as i32 - } - }; - - for _ in 0..trajectory.nsteps() { - trajectory.read(&mut frame)?; - - let positions = frame.positions(); - - let cell = if frame.cell().shape() == chemfiles::CellShape::Infinite { - UnitCell::infinite() - } else { - // transpose since chemfiles is using columns for the cell vectors and - // we want rows as cell vectors - UnitCell::from(Matrix3::from(frame.cell().matrix()).transposed()) - }; - let mut system = SimpleSystem::new(cell); - for i in 0..frame.size() { - let atom = frame.atom(i); - system.add_atom(get_atomic_type(atom), positions[i].into()); - } - - systems.push(system); - } - - return Ok(systems); -} - -/// Read all structures in the file at the given `path` using -/// [chemfiles](https://chemfiles.org/), and convert them to `SimpleSystem`s. -/// -/// This function can read all [formats supported by -/// chemfiles](https://chemfiles.org/chemfiles/latest/formats.html). -#[cfg(not(feature = "chemfiles"))] -pub fn read_from_file(_: impl AsRef) -> Result, Error> { - Err(Error::Chemfiles( - "read_from_file is only available with the chemfiles feature enabled \ - (RASCALINE_ENABLE_CHEMFILES=ON in CMake)".into() - )) -} - -#[cfg(all(test, feature = "chemfiles"))] -mod tests { - use std::path::PathBuf; - use approx::assert_relative_eq; - - use crate::{System, Vector3D}; - use super::*; - - #[test] - fn read() -> Result<(), Box> { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("benches"); - path.push("data"); - path.push("silicon_bulk.xyz"); - - let systems = read_from_file(&path).unwrap(); - - assert_eq!(systems.len(), 30); - assert_eq!(systems[0].size()?, 54); - assert_eq!(systems[0].types()?, [14; 54].as_ref()); - - assert_relative_eq!( - systems[0].positions()?[0], - Vector3D::from([7.8554, 7.84887, 0.0188612]) - ); - - let cell = systems[0].cell()?; - assert_relative_eq!(cell.a(), 11.098535905469692); - assert_relative_eq!(cell.b(), 11.098535905469692); - assert_relative_eq!(cell.c(), 11.098535905469692); - assert_relative_eq!(cell.alpha(), 60.0); - assert_relative_eq!(cell.beta(), 60.0); - assert_relative_eq!(cell.gamma(), 60.0); - - - let matrix = cell.matrix(); - assert_eq!(matrix[0], [7.847849999999999, 0.0, 7.847849999999999]); - assert_eq!(matrix[1], [7.847849999999999, 7.847849999999999, 0.0]); - assert_eq!(matrix[2], [0.0, 7.847849999999999, 7.847849999999999]); - - Ok(()) - } -} diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-5/gradients-input.json b/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-5/gradients-input.json deleted file mode 100644 index c312b720b..000000000 --- a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-5/gradients-input.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "hyperparameters": { - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "cutoff": 2.5, - "max_angular": 3, - "max_radial": 3, - "potential_exponent": 5, - "radial_basis": { - "Gto": { - "splined_radial_integral": false - } - } - }, - "systems": [ - { - "cell": [ - 6.0, - 0.0, - 0.0, - 0.0, - 6.0, - 0.0, - 0.0, - 0.0, - 6.0 - ], - "positions": [ - [ - 3.88997, - 5.11396, - 1.9859 - ], - [ - 1.60538, - 5.74085, - 3.48071 - ], - [ - 5.15178, - 5.59335, - 5.55114 - ], - [ - 2.22548, - 2.03678, - 4.16896 - ], - [ - 2.33853, - 2.79487, - 2.3533 - ], - [ - 3.54073, - 3.59016, - 2.34664 - ], - [ - 1.34344, - 2.94555, - 2.4665 - ], - [ - 1.74165, - 3.03466, - 0.921584 - ], - [ - 0.474942, - 3.34246, - 2.73754 - ] - ], - "types": [ - 6, - 1, - 8, - 6, - 6, - 6, - 6, - 1, - 1 - ] - } - ] -} diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-5/values-input.json b/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-5/values-input.json deleted file mode 100644 index 4c391c30d..000000000 --- a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-5/values-input.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "hyperparameters": { - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "cutoff": 2.5, - "max_angular": 4, - "max_radial": 4, - "potential_exponent": 5, - "radial_basis": { - "Gto": { - "splined_radial_integral": false - } - } - }, - "systems": [ - { - "cell": [ - 6.0, - 0.0, - 0.0, - 0.0, - 6.0, - 0.0, - 0.0, - 0.0, - 6.0 - ], - "positions": [ - [ - 3.88997, - 5.11396, - 1.9859 - ], - [ - 1.60538, - 5.74085, - 3.48071 - ], - [ - 5.15178, - 5.59335, - 5.55114 - ], - [ - 2.22548, - 2.03678, - 4.16896 - ], - [ - 2.33853, - 2.79487, - 2.3533 - ], - [ - 3.54073, - 3.59016, - 2.34664 - ], - [ - 1.34344, - 2.94555, - 2.4665 - ], - [ - 1.74165, - 3.03466, - 0.921584 - ], - [ - 0.474942, - 3.34246, - 2.73754 - ] - ], - "types": [ - 6, - 1, - 8, - 6, - 6, - 6, - 6, - 1, - 1 - ] - } - ] -} diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-6/gradients-input.json b/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-6/gradients-input.json deleted file mode 100644 index f2677bd51..000000000 --- a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-6/gradients-input.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "hyperparameters": { - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "cutoff": 2.5, - "max_angular": 3, - "max_radial": 3, - "potential_exponent": 6, - "radial_basis": { - "Gto": { - "splined_radial_integral": false - } - } - }, - "systems": [ - { - "cell": [ - 6.0, - 0.0, - 0.0, - 0.0, - 6.0, - 0.0, - 0.0, - 0.0, - 6.0 - ], - "positions": [ - [ - 3.88997, - 5.11396, - 1.9859 - ], - [ - 1.60538, - 5.74085, - 3.48071 - ], - [ - 5.15178, - 5.59335, - 5.55114 - ], - [ - 2.22548, - 2.03678, - 4.16896 - ], - [ - 2.33853, - 2.79487, - 2.3533 - ], - [ - 3.54073, - 3.59016, - 2.34664 - ], - [ - 1.34344, - 2.94555, - 2.4665 - ], - [ - 1.74165, - 3.03466, - 0.921584 - ], - [ - 0.474942, - 3.34246, - 2.73754 - ] - ], - "types": [ - 6, - 1, - 8, - 6, - 6, - 6, - 6, - 1, - 1 - ] - } - ] -} diff --git a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-6/values-input.json b/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-6/values-input.json deleted file mode 100644 index dc22fefe8..000000000 --- a/rascaline/tests/data/generated/lode-spherical-expansion/potential_exponent-6/values-input.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "hyperparameters": { - "atomic_gaussian_width": 0.3, - "center_atom_weight": 1.0, - "cutoff": 2.5, - "max_angular": 4, - "max_radial": 4, - "potential_exponent": 6, - "radial_basis": { - "Gto": { - "splined_radial_integral": false - } - } - }, - "systems": [ - { - "cell": [ - 6.0, - 0.0, - 0.0, - 0.0, - 6.0, - 0.0, - 0.0, - 0.0, - 6.0 - ], - "positions": [ - [ - 3.88997, - 5.11396, - 1.9859 - ], - [ - 1.60538, - 5.74085, - 3.48071 - ], - [ - 5.15178, - 5.59335, - 5.55114 - ], - [ - 2.22548, - 2.03678, - 4.16896 - ], - [ - 2.33853, - 2.79487, - 2.3533 - ], - [ - 3.54073, - 3.59016, - 2.34664 - ], - [ - 1.34344, - 2.94555, - 2.4665 - ], - [ - 1.74165, - 3.03466, - 0.921584 - ], - [ - 0.474942, - 3.34246, - 2.73754 - ] - ], - "types": [ - 6, - 1, - 8, - 6, - 6, - 6, - 6, - 1, - 1 - ] - } - ] -} diff --git a/scripts/build-all-wheels.sh b/scripts/build-all-wheels.sh new file mode 100644 index 000000000..b7b3faffb --- /dev/null +++ b/scripts/build-all-wheels.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -eux + +ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd) +cd "$ROOT_DIR" + +TMP_DIR="$1" +rm -rf "$TMP_DIR"/dist + +# check building sdist from a checkout, and wheel from the sdist +python -m build python/featomic --outdir "$TMP_DIR"/dist + +# get the version of featomic we just built +FEATOMIC_VERSION=$(basename "$(find "$TMP_DIR"/dist -name "featomic-*.tar.gz")" | cut -d - -f 2) +FEATOMIC_VERSION=${FEATOMIC_VERSION%.tar.gz} + +# for featomic-torch, we need a pre-built version of featomic, so +# we use the one we just generated and make it available to pip +dir2pi --no-symlink "$TMP_DIR"/dist + +PORT=8912 +if nc -z localhost $PORT; then + printf "\033[91m ERROR: an application is listening to port %d. Please free up the port first. \033[0m\n" $PORT >&2 + exit 1 +fi + +PYPI_SERVER_PID="" +function cleanup() { + kill $PYPI_SERVER_PID +} +# Make sure to stop the Python server on script exit/cancellation +trap cleanup INT TERM EXIT + +python -m http.server --directory "$TMP_DIR"/dist $PORT & +PYPI_SERVER_PID=$! + +# add the python server to the set of extra pip index URL +export PIP_EXTRA_INDEX_URL="http://localhost:$PORT/simple/ ${PIP_EXTRA_INDEX_URL=}" +# force featomic-torch to use a specific featomic version when building +export FEATOMIC_TORCH_BUILD_WITH_FEATOMIC_VERSION="$FEATOMIC_VERSION" + +# build featomic-torch, using featomic from `PIP_EXTRA_INDEX_URL` +# for the sdist => wheel build. +python -m build python/featomic_torch --outdir "$TMP_DIR/dist" diff --git a/scripts/clean-python.sh b/scripts/clean-python.sh index ef27ea871..fdf33c007 100755 --- a/scripts/clean-python.sh +++ b/scripts/clean-python.sh @@ -10,12 +10,18 @@ cd "$ROOT_DIR" rm -rf dist rm -rf build -rm -rf .coverage + rm -rf docs/build rm -rf docs/src/examples -rm -rf python/rascaline-torch/dist -rm -rf python/rascaline-torch/build +rm -rf python/featomic/dist +rm -rf python/featomic/build +rm -rf python/featomic/featomic-cxx-*.tar.gz + +rm -rf python/featomic_torch/dist +rm -rf python/featomic_torch/build +rm -rf python/featomic_torch/featomic-torch-cxx-*.tar.gz find . -name "*.egg-info" -exec rm -rf "{}" + find . -name "__pycache__" -exec rm -rf "{}" + +find . -name ".coverage" -exec rm -rf "{}" + diff --git a/scripts/create-torch-versions-range.py b/scripts/create-torch-versions-range.py new file mode 100755 index 000000000..8a71a7a80 --- /dev/null +++ b/scripts/create-torch-versions-range.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +""" +This script updates the `Requires-Dist` information in featomic-torch wheel METADATA to +contain the range of compatible torch versions. It expects newline separated +`Requires-Dist: torch ==...` information (corresponding to wheels built against a single +torch version) and will print `Requires-Dist: torch >=$MIN_VERSION,<${MAX_VERSION+1}` on +the standard output. + +This output can the be used in the merged wheel containing the build against all torch +versions. +""" + +import re +import sys + + +if __name__ == "__main__": + torch_versions_raw = sys.argv[1] + + torch_versions = [] + for version in torch_versions_raw.split("\n"): + if version.strip() == "": + continue + + match = re.match(r"Requires-Dist: torch[ ]?==(\d+)\.(\d+)\.\*", version) + if match is None: + raise ValueError(f"unexpected Requires-Dist format: {version}") + + major, minor = match.groups() + major = int(major) + minor = int(minor) + + version = (major, minor) + + if version in torch_versions: + raise ValueError(f"duplicate torch version: {version}") + + torch_versions.append(version) + + torch_versions = list(sorted(torch_versions)) + + min_version = f"{torch_versions[0][0]}.{torch_versions[0][1]}" + max_version = f"{torch_versions[-1][0]}.{torch_versions[-1][1] + 1}" + + print(f"Requires-Dist: torch >={min_version},<{max_version}") diff --git a/scripts/git-version-info.py b/scripts/git-version-info.py new file mode 100755 index 000000000..88db8033f --- /dev/null +++ b/scripts/git-version-info.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +""" +This script calls git to get the number of commits since the last tag, as well as a full +hash of all code. It then prints both to stdout. +""" + +import os +import re +import shutil +import subprocess +import sys +import tempfile + + +ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..")) + + +def warn_and_exit(message, exit_code=0): + print(message, file=sys.stderr) + print(0, file=sys.stdout) + sys.exit(exit_code) + + +def run_subprocess(args, check=True, env=None): + output = subprocess.run( + args, + capture_output=True, + encoding="utf8", + check=False, + env=env, + ) + + if check and output.returncode != 0: + raise Exception( + f"failed to run '{' '.join(args)}' (exit code {output.returncode})\n" + f"stdout: {output.stdout}\n" + f"stderr: {output.stderr}\n" + ) + + if output.stderr != "": + print(output.stderr, file=sys.stderr) + + return output + + +def n_commits_since_last_tag(tag_prefix): + # get the full list of tags + result = run_subprocess(["git", "tag", "--sort=-creatordate"]) + all_tags = result.stdout.strip().split("\n") + + latest_tag = None + for tag in all_tags: + if not tag: + continue + + if tag.startswith(tag_prefix): + latest_tag = tag + break + + if latest_tag is None: + # no matching tags, use the first commit as the original ref + result = run_subprocess(["git", "rev-list", "--max-parents=0", "HEAD"]) + reference = result.stdout.strip() + else: + # get the commit corresponding to the most recent tag + result = run_subprocess(["git", "rev-parse", f"{latest_tag}^0"]) + reference = result.stdout.strip() + + result = run_subprocess(["git", "rev-list", f"{reference}..HEAD", "--count"]) + n_commits = result.stdout.strip() + + return n_commits + + +def git_hash_all_code(): + # make sure the index is up to date before doing `git diff-index` + run_subprocess(["git", "update-index", "-q", "--really-refresh"]) + + output = subprocess.run( + ["git", "diff-index", "--quiet", "HEAD", "--"], + capture_output=True, + ) + + if output.returncode not in [0, 1]: + raise Exception( + "failed to get git information (`git diff-index`).\n" + f"stdout: {output.stdout}\n" + f"stderr: {output.stderr}\n" + ) + + is_dirty = output.returncode == 1 + if is_dirty: + # This gets the full git hash for the current repo, including non-committed and + # non-staged code (cf https://stackoverflow.com/a/48213033). It does so by + # pretending to stage all the file using a temporary index. This way the actual + # git index is left untouched. + with tempfile.NamedTemporaryFile("wb") as tmp: + with open(os.path.join(ROOT, ".git", "index"), "rb") as git_index: + shutil.copyfileobj(git_index, tmp) + tmp.close() + + git_env = os.environ.copy() + git_env["GIT_INDEX_FILE"] = tmp.name + run_subprocess(["git", "add", "--all"], env=git_env) + + output = run_subprocess(["git", "write-tree"], env=git_env) + short_hash = output.stdout[:7] + else: + output = run_subprocess(["git", "rev-parse", "HEAD"]) + short_hash = output.stdout[:7] + + return ("dirty." if is_dirty else "git.") + short_hash + + +if __name__ == "__main__": + if len(sys.argv) != 2: + warn_and_exit(f"usage: {sys.argv[0]} ", exit_code=1) + + tag_prefix = sys.argv[1] + + try: + result = run_subprocess(["git", "--version"]) + except Exception: + warn_and_exit("could not run `git --version`, is git installed on your system?") + + # We need git >=2.0 for `git tag --sort=-creatordate` + _, _, git_version, *_ = result.stdout.split() + if not re.match(r"2\.\d+\.\d+", git_version): + warn_and_exit(f"this script requires git>=2.0, we found git v{git_version}") + + result = run_subprocess(["git", "rev-parse", "--show-toplevel"], check=False) + + if result.returncode != 0 or not os.path.samefile(result.stdout.strip(), ROOT): + warn_and_exit( + "the git root is not featomic repository, if you are trying to build " + "featomic from source please use a git checkout" + ) + + print(n_commits_since_last_tag(tag_prefix)) + print(git_hash_all_code()) diff --git a/scripts/package-featomic-torch.sh b/scripts/package-featomic-torch.sh new file mode 100755 index 000000000..2776d00ab --- /dev/null +++ b/scripts/package-featomic-torch.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +# This script creates an archive containing the sources for the C++ part of +# featomic-torch, and copy it to the path given as argument + +set -eux + +OUTPUT_DIR="$1" +mkdir -p "$OUTPUT_DIR" +OUTPUT_DIR=$(cd "$OUTPUT_DIR" 2>/dev/null && pwd) + +ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd) + +VERSION=$(cat "$ROOT_DIR/featomic-torch/VERSION") +ARCHIVE_NAME="featomic-torch-cxx-$VERSION" + +TMP_DIR=$(mktemp -d) +mkdir "$TMP_DIR/$ARCHIVE_NAME" + +cp -r "$ROOT_DIR"/featomic-torch/* "$TMP_DIR/$ARCHIVE_NAME/" +cp "$ROOT_DIR/LICENSE" "$TMP_DIR/$ARCHIVE_NAME" +cp "$ROOT_DIR/AUTHORS" "$TMP_DIR/$ARCHIVE_NAME" + +# Get the git version information, this is used when building the +# code to change the version for development builds +cd "$ROOT_DIR" +./scripts/git-version-info.py "featomic-torch-v" > "$TMP_DIR/$ARCHIVE_NAME/cmake/git_version_info" + +cd "$TMP_DIR" +tar cf "$ARCHIVE_NAME".tar "$ARCHIVE_NAME" + +gzip -9 "$ARCHIVE_NAME".tar + +rm -f "$ROOT_DIR"/python/featomic_torch/featomic-torch-cxx-*.tar.gz +cp "$ARCHIVE_NAME".tar.gz "$OUTPUT_DIR/" diff --git a/scripts/package-featomic.sh b/scripts/package-featomic.sh new file mode 100755 index 000000000..66ad4b693 --- /dev/null +++ b/scripts/package-featomic.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +# This script creates an archive containing the sources for the featomic +# Rust crate, and copy it to the path given as argument + +set -eux + +OUTPUT_DIR="$1" +mkdir -p "$OUTPUT_DIR" +OUTPUT_DIR=$(cd "$OUTPUT_DIR" 2>/dev/null && pwd) + +ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd) + +rm -rf "$ROOT_DIR/target/package" +cd "$ROOT_DIR/featomic" + +# Package featomic using cargo tools +cargo package --allow-dirty --no-verify + +TMP_DIR=$(mktemp -d) + +cd "$TMP_DIR" +tar xf "$ROOT_DIR"/target/package/featomic-*.crate +ARCHIVE_NAME=$(ls) + +# extract the version part of the package from the .crate file name +VERSION=${ARCHIVE_NAME:9} +ARCHIVE_NAME="featomic-cxx-$VERSION" + +mv featomic-* "$ARCHIVE_NAME" +cp "$ROOT_DIR/LICENSE" "$TMP_DIR/$ARCHIVE_NAME" +cp "$ROOT_DIR/AUTHORS" "$TMP_DIR/$ARCHIVE_NAME" +cp "$ROOT_DIR/README.rst" "$TMP_DIR/$ARCHIVE_NAME" + +# Get the git version information, this is used when building the +# code to change the version for development builds +cd "$ROOT_DIR" +./scripts/git-version-info.py "featomic-v" > "$TMP_DIR/$ARCHIVE_NAME/cmake/git_version_info" + +cd "$TMP_DIR" +# Compile featomic as it's own Cargo workspace +echo "[workspace]" >> "$ARCHIVE_NAME/Cargo.toml" + +cargo generate-lockfile --manifest-path "$ARCHIVE_NAME/Cargo.toml" + +# remove tests files from the archive, these are relatively big +rm -rf "$ARCHIVE_NAME/tests" + +tar cf "$ARCHIVE_NAME.tar" "$ARCHIVE_NAME" +gzip -9 "$ARCHIVE_NAME.tar" + +cp "$TMP_DIR/$ARCHIVE_NAME.tar.gz" "$OUTPUT_DIR/" diff --git a/scripts/package-torch.sh b/scripts/package-torch.sh deleted file mode 100755 index 39a97eadb..000000000 --- a/scripts/package-torch.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -# This script creates an archive containing the sources for the C++ part of -# rascaline-torch, and copy it to be included in the rascaline-torch python -# package sdist. - -ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -set -eux - -cd "$ROOT_DIR" -tar cf rascaline-torch.tar rascaline-torch -gzip -9 rascaline-torch.tar - -mv rascaline-torch.tar.gz python/rascaline-torch/ diff --git a/setup.py b/setup.py index f588f4f2b..cb87c22ae 100644 --- a/setup.py +++ b/setup.py @@ -1,285 +1,24 @@ -import glob +# This is not the actual setup.py for this project, see `python/featomic/setup.py` for +# it. Instead, this file is here to enable `pip install .` from a git checkout or `pip +# install git+https://...` without having to specify a subdirectory + import os -import shutil -import subprocess -import sys -import uuid -from setuptools import Extension, setup -from setuptools.command.bdist_egg import bdist_egg -from setuptools.command.build_ext import build_ext -from setuptools.command.sdist import sdist -from wheel.bdist_wheel import bdist_wheel +from setuptools import setup ROOT = os.path.realpath(os.path.dirname(__file__)) -RASCALINE_TORCH = os.path.join(ROOT, "python", "rascaline-torch") - -RASCALINE_BUILD_TYPE = os.environ.get("RASCALINE_BUILD_TYPE", "release") -if RASCALINE_BUILD_TYPE not in ["debug", "release"]: - raise Exception( - f"invalid build type passed: '{RASCALINE_BUILD_TYPE}'," - "expected 'debug' or 'release'" - ) - - -class universal_wheel(bdist_wheel): - """Helper class for override wheel tag. - - When building the wheel, the `wheel` package assumes that if we have a - binary extension then we are linking to `libpython.so`; and thus the wheel - is only usable with a single python version. This is not the case for - here, and the wheel will be compatible with any Python >=3.6. This is - tracked in https://github.com/pypa/wheel/issues/185, but until then we - manually override the wheel tag. - """ - - def get_tag(self): - """Get the tag for override.""" - tag = bdist_wheel.get_tag(self) - # tag[2:] contains the os/arch tags, we want to keep them - return ("py3", "none") + tag[2:] - -class cmake_ext(build_ext): - """Build the native library using cmake.""" - - def run(self): - """Run cmake build and install the resulting library.""" - source_dir = os.path.join(ROOT, "rascaline-c-api") - build_dir = os.path.join(ROOT, "build", "cmake-build") - install_dir = os.path.join(os.path.realpath(self.build_lib), "rascaline") - - try: - os.mkdir(build_dir) - except OSError: - pass - - cmake_options = [ - f"-DCMAKE_INSTALL_PREFIX={install_dir}", - "-DCMAKE_INSTALL_LIBDIR=lib", - f"-DCMAKE_BUILD_TYPE={RASCALINE_BUILD_TYPE}", - # do not include chemfiles inside rascaline, instead users should - # use chemfiles python bindings directly - "-DRASCALINE_ENABLE_CHEMFILES=OFF", - "-DRASCALINE_FETCH_METATENSOR=ON", - "-DRASCALINE_INSTALL_BOTH_STATIC_SHARED=OFF", - "-DBUILD_SHARED_LIBS=ON", - "-DEXTRA_RUST_FLAGS=-Cstrip=symbols", +setup( + name="featomic-git", + version="0.0.0", + install_requires=[ + f"featomic @ file://{ROOT}/python/featomic", + ], + extras_require={ + "torch": [ + f"featomic[torch] @ file://{ROOT}/python/featomic", ] - - if "CARGO" in os.environ: - cmake_options.append(f"-DCARGO_EXE={os.environ['CARGO']}") - - # Handle cross-compilation by detecting cibuildwheels environnement - # variables - if sys.platform.startswith("darwin"): - # ARCHFLAGS is set by cibuildwheels - ARCHFLAGS = os.environ.get("ARCHFLAGS") - if ARCHFLAGS is not None: - archs = filter( - lambda u: bool(u), - ARCHFLAGS.strip().split("-arch "), - ) - archs = list(archs) - assert len(archs) == 1 - arch = archs[0].strip() - - if arch == "x86_64": - cmake_options.append("-DRUST_BUILD_TARGET=x86_64-apple-darwin") - elif arch == "arm64": - cmake_options.append("-DRUST_BUILD_TARGET=aarch64-apple-darwin") - else: - raise ValueError(f"unknown arch: {arch}") - - elif sys.platform.startswith("linux"): - # we set RUST_BUILD_TARGET in our custom docker image - RUST_BUILD_TARGET = os.environ.get("RUST_BUILD_TARGET") - if RUST_BUILD_TARGET is not None: - cmake_options.append(f"-DRUST_BUILD_TARGET={RUST_BUILD_TARGET}") - - elif sys.platform.startswith("win32"): - # CARGO_BUILD_TARGET is set by cibuildwheels - CARGO_BUILD_TARGET = os.environ.get("CARGO_BUILD_TARGET") - if CARGO_BUILD_TARGET is not None: - cmake_options.append(f"-DRUST_BUILD_TARGET={CARGO_BUILD_TARGET}") - - else: - raise ValueError(f"unknown platform: {sys.platform}") - - subprocess.run( - ["cmake", source_dir, *cmake_options], - cwd=build_dir, - check=True, - ) - subprocess.run( - ["cmake", "--build", build_dir, "--parallel", "--target", "install"], - check=True, - ) - - # do not include metatensor libraries/headers/cmake config within - # rascaline wheel - for file in glob.glob(os.path.join(install_dir, "lib", "libmetatensor.*")): - os.unlink(file) - - for file in glob.glob(os.path.join(install_dir, "bin", "metatensor.dll")): - os.unlink(file) - - shutil.rmtree(os.path.join(install_dir, "lib", "cmake", "metatensor")) - - for file in glob.glob(os.path.join(install_dir, "include", "metatensor*")): - os.unlink(file) - - -class bdist_egg_disabled(bdist_egg): - """Disabled version of bdist_egg - - Prevents setup.py install performing setuptools' default easy_install, - which it should never ever do. - """ - - def run(self): - sys.exit( - "Aborting implicit building of eggs. " - + "Use `pip install .` or `python setup.py bdist_wheel && pip " - + "uninstall metatensor -y && pip install dist/metatensor-*.whl` " - + "to install from source." - ) - - -class sdist_git_version(sdist): - """ - Create a sdist with an additional generated file containing the extra - version from git. - """ - - def run(self): - with open("git_extra_version", "w") as fd: - fd.write(git_extra_version()) - - # run original sdist - super().run() - - os.unlink("git_extra_version") - - -def get_rust_version(): - # read version from Cargo.toml - with open(os.path.join(ROOT, "rascaline-c-api", "Cargo.toml")) as fd: - for line in fd: - if line.startswith("version"): - _, version = line.split(" = ") - # remove quotes - version = version[1:-2] - # take the first version in the file, this should be the right - # version - break - - return version - - -def git_extra_version(): - """ - If git is available, it is used to check if we are installing a development - version or a released version (by checking how many commits happened since - the last tag). - """ - - # Add pre-release info the version - try: - tags_list = subprocess.run( - ["git", "tag"], - stderr=subprocess.DEVNULL, - stdout=subprocess.PIPE, - check=True, - ) - tags_list = tags_list.stdout.decode("utf8").strip() - - if tags_list == "": - first_commit = subprocess.run( - ["git", "rev-list", "--max-parents=0", "HEAD"], - stderr=subprocess.DEVNULL, - stdout=subprocess.PIPE, - check=True, - ) - reference = first_commit.stdout.decode("utf8").strip() - - else: - last_tag = subprocess.run( - ["git", "describe", "--tags", "--abbrev=0"], - stderr=subprocess.DEVNULL, - stdout=subprocess.PIPE, - check=True, - ) - - reference = last_tag.stdout.decode("utf8").strip() - - except Exception: - reference = "" - pass - - try: - n_commits_since_tag = subprocess.run( - ["git", "rev-list", f"{reference}..HEAD", "--count"], - stderr=subprocess.DEVNULL, - stdout=subprocess.PIPE, - check=True, - ) - n_commits_since_tag = n_commits_since_tag.stdout.decode("utf8").strip() - - if n_commits_since_tag != 0: - return ".dev" + n_commits_since_tag - except Exception: - pass - - return "" - - -if __name__ == "__main__": - if os.path.exists("git_extra_version"): - # we are building from a sdist, without git available, but the git - # version was recorded in a git_extra_version file - with open("git_extra_version") as fd: - extra_version = fd.read() - else: - extra_version = git_extra_version() - - version = get_rust_version() + extra_version - - with open(os.path.join(ROOT, "AUTHORS")) as fd: - authors = fd.read().splitlines() - - extras_require = {} - if os.path.exists(RASCALINE_TORCH): - # we are building from a git checkout - - # add a random uuid to the file url to prevent pip from using a cached - # wheel for rascaline-torch, and force it to re-build from scratch - uuid = uuid.uuid4() - extras_require["torch"] = f"rascaline-torch @ file://{RASCALINE_TORCH}?{uuid}" - else: - # we are building from a sdist/installing from a wheel - extras_require["torch"] = "rascaline-torch >=0.1.0.dev0,<0.2.0" - - setup( - version=version, - author=", ".join(authors), - extras_require=extras_require, - ext_modules=[ - # only declare the extension, it is built & copied as required by cmake - # in the build_ext command - Extension(name="rascaline", sources=[]), - ], - cmdclass={ - "build_ext": cmake_ext, - "bdist_egg": bdist_egg if "bdist_egg" in sys.argv else bdist_egg_disabled, - "bdist_wheel": universal_wheel, - "sdist": sdist_git_version, - }, - package_data={ - "rascaline": [ - "rascaline/lib/*", - "rascaline/include/*", - ] - }, - ) + }, + packages=[], +) diff --git a/tox.ini b/tox.ini index c4a5c555d..b01e45fea 100644 --- a/tox.ini +++ b/tox.ini @@ -14,11 +14,11 @@ envlist = [testenv] package = external -package_env = build-rascaline +package_env = build-featomic passenv = * lint-folders = "{toxinidir}/python" "{toxinidir}/setup.py" -# we need to manually install dependencies for rascaline, since tox will install +# we need to manually install dependencies for featomic, since tox will install # the fresh wheel with `--no-deps` after building it. metatensor-core-requirement = metatensor-core >=0.1.0,<0.2.0 @@ -35,7 +35,7 @@ warning_options = # internal warnings with Python 3.12 test_options = - --cov={env_site_packages_dir}/rascaline \ + --cov={env_site_packages_dir}/featomic \ --cov-append \ --cov-report= \ --import-mode=append \ @@ -44,27 +44,29 @@ test_options = packaging_deps = setuptools + packaging wheel cmake -[testenv:build-rascaline] +[testenv:build-featomic] description = This environment is only used to build the wheels which are then re-used by - all other environments requiring rascaline to be installed + all other environments requiring featomic to be installed passenv = * deps = {[testenv]packaging_deps} + {[testenv]metatensor-core-requirement} + commands = - pip wheel . {[testenv]build-single-wheel} --wheel-dir {envtmpdir}/dist + pip wheel python/featomic {[testenv]build-single-wheel} --wheel-dir {envtmpdir}/dist [testenv:all-deps] # note: platform_system can be "Linux","Darwin", or "Windows". description = - Run Python unit tests with all dependencies installed (ase, pyscf, - chemfiles and torch are optional dependencies) + Run Python unit tests with all dependencies installed deps = {[testenv]metatensor-core-requirement} ase @@ -78,6 +80,7 @@ deps = pyscf;platform_system!="Windows" wigners +changedir = python/featomic commands = pytest {[testenv]test_options} {posargs} @@ -88,6 +91,7 @@ deps = pytest pytest-cov +changedir = python/featomic commands = pytest {[testenv]test_options} {posargs} @@ -105,9 +109,9 @@ deps = torch ase -changedir = python/rascaline-torch +changedir = python/featomic_torch commands = - # install rascaline-torch + # install featomic-torch pip install . {[testenv]build-single-wheel} --force-reinstall # run the unit tests pytest {[testenv]test_options} --assert=plain {posargs} @@ -122,8 +126,8 @@ allowlist_externals = bash commands = - # install rascaline-torch - pip install python/rascaline-torch --no-deps --no-build-isolation --force-reinstall + # install featomic-torch + pip install python/featomic_torch --no-deps --no-build-isolation --force-reinstall sphinx-build {posargs:-E} -W -b html docs/src docs/build/html @@ -137,71 +141,51 @@ deps = pytest commands = - pytest --doctest-modules --pyargs rascaline + pytest --doctest-modules --pyargs featomic [testenv:lint] -description = - lint the Python code with flake8 (code linter), black (code formatter), and isort - (sorting of imports) +description = Run linters and formatter package = skip deps = - black - blackdoc - flake8 - flake8-bugbear - isort + ruff commands = - flake8 {[testenv]lint-folders} - black --check --diff {[testenv]lint-folders} - blackdoc --check --diff {[testenv]lint-folders} - isort --check-only --diff {[testenv]lint-folders} + ruff format --diff {[testenv]lint-folders} + ruff check {[testenv]lint-folders} [testenv:format] description = Abuse tox to do actual formatting on all files. package = skip deps = - black - blackdoc - isort + ruff commands = - black {[testenv]lint-folders} - blackdoc {[testenv]lint-folders} - isort {[testenv]lint-folders} + ruff format {[testenv]lint-folders} + ruff check --fix-only {[testenv]lint-folders} -[testenv:build-python] +[testenv:build-tests] +description = Asserts Pythons package build integrity so one can build sdist and wheels package = skip -# Make sure we can build sdist and a wheel for python deps = - twine build + twine # a tool to check sdist and wheels metadata + pip2pi # tool to create PyPI-like package indexes + setuptools -allowlist_externals = - bash - +allowlist_externals = bash commands = python --version # print the version of python used in this test - bash ./scripts/package-torch.sh - - bash -c "rm -rf {envtmpdir}/dist" - - # check building sdist from a checkout, and wheel from the sdist - python -m build . --outdir {envtmpdir}/dist - - # for rascaline-torch, we can not build from a sdist until rascaline - # is available on PyPI, so we build both sdist and wheel from a checkout - python -m build python/rascaline-torch --sdist --outdir {envtmpdir}/dist - python -m build python/rascaline-torch --wheel --outdir {envtmpdir}/dist + bash ./scripts/build-all-wheels.sh {envtmpdir} twine check {envtmpdir}/dist/*.tar.gz twine check {envtmpdir}/dist/*.whl # check building wheels directly from the a checkout - python -m build . --wheel --outdir {envtmpdir}/dist + python -m build python/featomic --wheel --outdir {envtmpdir}/dist + python -m build python/featomic_torch --wheel --outdir {envtmpdir}/dist [flake8] @@ -216,10 +200,10 @@ omit = examples/.* [coverage:paths] -rascaline = - python/rascaline/rascaline - .tox/*/lib/python*/site-packages/rascaline +featomic = + python/featomic/featomic + .tox/*/lib/python*/site-packages/featomic -rascaline_torch = - python/rascaline-torch/rascaline/torch - .tox/*/lib/python*/site-packages/rascaline/torch +featomic_torch = + python/featomic_torch/featomic/torch + .tox/*/lib/python*/site-packages/featomic/torch