From ae6baba66e59d0f074f93f91afc6db58d286a226 Mon Sep 17 00:00:00 2001 From: vdemestral Date: Fri, 17 Jan 2025 11:41:46 +0100 Subject: [PATCH] Add Pockels tensor calculation --- .cla/version1/CLA.md | 102 ++++++++ .cla/version1/signatures.json | 20 ++ .github/workflows/ci.yml | 13 +- .github/workflows/cla.yml | 55 +++++ .github/workflows/update_changelog.py | 85 +++++++ CHANGELOG.md | 85 +++++++ LICENSE.txt | 92 ++++++- README.md | 53 +++- codecov.yaml | 12 + docs/source/citeus.md | 4 +- docs/source/commands.md | 9 + docs/source/conf.py | 9 +- docs/source/howto/overrides.md | 12 +- docs/source/howto/postprocess.md | 26 +- docs/source/howto/understand.md | 2 +- docs/source/index.md | 11 +- docs/source/reference/{ => api}/index.md | 4 +- docs/source/topics/formulation.md | 4 +- examples/README.md | 27 ++ examples/commands/README.md | 77 ++++++ examples/commands/overrides.yaml | 32 +++ .../scripts/workflows/spectra/overrides.yaml | 70 ++++++ .../workflows/spectra/submit_default.py | 62 +++++ .../spectra/submit_default_custom_kpoints.py | 72 ++++++ .../spectra/submit_default_custom_pseudos.py | 95 +++++++ .../spectra/submit_with_overrides.py | 69 ++++++ pyproject.toml | 17 +- src/aiida_vibroscopy/__init__.py | 2 +- .../numerical_derivatives_utils.py | 35 ++- .../calculations/phonon_utils.py | 155 ------------ .../calculations/spectra_utils.py | 216 ++++++++++++++-- src/aiida_vibroscopy/calculations/symmetry.py | 23 +- src/aiida_vibroscopy/cli/__init__.py | 15 ++ src/aiida_vibroscopy/cli/utils/__init__.py | 3 + src/aiida_vibroscopy/cli/utils/defaults.py | 44 ++++ src/aiida_vibroscopy/cli/utils/display.py | 42 ++++ src/aiida_vibroscopy/cli/utils/launch.py | 43 ++++ src/aiida_vibroscopy/cli/utils/options.py | 126 ++++++++++ src/aiida_vibroscopy/cli/utils/validate.py | 34 +++ .../cli/workflows/__init__.py | 16 ++ .../cli/workflows/dielectric/__init__.py | 3 + .../cli/workflows/dielectric/base.py | 56 +++++ .../cli/workflows/phonons/__init__.py | 3 + .../cli/workflows/phonons/base.py | 58 +++++ .../cli/workflows/phonons/harmonic.py | 73 ++++++ .../cli/workflows/spectra/__init__.py | 3 + .../cli/workflows/spectra/iraman.py | 66 +++++ src/aiida_vibroscopy/common/constants.py | 10 +- src/aiida_vibroscopy/data/vibro_mixin.py | 187 +++++++++++--- src/aiida_vibroscopy/utils/broadenings.py | 2 +- src/aiida_vibroscopy/utils/validation.py | 18 +- .../workflows/dielectric/base.py | 5 +- .../dielectric/numerical_derivatives.py | 10 +- .../workflows/phonons/base.py | 36 ++- .../workflows/phonons/harmonic.py | 2 + tests/calculations/chi2_BTO.npy | Bin 0 -> 344 bytes tests/calculations/phonopy_BTO.yaml | 232 ++++++++++++++++++ tests/calculations/raman_BTO.npy | Bin 0 -> 1208 bytes .../test_numerical_derivatives.py | 191 ++++++++++++++ tests/calculations/test_spectra.py | 199 +++++++++++---- .../test_compute_clamped_pockels_tensor.npz | Bin 0 -> 1133 bytes .../test_spectra/test_compute_methods.npz | Bin 0 -> 144610 bytes ..._generate_vibrational_data_from_forces.npz | Bin 0 -> 1016 bytes tests/calculations/test_symmetry.py | 201 +++++++++++++++ tests/cli/__init__.py | 2 + tests/cli/conftest.py | 71 ++++++ tests/cli/fixtures/overrides/dielectric.yaml | 4 + tests/cli/fixtures/overrides/harmonic.yaml | 3 + .../fixtures/overrides/iraman-spectra.yaml | 3 + tests/cli/fixtures/overrides/phonon.yaml | 2 + tests/cli/test_commands.py | 46 ++++ tests/cli/workflows/__init__.py | 2 + tests/cli/workflows/dielectric/__init__.py | 2 + tests/cli/workflows/dielectric/test_base.py | 40 +++ tests/cli/workflows/phonons/__init__.py | 2 + tests/cli/workflows/phonons/test_base.py | 40 +++ tests/cli/workflows/phonons/test_harmonic.py | 40 +++ tests/cli/workflows/spectra/__init__.py | 2 + .../workflows/spectra/test_iraman_spectra.py | 40 +++ tests/conftest.py | 2 +- tests/data/test_vibro.py | 27 +- tests/data/test_vibro/test_methods.npz | Bin 0 -> 6864 bytes tests/utils/test_validation.py | 48 ++++ tests/workflows/phonons/test_harmonic.py | 24 +- tests/workflows/phonons/test_phonon.py | 54 +++- .../test_dielectric/test_default.yml | 2 + .../protocols/test_harmonic/test_default.yml | 4 + .../protocols/test_iraman/test_default.yml | 4 + tests/workflows/protocols/test_phonon.py | 4 + .../protocols/test_phonon/test_default.yml | 2 + 90 files changed, 3320 insertions(+), 378 deletions(-) create mode 100644 .cla/version1/CLA.md create mode 100644 .cla/version1/signatures.json create mode 100644 .github/workflows/cla.yml create mode 100644 .github/workflows/update_changelog.py create mode 100644 CHANGELOG.md create mode 100644 codecov.yaml create mode 100644 docs/source/commands.md rename docs/source/reference/{ => api}/index.md (82%) create mode 100644 examples/README.md create mode 100644 examples/commands/README.md create mode 100644 examples/commands/overrides.yaml create mode 100644 examples/scripts/workflows/spectra/overrides.yaml create mode 100644 examples/scripts/workflows/spectra/submit_default.py create mode 100644 examples/scripts/workflows/spectra/submit_default_custom_kpoints.py create mode 100644 examples/scripts/workflows/spectra/submit_default_custom_pseudos.py create mode 100644 examples/scripts/workflows/spectra/submit_with_overrides.py delete mode 100644 src/aiida_vibroscopy/calculations/phonon_utils.py create mode 100644 src/aiida_vibroscopy/cli/__init__.py create mode 100644 src/aiida_vibroscopy/cli/utils/__init__.py create mode 100644 src/aiida_vibroscopy/cli/utils/defaults.py create mode 100644 src/aiida_vibroscopy/cli/utils/display.py create mode 100644 src/aiida_vibroscopy/cli/utils/launch.py create mode 100644 src/aiida_vibroscopy/cli/utils/options.py create mode 100644 src/aiida_vibroscopy/cli/utils/validate.py create mode 100644 src/aiida_vibroscopy/cli/workflows/__init__.py create mode 100644 src/aiida_vibroscopy/cli/workflows/dielectric/__init__.py create mode 100755 src/aiida_vibroscopy/cli/workflows/dielectric/base.py create mode 100644 src/aiida_vibroscopy/cli/workflows/phonons/__init__.py create mode 100755 src/aiida_vibroscopy/cli/workflows/phonons/base.py create mode 100755 src/aiida_vibroscopy/cli/workflows/phonons/harmonic.py create mode 100644 src/aiida_vibroscopy/cli/workflows/spectra/__init__.py create mode 100755 src/aiida_vibroscopy/cli/workflows/spectra/iraman.py create mode 100644 tests/calculations/chi2_BTO.npy create mode 100644 tests/calculations/phonopy_BTO.yaml create mode 100644 tests/calculations/raman_BTO.npy create mode 100644 tests/calculations/test_numerical_derivatives.py create mode 100644 tests/calculations/test_spectra/test_compute_clamped_pockels_tensor.npz create mode 100644 tests/calculations/test_spectra/test_compute_methods.npz create mode 100644 tests/calculations/test_spectra/test_generate_vibrational_data_from_forces.npz create mode 100644 tests/calculations/test_symmetry.py create mode 100644 tests/cli/__init__.py create mode 100644 tests/cli/conftest.py create mode 100644 tests/cli/fixtures/overrides/dielectric.yaml create mode 100644 tests/cli/fixtures/overrides/harmonic.yaml create mode 100644 tests/cli/fixtures/overrides/iraman-spectra.yaml create mode 100644 tests/cli/fixtures/overrides/phonon.yaml create mode 100644 tests/cli/test_commands.py create mode 100644 tests/cli/workflows/__init__.py create mode 100644 tests/cli/workflows/dielectric/__init__.py create mode 100644 tests/cli/workflows/dielectric/test_base.py create mode 100644 tests/cli/workflows/phonons/__init__.py create mode 100644 tests/cli/workflows/phonons/test_base.py create mode 100644 tests/cli/workflows/phonons/test_harmonic.py create mode 100644 tests/cli/workflows/spectra/__init__.py create mode 100644 tests/cli/workflows/spectra/test_iraman_spectra.py create mode 100644 tests/data/test_vibro/test_methods.npz create mode 100644 tests/utils/test_validation.py diff --git a/.cla/version1/CLA.md b/.cla/version1/CLA.md new file mode 100644 index 0000000..a025e78 --- /dev/null +++ b/.cla/version1/CLA.md @@ -0,0 +1,102 @@ +# Contributor License Agreement + +> Adapted from the [Apache Software Foundation Individual Contributor License Agreement (ICLA)](https://www.apache.org/licenses/contributor-agreements.html) [version 2.2](https://www.apache.org/licenses/icla.pdf) + +You accept and agree to the following terms and conditions for Your +Contributions (present and future) that you submit to the copyright +holders (hereafter "HOLDERS", see the [`LICENSE.txt`](../../LICENSE.txt) +bundled with this software). In return, the HOLDERS shall not use +Your Contributions in a way that is contrary to the public benefit or +inconsistent with its nonprofit status and bylaws in effect at the +time of the Contribution. Except for the license granted herein to +the HOLDERS and recipients of software distributed by the HOLDERS, +You reserve all right, title, and interest in and to Your Contributions. + +1. Definitions. + + "You" (or "Your") shall mean the copyright owner or legal entity + authorized by the copyright owner that is making this Agreement + with the HOLDERS. For legal entities, the entity making a + Contribution and all other entities that control, are controlled + by, or are under common control with that entity are considered to + be a single Contributor. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + "Contribution" shall mean any original work of authorship, + including any modifications or additions to an existing work, that + is intentionally submitted by You to the HOLDERS for inclusion + in, or documentation of, any of the products owned or managed by + the HOLDERS (the "Work"). For the purposes of this definition, + "submitted" means any form of electronic, verbal, or written + communication sent to the HOLDERS or its representatives, + including but not limited to communication on electronic mailing + lists, source code control systems, and issue tracking systems that + are managed by, or on behalf of, the HOLDERS for the purpose of + discussing and improving the Work, but excluding communication that + is conspicuously marked or otherwise designated in writing by You + as "Not a Contribution." + +2. Grant of Copyright License. Subject to the terms and conditions of + this Agreement, You hereby grant to the HOLDERS and to + recipients of software distributed by the HOLDERS a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare derivative works of, + publicly display, publicly perform, sublicense, and distribute Your + Contributions and such derivative works. + +3. Grant of Patent License. Subject to the terms and conditions of + this Agreement, You hereby grant to the HOLDERS and to + recipients of software distributed by the HOLDERS a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have + made, use, offer to sell, sell, import, and otherwise transfer the + Work, where such license applies only to those patent claims + licensable by You that are necessarily infringed by Your + Contribution(s) alone or by combination of Your Contribution(s) + with the Work to which such Contribution(s) was submitted. If any + entity institutes patent litigation against You or any other entity + (including a cross-claim or counterclaim in a lawsuit) alleging + that your Contribution, or the Work to which you have contributed, + constitutes direct or contributory patent infringement, then any + patent licenses granted to that entity under this Agreement for + that Contribution or Work shall terminate as of the date such + litigation is filed. + +4. You represent that you are legally entitled to grant the above + license. If your employer(s) has rights to intellectual property + that you create that includes your Contributions, you represent + that you have received permission to make Contributions on behalf + of that employer, that your employer has waived such rights for + your Contributions to the HOLDERS, or that your employer has + executed a separate Corporate CLA with the HOLDERS. + +5. You represent that each of Your Contributions is Your original + creation (see section 7 for submissions on behalf of others). You + represent that Your Contribution submissions include complete + details of any third-party license or other restriction (including, + but not limited to, related patents and trademarks) of which you + are personally aware and which are associated with any part of Your + Contributions. + +6. You are not expected to provide support for Your Contributions, + except to the extent You desire to provide support. You may provide + support for free, for a fee, or not at all. Unless required by + applicable law or agreed to in writing, You provide Your + Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS + OF ANY KIND, either express or implied, including, without + limitation, any warranties or conditions of TITLE, NON- + INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + +7. Should You wish to submit work that is not Your original creation, + You may submit it to the HOLDERS separately from any + Contribution, identifying the complete details of its source and of + any license or other restriction (including, but not limited to, + related patents, trademarks, and license agreements) of which you + are personally aware, and conspicuously marking the work as + "Submitted on behalf of a third-party: [named here]". + +8. You agree to notify the HOLDERS of any facts or circumstances of + which you become aware that would make these representations + inaccurate in any respect. diff --git a/.cla/version1/signatures.json b/.cla/version1/signatures.json new file mode 100644 index 0000000..5719751 --- /dev/null +++ b/.cla/version1/signatures.json @@ -0,0 +1,20 @@ +{ + "signedContributors": [ + { + "name": "bastonero", + "id": 79980269, + "comment_id": 2343829130, + "created_at": "2024-09-11T14:22:56Z", + "repoId": 390674389, + "pullRequestNo": 78 + }, + { + "name": "vdemestral", + "id": 114935148, + "comment_id": 2580462168, + "created_at": "2025-01-09T14:49:31Z", + "repoId": 390674389, + "pullRequestNo": 67 + } + ] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9774839..13c101d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: strategy: matrix: - python-version: ['3.10'] + python-version: ['3.10','3.12'] services: rabbitmq: @@ -68,4 +68,13 @@ jobs: - name: Run pytest env: AIIDA_WARN_v3: 1 - run: pytest -sv tests + run: pytest -sv --cov aiida_vibroscopy tests + + - name: Upload to Codecov + if: matrix.python-version == 3.10 + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + name: pytests-vibroscopy3.10 + flags: pytests + fail_ci_if_error: true diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 0000000..72ac62d --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,55 @@ +name: "CLA Assistant" +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened, closed, synchronize] + +# explicitly configure permissions, in case your GITHUB_TOKEN workflow permissions are set to read-only in repository settings +permissions: + actions: write + contents: write + pull-requests: write + statuses: write + +jobs: + CLAAssistant: + runs-on: ubuntu-latest + steps: + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.PRIVATE_KEY }} + + - uses: contributor-assistant/github-action@v2.4.0 + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby accept the CLA') || github.event_name == 'pull_request_target' + env: + # the default github token does not allow github action to create & push commit, + # instead, we need to use a github app to generate a token + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + # the below token should have repo scope and must be manually added by you in the repository's secret + # This token is required only if you have configured to store the signatures in a remote repository/organization + # PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + with: + path-to-signatures: ".cla/version1/signatures.json" + path-to-document: "https://github.com/bastonero/aiida-vibroscopy/blob/main/.cla/version1/CLA.md" + # branch should not be protected + branch: "main" + allowlist: bot* + + # the followings are the optional inputs - If the optional inputs are not given, then default values will be taken + #remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository) + #remote-repository-name: enter the remote repository name where the signatures should be stored (Default is storing the signatures in the same repository) + #create-file-commit-message: 'For example: Creating file for storing CLA Signatures' + #signed-commit-message: 'For example: $contributorName has signed the CLA in $owner/$repo#$pullRequestNo' + custom-notsigned-prcomment: 'Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you accept our [Contributor License Agreement](https://github.com/bastonero/aiida-vibroscopy/blob/main/.cla/version1/CLA.md) before we can merge your contribution. You can accept the CLA by just copying the sentence below and posting it as a Pull Request Comment.' + custom-pr-sign-comment: 'I have read the CLA Document and I hereby accept the CLA' + custom-allsigned-prcomment: | + All contributors have accepted the CLA ✅ + + --- + You might need to click the "Update/Rebase branch" button to update the pull request and rerun the GitHub actions to pass the CLA check. + #lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true) + #use-dco-flag: true - If you are using DCO instead of CLA diff --git a/.github/workflows/update_changelog.py b/.github/workflows/update_changelog.py new file mode 100644 index 0000000..9479f44 --- /dev/null +++ b/.github/workflows/update_changelog.py @@ -0,0 +1,85 @@ +#!/bin/bash +# -*- coding: utf-8 -*- +"""Script for automatically updating the `CHANGELOG.md` based on the commits since the latest release tag.""" +from pathlib import Path +import re +import subprocess + +DEFAULT_CHANGELOG_SECTIONS = """ +### ‼️ Breaking changes + + +### ✨ New features + + +### 🗑️ Deprecations + + +### 👌 Improvements + + +### 🐛 Bug fixes + + +### 📚 Documentation + + +### 🔧 Maintenance + + +### ⬆️ Update dependencies + + +### ♻️ Refactor + +""" + + +def update_changelog(): + """Update the `CHANGELOG.md` for a first draft of the release.""" + print('🔍 Checking the current version number') + current_changelog = Path('CHANGELOG.md').read_text(encoding='utf-8') + + from aiida_vibroscopy import __version__ + + if str(__version__) in current_changelog: + print('🛑 Current version already in `CHANGELOG.md`. Skipping...') + return + + print('⬆️ Found updated version number, adapting `CHANGELOG.md`.') + tags = subprocess.run(['git', 'tag', '--sort=v:refname'], capture_output=True, check=True, encoding='utf-8').stdout + latest_tag = re.findall(r'(v\d\.\d\.\d)\n', tags)[-1] + + print(f'🔄 Comparing with latest tag `{latest_tag}`.') + commits = subprocess.run(['git', 'log', "--pretty=format:'%h|%H|%s'", f'{latest_tag}..origin/main'], + capture_output=True, + check=True, + encoding='utf-8').stdout + + pr_pattern = re.compile(r'\(\S(?P\d+)\)') + + changelog_message = f'## v{__version__}\n' + DEFAULT_CHANGELOG_SECTIONS + + for commit in commits.splitlines(): + + # Remove the PR number from the commit message + pr_match = pr_pattern.search(commit) + + if pr_match is not None: + pr_number = pr_match.groupdict()['pr_number'] + commit = commit.replace(fr'(#{pr_number})', '') + + # Add the commit hash (short) to link to the changelog + commit = commit.strip("'") + hash_short, hash_long, message = commit.split('|', maxsplit=2) + message += f'[[{hash_short}](https://github.com/aiidateam/aiida-quantumespresso/commit/{hash_long})]' + changelog_message += f'\n* {message}' + + with Path('CHANGELOG.md').open('w', encoding='utf8') as handle: + handle.write(changelog_message + '\n\n' + current_changelog) + + print("🚀 Success! Finalise the `CHANGELOG.md` and let's get this baby released.") + + +if __name__ == '__main__': + update_changelog() diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0df42a4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,85 @@ +## v1.1.1 + +This minor release adds the new AiiDA contributor license agreement (CLA), and its GitHub bot, +along with some dependency contraints for phonopy. The latest versions of phonopy (>v2.26) +break the tests. While figuring out why, we patch this until a solution is found. + +### 🐛 Bug fixes + +* Deps: constrain phonopy and spglib versions [[3a3e3d1](https://github.com/aiidateam/aiida-quantumespresso/commit/3a3e3d117e34c6a66fcdc74e1e21c6263c203565)] + +### 📚 Documentation + +* Fix some docstrings and reports [[3ee9e7c](https://github.com/aiidateam/aiida-quantumespresso/commit/3ee9e7cbd2f5e6b8f15229dafbed58ae7ef4fa0d)] +* Update main paper reference[[504c1b7](https://github.com/aiidateam/aiida-quantumespresso/commit/504c1b7b65a8852395d0ff3ec7271cb8c05c6931)] + +### 🔧 Maintenance + +* CLA: update and remove old cla-bot [[32bd829](https://github.com/aiidateam/aiida-quantumespresso/commit/32bd829987751deba056b7bfa739f6c82cf89d3e)] +* @bastonero has signed the CLA in bastonero/aiida-vibroscopy#78[[e83739f](https://github.com/aiidateam/aiida-quantumespresso/commit/e83739f6aaecfcb304f8cac3da6d54b93f0fafb7)] +* Add the AiiDA CLA [[df2cade](https://github.com/aiidateam/aiida-quantumespresso/commit/df2cade1bf200b8a2dd7004a48e40b118257f134)] +* Add CLA bot [[3ba3e9e](https://github.com/aiidateam/aiida-quantumespresso/commit/3ba3e9e9f094106254b1a8ee4c97b85e66b41f85)] + +### ⬆️ Update dependencies + +* Deps: constrain phonopy and spglib versions [[3a3e3d1](https://github.com/aiidateam/aiida-quantumespresso/commit/3a3e3d117e34c6a66fcdc74e1e21c6263c203565)] + + + + +## v1.1.0 + +This minor release includes new post-processing utilities, a small breaking change in [[42503f3]](https://github.com/bastonero/aiida-vibroscopy/commit/42503f312d9a812cfc46d4c4a03a78641201e1d3) with regards to reference system for non-analytical and polarization directions. Some examples providing +a unique python script to run the `IRamanSpectraWorkChain` are also added to help new users to get started. The license terms are also updated. +A CHANEGELOG file is finally added to keep track in a pretty format of the changes among releases of the code. + +The new post-processing utilities can be used directly through a `VibrationalData` node, in a similar fashion to the other methods. +For instance, to compute the complex dielectric matrix and the normal reflectivity in the infrared regime: + +```python +node = load_node(PK) # PK to a VibrationalData node + +complex_dielectric = node.run_complex_dielectric_function() # (3,3,num_steps) shape complex array; num_steps are the number of frequency points where the function is evaluated +reflectivity = node.run_normal_reflectivity_spectrum([0,0,1]) # (frequency points, reflectance value), [0,0,1] is the orthogonal direction index probed via q.eps.q +``` + +Now, the polarization and non-analytical directions in _all_ methods in aiida-vibroscopy should be given in Cartesian coordinates: + +```python +node = load_node(PK) # PK to a VibrationalData node + +scattering_geometry = dict(pol_incoming=[1,0,0], pol_outgoing=[1,0,0], nac_direction=[0,0,1]) # corresponding to ZXXZ scattering setup +intensities, frequencies, mode_symmetry_labels = node.run_single_crystal_raman_intensities(**scattering_geometry) +``` + +### ‼️ Breaking changes + +* Post-processing: polarization and nac directions in Cartesian coordinates [[42503f3]](https://github.com/bastonero/aiida-vibroscopy/commit/42503f312d9a812cfc46d4c4a03a78641201e1d3) + +### 👌 Improvements + +* Post-processing: computation of complex dielectric function and normal reflectivity in the infrared [[42503f3]](https://github.com/bastonero/aiida-vibroscopy/commit/42503f312d9a812cfc46d4c4a03a78641201e1d3) +* `Examples`: new folder with working examples for different use cases to get new users started [[7deb31b]](https://github.com/bastonero/aiida-vibroscopy/commit/7deb31b5f547ca16e4522be960b4aa5bbe13fccf) +* CI: add codecov step [[f36e8a1]](https://github.com/bastonero/aiida-vibroscopy/commit/f36e8a10566af68843546bae428560dff393aaf1) + +### 🐛 Bug Fixes + +* `Docs`: fix typos [[85b1830]](https://github.com/bastonero/aiida-vibroscopy/commit/85b18305be6e7e76efce35d9e4ae4c5a3547f9bc), [[e924b3d]](https://github.com/bastonero/aiida-vibroscopy/commit/e924b3dd436a67192f6c0780ff3a318581ab1fc5) +* Post-processing: fix coordinates and units [[42503f3]](https://github.com/bastonero/aiida-vibroscopy/commit/42503f312d9a812cfc46d4c4a03a78641201e1d3) + +### 📚 Documentation + +* Set correct hyperlink for AiiDA paper [[c92994d]](https://github.com/bastonero/aiida-vibroscopy/commit/c92994de36c336a265ac262eea2dc8d77fb11f08) + +### 🔧 Maintenance + +* Adapt tests also for other changes [[be3a6b7]](https://github.com/bastonero/aiida-vibroscopy/commit/be3a6b7d67926816957634fd7b520cd021532f0f) +* Add loads of tests [[42503f3]](https://github.com/bastonero/aiida-vibroscopy/commit/42503f312d9a812cfc46d4c4a03a78641201e1d3) +* `README`: add more information and badges [[c92994d]](https://github.com/bastonero/aiida-vibroscopy/commit/c92994de36c336a265ac262eea2dc8d77fb11f08) +* Docs: Remove aiida.manage.configuration.load_documentation_profile [[f914cbb]](https://github.com/bastonero/aiida-vibroscopy/commit/f914cbb5460d4f988dd117628890a8f53f1c976a) +* DevOps: update docs dependencies [[a0d124e]](https://github.com/bastonero/aiida-vibroscopy/commit/a0d124ee24cb287f9d90583b389f38d6b6265b9e) +* Bump SSSP version to 1.3 in tests [[94c72e5]](https://github.com/bastonero/aiida-vibroscopy/commit/94c72e5183584af08d9874fe2b6fc2ad41fce1b5) + +### ⬆️ Update dependencies + +* DevOps: update docs dependencies [[a0d124e]](https://github.com/bastonero/aiida-vibroscopy/commit/a0d124ee24cb287f9d90583b389f38d6b6265b9e) diff --git a/LICENSE.txt b/LICENSE.txt index 6fc03b8..8abd704 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,27 +1,97 @@ Non-Commercial, End-User Software License Agreement for AiiDA-Vibroscopy +Copyright (c), 2021-2024, University of Bremen, Germany (U Bremen Excellence Chair), +École Polytechnique Fédérale de Lausanne (Theory and Simulation of Materials (THEOS) +and National Centre for Computational Design and Discovery of Novel Materials +(NCCR MARVEL)), Switzerland and Paul Scherrer Institut (Laboratory for Materials + Simulations (LMS)), Switzerland. All rights reserved. + INTRODUCTION -- This license agreement sets forth the terms and conditions under which the Authors and their Insitutions (hereafter "LICENSORS") of the program "AiiDA-Vibroscopy" (hereafter "PROGRAM"), will grant you (hereafter "LICENSEE") a fully-paid, non-exclusive, and non-transferable license for academic, non-commercial purposes only (hereafter “LICENSE”) to use the PROGRAM computer software and associated documentation furnished hereunder. +- This license agreement sets forth the terms and conditions under which the + Authors and their Institutions (hereafter "LICENSORS") of the program +"AiiDA-Vibroscopy" (hereafter "PROGRAM"), will grant you (hereafter +"LICENSEE") a fully-paid, non-exclusive, and non-transferable license for +academic, non-commercial purposes only (hereafter "LICENSE") to use the PROGRAM +computer software and associated documentation furnished hereunder. -- LICENSEE acknowledges that the PROGRAM is a research tool still in the development stage, that is being supplied "as is", without any related services, improvements or warranties from LICENSORS and that this license is entered into in order to enable others to utilize the PROGRAM in their academic activities. +- LICENSEE acknowledges that the PROGRAM is a research tool still in the + development stage, that is being supplied "as is", without any related +services, improvements or warranties from LICENSORS and that this license is +entered into in order to enable others to utilize the PROGRAM in their academic +activities. TERMS AND CONDITIONS OF THE LICENSE -1. LICENSORS grants to LICENSEE a fully-paid up, non-exclusive, and non-transferable license to use the PROGRAM for academic, non-commercial purposes, upon the terms and conditions hereinafter set out and until termination of this license as set forth below. +1. LICENSORS grant to LICENSEE a fully-paid up, non-exclusive, and +non-transferable license to use the PROGRAM for academic, non-commercial +purposes, upon the terms and conditions hereinafter set out and until +termination of this license as set forth below. -2. LICENSEE acknowledges that the PROGRAM is a research tool still in the development stage. The PROGRAM is provided "as is", without any related services or improvements from LICENSORS and that the LICENSE is entered into in order to enable others to utilize the PROGRAM in their academic activities. +2. LICENSEE acknowledges that the PROGRAM is a research tool still in the +development stage. The PROGRAM is provided "as is", without any related +services or improvements from LICENSORS and that the LICENSE is entered into in +order to enable others to utilize the PROGRAM in their academic activities. -3. LICENSORS MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY REPRESENTATIONS OR WARRANTIES OF MERCHANTABILITY OR FITNESS FOR PARTICULAR PURPOSE OR THAT THE USE OF THE PROGRAM WILL NOT INFRINGE ANY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. LICENSORS shall not be liable for any direct, indirect or consequential damages with respect to any claim by LICENSEE or any third party arising from this Agreement or use of the PROGRAM. +3. LICENSORS MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, +INCLUDING WITHOUT LIMITATION ANY REPRESENTATIONS OR WARRANTIES OF +MERCHANTABILITY OR FITNESS FOR PARTICULAR PURPOSE OR THAT THE USE OF THE +PROGRAM WILL NOT INFRINGE ANY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. +LICENSORS shall not be liable for any direct, indirect or consequential damages +with respect to any claim by LICENSEE or any third party arising from this +Agreement or use of the PROGRAM. -4. LICENSEE agrees that it will use the PROGRAM, and any modifications, improvements, or derivatives to PROGRAM that LICENSEE may create (collectively, "IMPROVEMENTS") solely for academic, non-commercial purposes and shall not distribute or transfer the PROGRAM or any IMPROVEMENTS to any person without prior written permission from LICENSORS. The terms "academic, non-commercial", as used in this Agreement, mean academic or other scholarly research which (a) is not undertaken for profit, or (b) is not intended to produce works, services, or data for commercial use, or (c) is neither conducted, nor funded, by a person or an entity engaged in the commercial use, application or exploitation of works similar to the PROGRAM. +4. LICENSEE agrees that it will use the PROGRAM, and any modifications, +improvements, or derivatives to PROGRAM that LICENSEE may create (collectively, +"IMPROVEMENTS") solely for academic, non-commercial purposes and shall not +distribute or transfer the PROGRAM or any IMPROVEMENTS to any person without +prior written permission from LICENSORS. The terms "academic, non-commercial", +as used in this Agreement, mean academic or other scholarly research which (a) +is not undertaken for profit, or (b) is not intended to produce works, +services, or data for commercial use, or (c) is neither conducted, nor funded, +by a person or an entity engaged in the commercial use, application or +exploitation of works similar to the PROGRAM. -5. LICENSEE agrees that he/she shall make the following acknowledgement in publications resulting from the use of the PROGRAM : "Lorenzo Bastonero and Nicola Marzari, Automated all-functional infrared and Raman spectra, XXX, YYY (2023), DOI: ZZZ" (LINK TO DOCUMENTATION); "Sebastiaan. P. Huber, Spyros Zoupanos, Martin Uhrin, Leopold Talirz, Leonid Kahle, Rico Häuselmann, Dominik Gresch, Tiziano Müller, Aliaksandr V. Yakutovich, Casper W. Andersen, Francisco F. Ramirez, Carl S. Adorf, Fernando Gargiulo, Snehal Kumbhar, Elsa Passaro, Conrad Johnston, Andrius Merkys, Andrea Cepellotti, Nicolas Mounet, Nicola Marzari, Boris Kozinsky, and Giovanni Pizzi, AiiDA 1.0, a scalable computational infrastructure for automated reproducible workflows and data provenance, Scientific Data 7, 300 (2020), DOI: 10.1038/s41597-020-00638-4" (http://www.aiida.net), plus any additional reference explicitly mention in the custom workflow used. Except for the above-mentioned acknowledgment, LICENSEE shall not use the PROGRAM title or the names or logos of LICENSORS, nor any adaptation thereof, nor the names of any of its employees or laboratories, in any advertising, promotional or sales material without prior written consent obtained from LICENSORS in each case. +5. LICENSEE agrees that he/she shall make the following acknowledgement in +publications resulting from the use of the PROGRAM: "Lorenzo Bastonero and +Nicola Marzari, Automated all-functionals infrared and Raman spectra, +npj Computational Materials 10, 55 (2024), DOI: 10.1038/s41524-024-01236-3"; +"Sebastiaan. P. Huber, Spyros Zoupanos, Martin Uhrin, Leopold Talirz, Leonid +Kahle, Rico Häuselmann, Dominik Gresch, Tiziano Müller, Aliaksandr V. +Yakutovich, Casper W. Andersen, Francisco F. Ramirez, Carl S. Adorf, Fernando +Gargiulo, Snehal Kumbhar, Elsa Passaro, Conrad Johnston, Andrius Merkys, +Andrea Cepellotti, Nicolas Mounet, Nicola Marzari, Boris Kozinsky, and +Giovanni Pizzi, AiiDA 1.0, a scalable computational infrastructure for +automated reproducible workflows and data provenance, +Scientific Data 7, 300 (2020), DOI: 10.1038/s41597-020-00638-4" +(http://www.aiida.net), plus any additional reference explicitly mentioned in +the custom workflow used. Except for the above-mentioned acknowledgment, +LICENSEE shall not use the PROGRAM title or the names or logos of LICENSORS, +nor any adaptation thereof, nor the names of any of its employees or +laboratories, in any advertising, promotional or sales material without prior +written consent obtained from LICENSORS in each case. -6. Ownership of all rights, including copyright in the PROGRAM and in any material associated therewith, shall at all times remain with LICENSORS and LICENSEE agrees to preserve same. LICENSEE agrees not to use any portion of the PROGRAM or of any IMPROVEMENTS in any machine-readable form outside the PROGRAM, nor to make any copies except for its internal use, without prior written consent of LICENSORS. LICENSEE agrees to place the following copyright notice on any such copies: © All rights reserved. University of Bremen, Germany, U Bremen Excellence Chair, 2023; Authors: Lorenzo Bastonero. Paul Scherrer Institut, Switzerland, Laboratory of Materials Simulations, 2023; Authors: Giovanni Pizzi. ECOLE POLYTECHNIQUE FEDERALE DE LAUSANNE, Switzerland, Laboratory of Theory and Simulation of Materials (THEOS), 2023; Authors: Nicola Marzari. +6. Ownership of all rights, including copyright in the PROGRAM and in any +material associated therewith, shall at all times remain with LICENSORS, and +LICENSEE agrees to preserve the same. LICENSEE agrees not to use any portion of +the PROGRAM or of any IMPROVEMENTS in any machine-readable form outside the +PROGRAM, nor to make any copies except for its internal use, without prior +written consent of LICENSORS. LICENSEE agrees to place the following copyright +notice on any such copies: © All rights reserved. University of Bremen, Germany, +U Bremen Excellence Chair, 2024; Authors: Lorenzo Bastonero and Nicola Marzari. +École Polytechnique Fédérale de Lausanne, Switzerland, Laboratory of Theory +and Simulation of Materials (THEOS), 2024; Authors: Nicola Marzari. +Paul Scherrer Institut, Switzerland, Laboratory for Materials Simulations +(LMS), 2024; Authors: Giovanni Pizzi and Nicola Marzari. -7. The LICENSE shall not be construed to confer any rights upon LICENSEE by implication or otherwise except as specifically set forth herein. +7. The LICENSE shall not be construed to confer any rights upon LICENSEE by +implication or otherwise except as specifically set forth herein. -8. This Agreement shall be governed by the material laws of Switzerland and any dispute arising out of this Agreement or use of the PROGRAM shall be brought before the courts of Lausanne, Switzerland. +8. This Agreement shall be governed by the material laws of Switzerland and any +dispute arising out of this Agreement or use of the PROGRAM shall be brought +before the courts of Lausanne, Switzerland. -9. This Agreement and the LICENSE shall remain effective until expiration of the copyrights of the PROGRAM except that upon any breach of this Agreement by LICENSEE, LICENSORS shall have the right to terminate the LICENSE immediately upon notice to LICENSEE. +9. This Agreement and the LICENSE shall remain effective until expiration of +the copyrights of the PROGRAM except that upon any breach of this Agreement by +LICENSEE, LICENSORS shall have the right to terminate the LICENSE immediately +upon notice to LICENSEE. diff --git a/README.md b/README.md index 7c7454f..359a6a8 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,19 @@ # aiida-vibroscopy -[![PyPI version](https://badge.fury.io/py/aiida-vibroscopy.svg)](https://badge.fury.io/py/aiida-vibroscopy) -[![PyPI pyversions](https://img.shields.io/pypi/pyversions/aiida-vibroscopy.svg)](https://pypi.python.org/pypi/aiida-vibroscopy) -[![Build Status](https://github.com/bastonero/aiida-vibroscopy/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/bastonero/aiida-vibroscopy/actions) -[![Docs status](https://readthedocs.org/projects/aiida-vibroscopy/badge)](http://aiida-vibroscopy.readthedocs.io/) AiiDA plugin that uses finite displacements and fields -to compute phonon properties, dielectric and -Born effective charges tensors, Infrared and Raman spectra. +to compute phonon properties, dielectric, Born effective charges, + Raman and non-linear optical susceptibility tensors, +coming with lots of post-processing tools to compute infrared and +Raman spectra in different settings. + +| | | +|-----|----------------------------------------------------------------------------| +|Latest release| [![PyPI version](https://badge.fury.io/py/aiida-vibroscopy.svg)](https://badge.fury.io/py/aiida-vibroscopy)[![PyPI pyversions](https://img.shields.io/pypi/pyversions/aiida-vibroscopy.svg)](https://pypi.python.org/pypi/aiida-vibroscopy) | +|References| [![Static Badge](https://img.shields.io/badge/npj%20comp.%20mat.%20-%20implementation%20-%20purple?style=flat)](https://www.nature.com/articles/s41524-024-01236-3) | +|Getting help| [![Docs status](https://readthedocs.org/projects/aiida-vibroscopy/badge)](http://aiida-vibroscopy.readthedocs.io/) [![Discourse status](https://img.shields.io/discourse/status?server=https%3A%2F%2Faiida.discourse.group%2F)](https://aiida.discourse.group/) +|Build status| [![Build Status](https://github.com/bastonero/aiida-vibroscopy/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/bastonero/aiida-vibroscopy/actions) [![Coverage Status](https://codecov.io/gh/bastonero/aiida-vibroscopy/branch/main/graph/badge.svg)](https://codecov.io/gh/bastonero/aiida-vibroscopy) | +|Activity| [![PyPI-downloads](https://img.shields.io/pypi/dm/aiida-vibroscopy.svg?style=flat)](https://pypistats.org/packages/aiida-vibroscopy) [![Commit Activity](https://img.shields.io/github/commit-activity/m/bastonero/aiida-vibroscopy.svg)](https://github.com/bastonero/aiida-vibroscopy/pulse) +|Community| [![Discourse](https://img.shields.io/discourse/topics?server=https%3A%2F%2Faiida.discourse.group%2F&logo=discourse)](https://aiida.discourse.group/) ## Installation To install from PyPI, simply execute: @@ -18,6 +25,40 @@ or when installing from source: git clone https://github.com/bastonero/aiida-vibrosopy pip install . +## Command line interface tool +The plugin comes with a builtin CLI tool: `aiida-vibroscopy`. +For example, the following command should print: + +```console +> aiida-vibroscopy launch --help +Usage: aiida-vibroscopy launch [OPTIONS] COMMAND [ARGS]... + + Launch workflows. + +Options: + -v, --verbosity [notset|debug|info|report|warning|error|critical] + Set the verbosity of the output. + -h, --help Show this message and exit. + +Commands: + dielectric Run an `DielectricWorkChain`. + harmonic Run a `HarmonicWorkChain`. + iraman-spectra Run an `IRamanSpectraWorkChain`. + phonon Run an `PhononWorkChain`. +``` + +## How to cite + +If you use this plugin for your research, please cite the following works: + +* L. Bastonero and N. Marzari, [*Automated all-functionals infrared and Raman spectra*](https://doi.org/10.1038/s41524-024-01236-3), npj Computational Materials **10**, 55 (2024) + +* S. P. Huber _et al._, [*AiiDA 1.0, a scalable computational infrastructure for automated reproducible workflows and data provenance*](https://doi.org/10.1038/s41597-020-00638-4), Scientific Data **7**, 300 (2020) + +* M. Uhrin _et al._, [*Workflows in AiiDA: Engineering a high-throughput, event-based engine for robust and modular computational workflows*](https://www.sciencedirect.com/science/article/pii/S0010465522001746), Computational Materials Science **187**, 110086 (2021) + +Please, also cite the underlying Quantum ESPRESSO and Phonopy codes references. + ## License The `aiida-vibroscopy` plugin package is released under a special academic license. See the `LICENSE.txt` file for more details. diff --git a/codecov.yaml b/codecov.yaml new file mode 100644 index 0000000..4abd24a --- /dev/null +++ b/codecov.yaml @@ -0,0 +1,12 @@ +coverage: + status: + project: + default: + target: 75% + threshold: 0.2% + patch: + default: + target: 70% + threshold: 0.2% + ignore: + - "src/aiida_vibroscopy/cli/*" diff --git a/docs/source/citeus.md b/docs/source/citeus.md index b15f161..dd83723 100644 --- a/docs/source/citeus.md +++ b/docs/source/citeus.md @@ -4,8 +4,10 @@ If you use this plugin for your research, please cite the following works: -> Lorenzo Bastonero and Nicola Marzari, [*Automated all-functionals infrared and Raman spectra*](https://arxiv.org/abs/2308.04308) (2023) +> Lorenzo Bastonero and Nicola Marzari, [*Automated all-functionals infrared and Raman spectra*](https://doi.org/10.1038/s41524-024-01236-3), npj Computational Materials **10**, 55 (2024) > Sebastiaan. P. Huber, Spyros Zoupanos, Martin Uhrin, Leopold Talirz, Leonid Kahle, Rico Häuselmann, Dominik Gresch, Tiziano Müller, Aliaksandr V. Yakutovich, Casper W. Andersen, Francisco F. Ramirez, Carl S. Adorf, Fernando Gargiulo, Snehal Kumbhar, Elsa Passaro, Conrad Johnston, Andrius Merkys, Andrea Cepellotti, Nicolas Mounet, Nicola Marzari, Boris Kozinsky, and Giovanni Pizzi, [*AiiDA 1.0, a scalable computational infrastructure for automated reproducible workflows and data provenance*](https://doi.org/10.1038/s41597-020-00638-4), Scientific Data **7**, 300 (2020) > Martin Uhrin, Sebastiaan. P. Huber, Jusong Yu, Nicola Marzari, and Giovanni Pizzi, [*Workflows in AiiDA: Engineering a high-throughput, event-based engine for robust and modular computational workflows*](https://www.sciencedirect.com/science/article/pii/S0010465522001746), Computational Materials Science **187**, 110086 (2021) + +Please, also cite the underlying Quantum ESPRESSO and Phonopy codes references. diff --git a/docs/source/commands.md b/docs/source/commands.md new file mode 100644 index 0000000..d372198 --- /dev/null +++ b/docs/source/commands.md @@ -0,0 +1,9 @@ +(commands)= + +# Commands (CLI) + +```{eval-rst} +.. click:: aiida_vibroscopy.cli:cmd_root + :prog: aiida-vibroscopy + :show-nested: +``` diff --git a/docs/source/conf.py b/docs/source/conf.py index 2d32557..ab38727 100755 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,17 +10,15 @@ import pathlib import time -# Load the dummy profile even if we are running locally, this way the documentation will succeed even if the current -# default profile of the AiiDA installation does not use a Django backend. -from aiida.manage.configuration import load_documentation_profile +from aiida.manage.configuration import Profile, load_profile + +load_profile(Profile('docs', {'process_control': {}, 'storage': {}})) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. import aiida_vibroscopy -load_documentation_profile() - # -- Project information ----------------------------------------------------- project = 'aiida-vibroscopy' @@ -51,6 +49,7 @@ 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', 'sphinx_copybutton', + 'sphinx_click.ext', 'sphinx_togglebutton', 'sphinx_design', 'aiida.sphinxext', diff --git a/docs/source/howto/overrides.md b/docs/source/howto/overrides.md index f653d3d..8d9e87c 100644 --- a/docs/source/howto/overrides.md +++ b/docs/source/howto/overrides.md @@ -1,6 +1,6 @@ (howto-overrides)= -# How-to use protocols and overrides +# Protocols and overrides :::{important} The following how-to assume that you are familiar with submitting the workflows of the package. @@ -17,7 +17,7 @@ as you probably noticed in the [tutorials](tutorials). Not necesseraly all the WorkChains have the `get_builder_from_protocol`. E.g. The {class}`~aiida_vibroscopy.workflows.dielectric.numerical_derivatives`. ::: -## How to use in general +## General usage Depending on the WorkChain, you will need to specify some minimal, _but required_, inputs. Here is an example: @@ -48,7 +48,7 @@ Out[1]: ``` -## How to use overrides (beginner) +## Overrides (beginner) As stated at the beginning, you might need to tweak your builder inputs before submitting. Instead of modifying the inputs via the builder, you can do it via `overrides`. The way the overrides must be secified __is WorkChain dependent__. The structure should be the same as the input specification of the specific WorkChain, so always refer to the inputs of the particular workflow, which you can find [here](topics-workflows). @@ -169,7 +169,7 @@ builder = DielectricWorkChain.get_builder_from_protocol(code=code, structure=str You got the gist, so it will be the same for the `phonon` inputs, which corresponds to the `PhononWorkChain`. -## How to overrides (advanced) +## Overrides (advanced) Once you got used to using the overrides, you will notice your python script will become quite a mess. The idea is to write the overrides in a separate file, using for example the convenient `YAML` format, @@ -199,7 +199,7 @@ phonon: scf: pw: kpoints_distance: 0.2 - pw: + parameters: ... dielectric: central_difference: @@ -209,7 +209,7 @@ dielectric: ... ``` -## Get automated inputs for insulators, metals, and magnetism +## Automated inputs for insulators, metals, and magnetism Inputs might change depending on the nature of the material: insulating, metallic, a form of magnetism. This can be the need of specifying a smearing or a magnetic configuration. diff --git a/docs/source/howto/postprocess.md b/docs/source/howto/postprocess.md index 53c5d2d..674df8a 100644 --- a/docs/source/howto/postprocess.md +++ b/docs/source/howto/postprocess.md @@ -1,6 +1,6 @@ (howto-postprocess)= -# How-to post-process data +# Post-process data Here you will find some _how-tos_ on how to post-process the `VibrationalData` and `PhonopyData`. These can be generated via the `HarmonicWorkChain`, the `IRamanSpectraWorkChain` or the `PhononWorkChain`. @@ -10,13 +10,12 @@ Please, have a look at the [tutorials](tutorials) for an overview, and at the sp You can always access to the detailed description of a function/method putting a `?` question mark in front of the function and press enter. This will print the description of the function, with detailed information on inputs, outputs and purpose. -``` {note} + A rendered version can also be found in the documentation referring to the dedicated [API section](reference). -``` ::: -## How to get powder spectra +## Powder spectra You can get the powder infrared and/or Raman spectra from a computed {{ vibrational_data }}. For Raman: @@ -28,8 +27,8 @@ vibro = load_node(IDENTIFIER) # a VibrationalData node polarized_intensities, depolarized_intensities, frequencies, labels = vibro.run_powder_raman_intensities(frequency_laser=532, temperature=300) ``` -The total powder intensity will be the some of the polarized (backscattering geometry) -and depolarized (90º geometry) intensities: +The total powder intensity will be the some of the polarized (backscattering geometry, HH/VV setup) +and depolarized (90º geometry, HV setup) intensities: ```python total = polarized_intensities + depolarized_intensities @@ -44,7 +43,7 @@ intensities, frequencies, labels = vibro.run_powder_ir_intensities() The `labels` output is referring to the irreducible representation labels of the modes. -## How to get single crystal spectra +## Single crystal polarized spectra ::: {important} It is extremely important that you match the experimental conditions, meaning the direction of @@ -53,7 +52,7 @@ on the convention used, both in the code and in the experiments. ::: ::: {note} Convention -In the following methods, the direction of the light must be given in crystal/fractional coordinates. +In the following methods, the direction of the light must be given in **Cartesian coordinates**. ::: You can get the single crystal infrared and/or Raman spectra from a computed {{ vibrational_data }}. For Raman: @@ -86,7 +85,7 @@ incoming = [0,0,1] # light polatization of the incident laser beam intensities, frequencies, labels = vibro.run_single_crystal_ir_intensities(pol_incoming=incoming) ``` -::: {admonition} Cartesian coordinates + -## How to get the IR/Raman active modes +## IR/Raman active modes To get the active modes from a computed {{ vibrational_data }}: diff --git a/docs/source/howto/understand.md b/docs/source/howto/understand.md index 7aeba4e..f4050e7 100644 --- a/docs/source/howto/understand.md +++ b/docs/source/howto/understand.md @@ -1,6 +1,6 @@ (howto-understand)= -# How-to understand the input/builder structure +# Understand the input/builder structure In AiiDA the CalcJobs and WorkChains have usually nested inputs and different options on how to run the calculation and/or workflows. To understand the nested input structure of CalcJobs/Workflows, we made them available in a clickable diff --git a/docs/source/index.md b/docs/source/index.md index 73c87be..ba77356 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -14,6 +14,7 @@ myst: installation/index tutorials/index +commands ``` ```{toctree} @@ -38,7 +39,7 @@ topics/conventions :hidden: true :caption: Reference -reference/index +reference/api/index ``` # AiiDA Vibroscopy @@ -100,13 +101,15 @@ To the tutorials # How to cite -If you use this plugin for your research, please cite the following work: +If you use this plugin for your research, please cite the following works: -> Lorenzo Bastonero and Nicola Marzari, [*Automated all-functionals infrared and Raman spectra*](https://arxiv.org/abs/2308.04308) (2023) +> Lorenzo Bastonero and Nicola Marzari, [*Automated all-functionals infrared and Raman spectra*](https://doi.org/10.1038/s41524-024-01236-3), npj Computational Materials **10**, 55 (2024) > Sebastiaan. P. Huber, Spyros Zoupanos, Martin Uhrin, Leopold Talirz, Leonid Kahle, Rico Häuselmann, Dominik Gresch, Tiziano Müller, Aliaksandr V. Yakutovich, Casper W. Andersen, Francisco F. Ramirez, Carl S. Adorf, Fernando Gargiulo, Snehal Kumbhar, Elsa Passaro, Conrad Johnston, Andrius Merkys, Andrea Cepellotti, Nicolas Mounet, Nicola Marzari, Boris Kozinsky, and Giovanni Pizzi, [*AiiDA 1.0, a scalable computational infrastructure for automated reproducible workflows and data provenance*](https://doi.org/10.1038/s41597-020-00638-4), Scientific Data **7**, 300 (2020) -> Martin Uhrin, Sebastiaan. P. Huber, Jusong Yu, Nicola Marzari, and Giovanni Pizzi, [*Workflows in AiiDA: Engineering a high-throughput, event-based engine for robust and modular computational workflows*](https://www.sciencedirect.com/science/article/pii/S0010465522001746), Computational Materials Science **187**, 110086 (2021) +> Martin Uhrin, Sebastiaan. P. Huber, Jusong Yu, Nicola Marzari, and Giovanni Pizzi, [*Workflows in AiiDA: Engineering a high-throughput, event-based engine for robust and modular computational workflows*](https://doi.org/10.1016/j.commatsci.2020.110086), Computational Materials Science **187**, 110086 (2021) + +Please, also cite the underlying Quantum ESPRESSO and Phonopy codes references. # Acknowledgements diff --git a/docs/source/reference/index.md b/docs/source/reference/api/index.md similarity index 82% rename from docs/source/reference/index.md rename to docs/source/reference/api/index.md index 54541cf..8db6c3b 100644 --- a/docs/source/reference/index.md +++ b/docs/source/reference/api/index.md @@ -1,4 +1,4 @@ -(reference)= +(reference-api)= # Python API @@ -7,5 +7,5 @@ If you only want to get to know the input nested structure of the workflows, ple ```{toctree} :maxdepth: 4 -api/aiida_vibroscopy/index +aiida_vibroscopy/index ``` diff --git a/docs/source/topics/formulation.md b/docs/source/topics/formulation.md index 67896a9..ede5b50 100644 --- a/docs/source/topics/formulation.md +++ b/docs/source/topics/formulation.md @@ -3,7 +3,7 @@ # Formulation In this section we briefly explore the underlying theory and formulation made used in the workflows. -For a more in depth explanation, please refer to the [main paper of the package](https://arxiv.org/abs/2308.04308). +For a more in depth explanation, please refer to the [main paper of the package](https://doi.org/10.1038/s41524-024-01236-3). Considered good sources are: @@ -21,7 +21,7 @@ In the code, all properties are computed within the __Born-Oppenheimer__ and __h The vibrational spectra are computed in the __first-order non-resonant__ regime: the infrared using the __dipole-dipole__ approximation, and the Raman using the __Placzek__ approximation. :::{important} -These are considered __good approximations for insulators__. Nevertheless, a frequency dependent solution form is usually used also for the __resonant__ case and for __metals__. _Nevertheless_, one must be aware that in such cases (resonance, metals) these approximations might not hold, as multiphonon processes, non-adiabaticity, excitonic effects (i.e. electronic excitations), or even exciton-phonon interactions might be non negligible, thus comparison with experiments could result poor. If these effects are important for your case, you can refer to [S. Reichardt and L. Wirtz, _Science Advances_, __7__, 32 (__2020__)](https://www.science.org/doi/10.1126/sciadv.abb5915). +These are considered __good approximations for insulators__. Nevertheless, a frequency dependent solution form is usually used also for the __resonant__ case and for __metals__. _Nevertheless_, one must be aware that in such cases (resonance, metals) these approximations might not hold, as multiphonon processes, non-adiabaticity, excitonic effects (i.e. electronic excitations), or even exciton-phonon interactions might be non negligible, thus comparison with experiments could result poor. If these effects are important for your case, you can refer to [S. Reichardt and L. Wirtz, _Science Advances_, **7**, 32 (__2020__)](https://www.science.org/doi/10.1126/sciadv.abb5915). Moreover, temperature effects can also play a crucial role, as __anharmonic__ effects (of ions) should be incorporate to the phonons. A state-of-the-art approach, which differs from the classical molecular dynamics solutions, can be found using the [time-dependent self-consistent harmonic approximation](https://journals.aps.org/prb/abstract/10.1103/PhysRevB.103.104305). ::: diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..4b48087 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,27 @@ +# General information + +All the examples make use of the `get_builder_from_protocol` to help filling all the necessary information. +This means you need to install the compatible SSSP library via `aiida-pseudo`. + +For instance: + +```console +aiida-pseudo install sssp -v 1.3 -x PBEsol +``` + +If you cannot download, and/or you want to use your own pseudo-potentials, you can still download separately the files on the [_Materials Cloud Archive_](https://archive.materialscloud.org/record/2023.65) (search for _SSSP_ if this link doesn't work). Then, you can proceed by installing a `CutoffsPseudoPotentialFamily`, i.e. a family of pseudo-potentials with pre-defined cutoffs. + +See [here](https://aiida-pseudo.readthedocs.io/en/latest/howto.html#adding-recommended-cutoffs) for further instructions. + +Then you will need to specify the `pseudo_family` you created in the `overrides`. Where to specify it depends on the `WorkChain` you are using. For example, in the case of the `IRamanSpectraWorkChain` the two places are: + +``` +dielectric: + scf: + pseudo_family: 'YourPseudoFamilyWithCutoff' # replace here with the label you created your family +phonon: + scf: + pseudo_family: 'YourPseudoFamilyWithCutoff' # replace here with the label you created your family +``` + +In general, everywhere where the inputs of the `PwBaseWorkChain` are _exposed_. In the previous example, these inputs are exposed under the `scf` namespace. diff --git a/examples/commands/README.md b/examples/commands/README.md new file mode 100644 index 0000000..4083437 --- /dev/null +++ b/examples/commands/README.md @@ -0,0 +1,77 @@ +# General information + +You need to install first a pseudpotential library via `aiida-pseudo` (see examples/README.md). + +You can run workflows using the command `aiida-vibroscopy launch WORKFLOW_NAME`. + +To know the available implemented workflows that can be used via CLI: + +```console +> aiida-vibroscopy launch --help +Usage: aiida-vibroscopy launch [OPTIONS] COMMAND [ARGS]... + + Launch workflows. + +Options: + -v, --verbosity [notset|debug|info|report|warning|error|critical] + Set the verbosity of the output. + -h, --help Show this message and exit. + +Commands: + dielectric Run an `DielectricWorkChain`. + harmonic Run a `HarmonicWorkChain`. + iraman-spectra Run an `IRamanSpectraWorkChain`. + phonon Run an `PhononWorkChain`. +``` + +For furhter information, for instance: + +```console +> aiida-vibroscopy launch dielectric --help +Usage: aiida-vibroscopy launch dielectric [OPTIONS] + + Run an `DielectricWorkChain`. + + It computes dielectric, Born charges, Raman and non-linear optical + susceptibility tensors for a given structure. + +Options: + --pw CODE The code to use for the pw.x executable. + [required] + -S, --structure DATA A StructureData node identified by its ID or + UUID. + -p, --protocol [fast|moderate|precise] + Select the protocol that defines the + accuracy of the calculation. [default: + moderate] + -F, --pseudo-family GROUP Select a pseudopotential family, identified + by its label. + -k, --kpoints-mesh ... + The number of points in the kpoint mesh + along each basis vector and the offset. + Example: `-k 2 2 2 0 0 0`. Specify `0.5 0.5 + 0.5` for the offset if you want to result in + the equivalent Quantum ESPRESSO pw.x `1 1 1` + shift. + -o, --overrides FILENAME The filename or filepath containing the + overrides, in YAML format. + -D, --daemon Submit the process to the daemon instead of + running it and waiting for it to finish. + [default: True] + -v, --verbosity [notset|debug|info|report|warning|error|critical] + Set the verbosity of the output. + -h, --help Show this message and exit. +``` + +## Example + +```console +> aiida-vibroscopy launch dielectric \ + --pw pw@localhost \ # change here with your installed code + -S 12345 \ # replace with you StructureData + --protocol moderate \ # (optional) choose between fast, moderate, precise + --pseudo-family SSSP/1.3/PBEsol/efficiency \ # (optional) change with your favorite + --kpoints-mesh 4 4 4 \ # (optional) it overrides the overrides, in particular removes kpoints_distance and kpoints_parallel_distance + --overrides overrides.yaml \ # (optional) filepath to overrides + --deamon True # (optional) submit to deamon +``` diff --git a/examples/commands/overrides.yaml b/examples/commands/overrides.yaml new file mode 100644 index 0000000..ea5dd2f --- /dev/null +++ b/examples/commands/overrides.yaml @@ -0,0 +1,32 @@ +clean_workdir: false +kpoints_parallel_distance: 0.2 # kpoints distance in Angstrom^-1 to sample the BZ parallel to the electric field. If used, it should help in converging faster the final results +property: raman +# central_difference: # if you know what you are doing, custom numerical derivatives with respect to electric field +# accuracy: 2 +# electric_field_step: 0.0005 +scf: + kpoints_distance: 0.4 # kpoints distance in Angstrom^-1 to sample the BZ + kpoints_force_parity: false + max_iterations: 5 + pw: + settings: + cmdline: ['-nd','1'] + metadata: + options: + max_wallclock_seconds: 43200 + resources: + num_machines: 1 + num_mpiprocs_per_machine: 1 + # queue_name: partition_name # for SLURM + # account: account_name # for SLURM, also for project etc + withmpi: true + parameters: + ELECTRONS: + conv_thr: 2.0e-12 + electron_maxstep: 80 + mixing_beta: 0.4 + SYSTEM: + ecutrho: 240.0 + ecutwfc: 30.0 +settings: + sleep_submission_time: 1.0 diff --git a/examples/scripts/workflows/spectra/overrides.yaml b/examples/scripts/workflows/spectra/overrides.yaml new file mode 100644 index 0000000..4bd41e4 --- /dev/null +++ b/examples/scripts/workflows/spectra/overrides.yaml @@ -0,0 +1,70 @@ +clean_workdir: false # whether to clean the working directiories +dielectric: + clean_workdir: false + kpoints_parallel_distance: 0.2 # kpoints distance in Angstrom^-1 to sample the BZ parallel to the electric field. If used, it should help in converging faster the final results + property: raman + # central_difference: # if you know what you are doing, custom numerical derivatives with respect to electric field + # accuracy: 2 + # electric_field_step: 0.0005 + scf: + kpoints_distance: 0.4 # kpoints distance in Angstrom^-1 to sample the BZ + kpoints_force_parity: false + max_iterations: 5 + pw: + metadata: + options: + max_wallclock_seconds: 43200 + resources: + num_machines: 1 + num_mpiprocs_per_machine: 1 + # queue_name: partition_name # for SLURM + # account: account_name # for SLURM, also for project etc + withmpi: true + parameters: + ELECTRONS: + conv_thr: 2.0e-12 + electron_maxstep: 80 + mixing_beta: 0.4 + SYSTEM: + ecutrho: 240.0 + ecutwfc: 30.0 + settings: + sleep_submission_time: 1.0 +phonon: + clean_workdir: false + displacement_generator: + distance: 0.01 # atomic displacements for phonon calculation, in Angstrom + scf: + kpoints_distance: 0.15 # kpoints distance in Angstrom^-1 to sample the BZ + kpoints_force_parity: false + max_iterations: 5 + pw: + metadata: + options: + max_wallclock_seconds: 43200 + resources: + num_machines: 1 + num_mpiprocs_per_machine: 1 + # queue_name: partition_name # for SLURM + # account: account_name # for SLURM, also for project etc + withmpi: true + settings: + cmdline: ['-nk', '8'] + # gamma_only: True # to use only if KpointsData has only a mesh 1 1 1 0 0 0 (i.e. Gamma not shifted) + parameters: + ELECTRONS: + conv_thr: 2.0e-12 + electron_maxstep: 80 + mixing_beta: 0.4 + SYSTEM: + ecutrho: 240.0 + ecutwfc: 30.0 + settings: + sleep_submission_time: 1.0 # waiting time in seconds between different submission of SCF calculation. Recommended to be at least 1 second, to not overload. +settings: + run_parallel: true + use_primitive_cell: false +symmetry: + distinguish_kinds: false + is_symmetry: true + symprec: 1.0e-05 diff --git a/examples/scripts/workflows/spectra/submit_default.py b/examples/scripts/workflows/spectra/submit_default.py new file mode 100644 index 0000000..d4e84b6 --- /dev/null +++ b/examples/scripts/workflows/spectra/submit_default.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,wildcard-import,pointless-string-statement,unused-wildcard-import +"""Submit an IRamanSpectraWorkChain via the get_builder_from_protocol.""" +from aiida import load_profile +from aiida.engine import submit +from aiida.orm import * +from aiida_quantumespresso.common.types import ElectronicType + +from aiida_vibroscopy.workflows.spectra.iraman import IRamanSpectraWorkChain + +load_profile() + +# =============================== INPUTS =============================== # +# Please, change the following inputs. +pw_code_label = 'pw@localhost' +structure_id = 0 # PK or UUID of your AiiDA StructureData +protocol = 'fast' # also 'moderate' and 'precise'; 'moderate' should be good enough in general +# ====================================================================== # +# If you don't have a StructureData, but you have a CIF or XYZ, or similar, file +# you can import your structure uncommenting the following: +# from ase.io import read +# atoms = read('/path/to/file.cif') +# structure = StructureData(ase=atoms) +# structure.store() +# structure_id = structure.pk +# print(f"Your structure has been stored in the database with PK={structure_id}") + + +def main(): + """Submit an IRamanSpectraWorkChain calculation.""" + code = load_code(pw_code_label) + structure = load_node(structure_id) + kwargs = {'electronic_type': ElectronicType.INSULATOR} + + builder = IRamanSpectraWorkChain.get_builder_from_protocol( + code=code, + structure=structure, + protocol=protocol, + **kwargs, + ) + + calc = submit(builder) + print(f'Submitted IRamanSpectraWorkChain with PK={calc.pk} and UUID={calc.uuid}') + print('Register *at least* the PK number, e.g. in you submit script.') + print('You can monitor the status of your calculation with the following commands:') + print(' * verdi process status PK') + print(' * verdi process list -L IRamanSpectraWorkChain # list all running IRamanSpectraWorkChain') + print( + ' * verdi process list -ap1 -L IRamanSpectraWorkChain # list all IRamanSpectraWorkChain submitted in the previous 1 day' + ) + print('If the WorkChain finishes with exit code 0, then you can inspect the outputs and post-process the data.') + print('Use the command') + print(' * verdi process show PK') + print('To show further information about your WorkChain. When finished, you should see some outputs.') + print('The main output can be accessed via `load_node(PK).outputs.vibrational_data.numerical_accuracy_*`.') + print('You have to complete the remaning `*`, which depends upond the accuracy of the calculation.') + print('See also the documentation and the reference paper for further details.') + + +if __name__ == '__main__': + """Run script.""" + main() diff --git a/examples/scripts/workflows/spectra/submit_default_custom_kpoints.py b/examples/scripts/workflows/spectra/submit_default_custom_kpoints.py new file mode 100644 index 0000000..e222489 --- /dev/null +++ b/examples/scripts/workflows/spectra/submit_default_custom_kpoints.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,wildcard-import,pointless-string-statement,unused-wildcard-import +"""Submit an IRamanSpectraWorkChain via the get_builder_from_protocol with custom kpoints mesh.""" +from aiida import load_profile +from aiida.engine import submit +from aiida.orm import * +from aiida_quantumespresso.common.types import ElectronicType + +from aiida_vibroscopy.workflows.spectra.iraman import IRamanSpectraWorkChain + +load_profile() + +# =============================== INPUTS =============================== # +# Please, change the following inputs. +pw_code_label = 'pw@localhost' +structure_id = 0 # PK or UUID of your AiiDA StructureData +protocol = 'fast' # also 'moderate' and 'precise'; 'moderate' should be good enough in general +mesh = [[2, 2, 2], [0, 0, 0]] # k-point mesh 2x2x2 gamma centered +# kpoints = [[2,2,2], [0.5,0.5,0.5]] # k-point mesh 2x2x2 shifted. Corresponds to 2 2 2 1 1 1 in QE input +# ====================================================================== # +# If you don't have a StructureData, but you have a CIF or XYZ, or similar, file +# you can import your structure uncommenting the following: +# from ase.io import read +# atoms = read('/path/to/file.cif') +# structure = StructureData(ase=atoms) +# structure.store() +# structure_id = structure.pk +# print(f"Your structure has been stored in the database with PK={structure_id}") + + +def main(): + """Submit an IRamanSpectraWorkChain calculation.""" + code = load_code(pw_code_label) + structure = load_node(structure_id) + kwargs = {'electronic_type': ElectronicType.INSULATOR} + + kpoints = KpointsData() + kpoints.set_kpoints_mesh(mesh[0], mesh[1]) + + builder = IRamanSpectraWorkChain.get_builder_from_protocol( + code=code, + structure=structure, + protocol=protocol, + **kwargs, + ) + + builder.dielectric.scf.kpoints = kpoints + builder.dielectric.pop('kpoints_parallel_distance', None) + builder.dielectric.scf.pop('kpoints_distance', None) + builder.phonon.scf.kpoints = kpoints + + calc = submit(builder) + print(f'Submitted IRamanSpectraWorkChain with PK={calc.pk} and UUID={calc.uuid}') + print('Register *at least* the PK number, e.g. in you submit script.') + print('You can monitor the status of your calculation with the following commands:') + print(' * verdi process status PK') + print(' * verdi process list -L IRamanSpectraWorkChain # list all running IRamanSpectraWorkChain') + print( + ' * verdi process list -ap1 -L IRamanSpectraWorkChain # list all IRamanSpectraWorkChain submitted in the previous 1 day' + ) + print('If the WorkChain finishes with exit code 0, then you can inspect the outputs and post-process the data.') + print('Use the command') + print(' * verdi process show PK') + print('To show further information about your WorkChain. When finished, you should see some outputs.') + print('The main output can be accessed via `load_node(PK).outputs.vibrational_data.numerical_accuracy_*`.') + print('You have to complete the remaning `*`, which depends upond the accuracy of the calculation.') + print('See also the documentation and the reference paper for further details.') + + +if __name__ == '__main__': + """Run script.""" + main() diff --git a/examples/scripts/workflows/spectra/submit_default_custom_pseudos.py b/examples/scripts/workflows/spectra/submit_default_custom_pseudos.py new file mode 100644 index 0000000..febb9d9 --- /dev/null +++ b/examples/scripts/workflows/spectra/submit_default_custom_pseudos.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,wildcard-import,pointless-string-statement,unused-wildcard-import +"""Submit an IRamanSpectraWorkChain via the get_builder_from_protocol with custom pseudo potentials.""" +from aiida import load_profile +from aiida.engine import submit +from aiida.orm import * +from aiida_quantumespresso.common.types import ElectronicType + +from aiida_vibroscopy.workflows.spectra.iraman import IRamanSpectraWorkChain + +load_profile() + +# =================== HOW TO STORE CUSTOM PSEUDOS ====================== # +# Please consult also aiida-pseudo documentation. +# Prepare a folder with the pseudopotentials you want to use, with all the elements. +# The format should be ELEMENT.EVENTUAL_DETAILS.UPF . For instance: +# * Si.upf +# * Si.UPF +# * Si.us-v1.0.upf +# * Si.paw-rjjk.v1.3.upf +# Please prepare a folder like: +# -- MyPseudos +# -- |_ Si.upf +# -- |_ O.upf +# -- |_ ... +# Then run +# $ aiida-pseudo install family MyPseudos LABEL -P pseudo.upf +# Substitute LABEL with some significant label referring to the pseudo family you use. +# For instance, good practices: +# * LDA/NC/1.1 +# * PseudoDojo/LDA/US/standard/1.1 +# Consider that you can also install directly well tested pseudo potentials, +# for example from the SSSP library, with the following: +# $ aiida-pseudo install sssp -v 1.3 -x PBEsol -p efficiency +# This will automatically download and store the pseudopotentials in a family. +# Register the name. You can inspect the pseudo potential families you have with +# $ aiida-pseudo list + +# =============================== INPUTS =============================== # +# Please, change the following inputs. +pw_code_label = 'pw@localhost' +structure_id = 0 # PK or UUID of your AiiDA StructureData +protocol = 'fast' # also 'moderate' and 'precise'; 'moderate' should be good enough in general +pseudo_family_name = 'LABEL' # here the LABEL you registered before, or e.g. SSSP/1.3/PBEsol/efficiency for the SSSP example showed +# ====================================================================== # +# If you don't have a StructureData, but you have a CIF or XYZ, or similar, file +# you can import your structure uncommenting the following: +# from ase.io import read +# atoms = read('/path/to/file.cif') +# structure = StructureData(ase=atoms) +# structure.store() +# structure_id = structure.pk +# print(f"Your structure has been stored in the database with PK={structure_id}") + + +def main(): + """Submit an IRamanSpectraWorkChain calculation.""" + code = load_code(pw_code_label) + structure = load_node(structure_id) + kwargs = {'electronic_type': ElectronicType.INSULATOR} + + pseudo_family = load_group(pseudo_family_name) + pseudos = pseudo_family.get_pseudos(structure=structure) + + builder = IRamanSpectraWorkChain.get_builder_from_protocol( + code=code, + structure=structure, + protocol=protocol, + **kwargs, + ) + + builder.dielectric.scf.pw.pseudos = pseudos + builder.phonon.scf.pw.pseudos = pseudos + + calc = submit(builder) + print(f'Submitted IRamanSpectraWorkChain with PK={calc.pk} and UUID={calc.uuid}') + print('Register *at least* the PK number, e.g. in you submit script.') + print('You can monitor the status of your calculation with the following commands:') + print(' * verdi process status PK') + print(' * verdi process list -L IRamanSpectraWorkChain # list all running IRamanSpectraWorkChain') + print( + ' * verdi process list -ap1 -L IRamanSpectraWorkChain # list all IRamanSpectraWorkChain submitted in the previous 1 day' + ) + print('If the WorkChain finishes with exit code 0, then you can inspect the outputs and post-process the data.') + print('Use the command') + print(' * verdi process show PK') + print('To show further information about your WorkChain. When finished, you should see some outputs.') + print('The main output can be accessed via `load_node(PK).outputs.vibrational_data.numerical_accuracy_*`.') + print('You have to complete the remaning `*`, which depends upond the accuracy of the calculation.') + print('See also the documentation and the reference paper for further details.') + + +if __name__ == '__main__': + """Run script.""" + main() diff --git a/examples/scripts/workflows/spectra/submit_with_overrides.py b/examples/scripts/workflows/spectra/submit_with_overrides.py new file mode 100644 index 0000000..dc9df6b --- /dev/null +++ b/examples/scripts/workflows/spectra/submit_with_overrides.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,wildcard-import,pointless-string-statement,unused-wildcard-import +"""Submit an IRamanSpectraWorkChain via the get_builder_from_protocol using the overrides.""" +from pathlib import Path + +from aiida import load_profile +from aiida.engine import submit +from aiida.orm import * +from aiida_quantumespresso.common.types import ElectronicType + +from aiida_vibroscopy.workflows.spectra.iraman import IRamanSpectraWorkChain + +load_profile() + +# =============================== INPUTS =============================== # +# Please, change the following inputs. +pw_code_label = 'pw@localhost' +structure_id = 0 # PK or UUID of your AiiDA StructureData +protocol = 'fast' # also 'moderate' and 'precise'; 'moderate' should be good enough in general +overrides_filepath = './overrides.yaml' # should be a path, e.g. /path/to/overrides.yaml. Format is YAML +# Consult the documentation for HOW-TO for how to use properly the overrides. +# !!!!! FOR FULL INPUT NESTED STRUCTURE: https://aiida-vibroscopy.readthedocs.io/en/latest/topics/workflows/spectra/iraman.html +# You can follow the input structure provided on the website to fill further the overrides. +# ====================================================================== # +# If you don't have a StructureData, but you have a CIF or XYZ, or similar, file +# you can import your structure uncommenting the following: +# from ase.io import read +# atoms = read('/path/to/file.cif') +# structure = StructureData(ase=atoms) +# structure.store() +# structure_id = structure.pk +# print(f"Your structure has been stored in the database with PK={structure_id}") + + +def main(): + """Submit an IRamanSpectraWorkChain calculation.""" + code = load_code(pw_code_label) + structure = load_node(structure_id) + kwargs = {'electronic_type': ElectronicType.INSULATOR} + + builder = IRamanSpectraWorkChain.get_builder_from_protocol( + code=code, + structure=structure, + protocol=protocol, + overrides=Path(overrides_filepath), + **kwargs, + ) + + calc = submit(builder) + print(f'Submitted IRamanSpectraWorkChain with PK={calc.pk} and UUID={calc.uuid}') + print('Register *at least* the PK number, e.g. in you submit script.') + print('You can monitor the status of your calculation with the following commands:') + print(' * verdi process status PK') + print(' * verdi process list -L IRamanSpectraWorkChain # list all running IRamanSpectraWorkChain') + print( + ' * verdi process list -ap1 -L IRamanSpectraWorkChain # list all IRamanSpectraWorkChain submitted in the previous 1 day' + ) + print('If the WorkChain finishes with exit code 0, then you can inspect the outputs and post-process the data.') + print('Use the command') + print(' * verdi process show PK') + print('To show further information about your WorkChain. When finished, you should see some outputs.') + print('The main output can be accessed via `load_node(PK).outputs.vibrational_data.numerical_accuracy_*`.') + print('You have to complete the remaning `*`, which depends upond the accuracy of the calculation.') + print('See also the documentation and the reference paper for further details.') + + +if __name__ == '__main__': + """Run script.""" + main() diff --git a/pyproject.toml b/pyproject.toml index 0281134..f24bf4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [ readme = 'README.md' license = {file = 'LICENSE.txt'} classifiers = [ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Framework :: AiiDA', 'License :: Other/Proprietary License', 'Operating System :: POSIX :: Linux', @@ -21,6 +21,7 @@ classifiers = [ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', ] keywords = ['aiida', 'workflows'] requires-python = '>=3.8' @@ -28,7 +29,8 @@ dependencies = [ "aiida-core>=2.2.2,<3.0.0", "aiida-quantumespresso>=4.3.0", "aiida-phonopy>=1.1.3", - "phonopy>=2.19.0,<3.0.0", + "spglib<2.5", + "phonopy>=2.19.0,<=2.25.0", ] [project.urls] @@ -44,20 +46,27 @@ pre-commit = [ tests = [ 'pgtest~=1.3', 'pytest~=6.0', + 'coverage[toml]', + 'pytest-cov', 'pytest-regressions~=2.3', + 'pytest-timeout', ] docs = [ - 'myst-nb@git+https://github.com/executablebooks/MyST-NB.git', + 'myst-nb~=1.0.0', 'sphinx~=6.2.1', 'sphinx-copybutton~=0.5.2', 'sphinx-book-theme~=1.0.1', + 'sphinx-click~=4.4.0', 'sphinx-design~=0.4.1', 'sphinxcontrib-details-directive~=0.1.0', - 'sphinx-autoapi~=2.0.1', + 'sphinx-autoapi~=3.0.0', 'myst_parser~=1.0.0', 'sphinx-togglebutton', ] +[project.scripts] +aiida-vibroscopy = 'aiida_vibroscopy.cli:cmd_root' + [project.entry-points.'aiida.data'] "vibroscopy.fp" = "aiida_vibroscopy.data.vibro_fp:VibrationalFrozenPhononData" "vibroscopy.vibrational" = "aiida_vibroscopy.data.vibro_lr:VibrationalData" diff --git a/src/aiida_vibroscopy/__init__.py b/src/aiida_vibroscopy/__init__.py index 48b124e..1579648 100644 --- a/src/aiida_vibroscopy/__init__.py +++ b/src/aiida_vibroscopy/__init__.py @@ -7,4 +7,4 @@ # For further information on the license, see the LICENSE.txt file # ################################################################################# """AiiDA plugin for vibrational spectoscopy using Quantum ESPRESSO.""" -__version__ = '1.0.2' +__version__ = '1.1.1' diff --git a/src/aiida_vibroscopy/calculations/numerical_derivatives_utils.py b/src/aiida_vibroscopy/calculations/numerical_derivatives_utils.py index 5541a24..2bdc57e 100644 --- a/src/aiida_vibroscopy/calculations/numerical_derivatives_utils.py +++ b/src/aiida_vibroscopy/calculations/numerical_derivatives_utils.py @@ -34,6 +34,32 @@ 'compute_nac_parameters' ) +# def map_polarization(polarization: np.ndarray, cell: np.ndarray, sign: Literal[-1, 1]) -> np.ndarray: +# """Map the polarization within a quantum of polarization. + +# It maps P(dE) in [0, Pq] and P(-dE) in [-Pq, 0]. + +# :param polarization: (3,) vector in Cartesian coordinates, Ry atomic units +# :param cell: (3, 3) matrix cell in Cartesian coordinates +# (rows are lattice vectors, i.e. cell[0] = v1, ...) +# :param sign: sign (1 or -1) to select the side of the branch +# :return: (3,) vector in Cartesian coordinates, Ry atomic units +# """ +# inv_cell = np.linalg.inv(cell) +# lengths = np.sqrt(np.sum(cell**2, axis=1)) / CONSTANTS.bohr_to_ang # in Bohr +# volume = float(abs(np.dot(np.cross(cell[0], cell[1]), cell[2]))) / CONSTANTS.bohr_to_ang**3 # in Bohr^3 + +# pol_quantum = np.sqrt(2) * lengths / volume +# pol_crys = lengths * np.dot(polarization, inv_cell) # in Bohr + +# pol_branch = pol_quantum * (pol_crys // pol_quantum) +# pol_crys -= pol_branch + +# if sign < 0: +# pol_crys -= pol_quantum + +# return np.dot(pol_crys / lengths, cell) + def get_central_derivatives_coefficients(accuracy: int, order: int) -> list[int]: r"""Return an array with the central derivatives coefficients. @@ -174,7 +200,7 @@ def compute_susceptibility_derivatives( * Units are pm/V for non-linear optical susceptibility * Raman tensors indecis: (atomic, atomic displacement, electric field, electric field) - :return: dictionary with :class:`~aiida.orm.ArrayData` having keys: + :return: dictionaries of numerical accuracies with :class:`~aiida.orm.ArrayData` having keys: * `raman_tensors` containing (num_atoms, 3, 3, 3) arrays; * `nlo_susceptibility` containing (3, 3, 3) arrays; * `units` as :class:`~aiida.orm.Dict` containing the units of the tensors. @@ -211,10 +237,11 @@ def compute_susceptibility_derivatives( data = get_trajectories_from_symmetries( preprocess_data=preprocess_data, data=raw_data, data_0=data_0, accuracy_order=accuracy_order.value ) + else: + data = raw_data # Conversion factors - # dchi_factor = evang_to_rybohr * CONSTANTS.bohr_to_ang**2 # --> angstrom^2 - dchi_factor = (evang_to_rybohr * CONSTANTS.bohr_to_ang**2) / volume # --> angstrom^-1 + dchi_factor = (evang_to_rybohr * CONSTANTS.bohr_to_ang**2) / volume # --> 4*pi / angstrom chi2_factor = 0.5 * (4 * np.pi) * 100 / (volume_au_units * efield_au_to_si) # --> pm/Volt # Variables @@ -403,6 +430,8 @@ def compute_nac_parameters( data = get_trajectories_from_symmetries( preprocess_data=preprocess_data, data=raw_data, data_0=data_0, accuracy_order=accuracy_order.value ) + else: + data = raw_data # Conversion factors bec_factor = evang_to_rybohr / np.sqrt(2) diff --git a/src/aiida_vibroscopy/calculations/phonon_utils.py b/src/aiida_vibroscopy/calculations/phonon_utils.py deleted file mode 100644 index 8e0a882..0000000 --- a/src/aiida_vibroscopy/calculations/phonon_utils.py +++ /dev/null @@ -1,155 +0,0 @@ -# -*- coding: utf-8 -*- -################################################################################# -# Copyright (c), All rights reserved. # -# This file is part of the AiiDA-Vibroscopy code. # -# # -# The code is hosted on GitHub at https://github.com/bastonero/aiida-vibroscopy # -# For further information on the license, see the LICENSE.txt file # -################################################################################# -"""Calcfunctions utils for extracting outputs related to phonon workflows.""" - -__all__ = ( - 'extract_max_order', 'extract_orders', 'get_forces', 'get_energy', 'get_non_analytical_constants', - 'elaborate_non_analytical_constants' -) - -from aiida import orm -from aiida.engine import calcfunction -from aiida_phonopy.data.preprocess import PreProcessData - - -@calcfunction -def extract_max_order(**kwargs): - """Extract max order accuracy tensor.""" - output = {} - for key, value in kwargs.items(): - max_accuracy = 0 - for name in value.get_arraynames(): - if int(name[-1]) > max_accuracy: - max_accuracy = int(name[-1]) - - array_data = orm.ArrayData() - array_data.set_array(key, value.get_array(f'numerical_accuracy_{max_accuracy}')) - output.update({key: array_data}) - - return output - - -@calcfunction -def extract_orders(**kwargs): - """Extract all the orders of all the tensors of a `DielectricWorkChain` output.""" - order_outputs = {} - - all_dielectric = kwargs['dielectric'] - all_born_charges = kwargs['born_charges'] - - for order_name in all_dielectric.get_arraynames(): - dielectric_array = all_dielectric.get_array(order_name) - born_charges_array = all_born_charges.get_array(order_name) - - order_array = orm.ArrayData() - order_array.set_array('dielectric', dielectric_array) - order_array.set_array('born_charges', born_charges_array) - - order_outputs[order_name] = {'nac_parameters': order_array} - - try: - all_dph0 = kwargs['raman_tensors'] - all_nlo = kwargs['nlo_susceptibility'] - - for order_name in all_dielectric.get_arraynames(): - dph0_array = all_dph0.get_array(order_name) - nlo_array = all_nlo.get_array(order_name) - - order_dph0_array = orm.ArrayData() - order_dph0_array.set_array('raman_tensors', dph0_array) - - order_nlo_array = orm.ArrayData() - order_nlo_array.set_array('nlo_susceptibility', nlo_array) - - order_outputs[order_name].update({'raman_tensors': order_dph0_array}) - order_outputs[order_name].update({'nlo_susceptibility': order_nlo_array}) - except KeyError: - pass - - return order_outputs - - -@calcfunction -def get_forces(trajectory: orm.TrajectoryData) -> orm.ArrayData: - """Extract the `forces` array from a TrajectoryData.""" - from aiida.orm import ArrayData - forces = ArrayData() - forces.set_array('forces', trajectory.get_array('forces')[-1]) - return forces - - -@calcfunction -def get_energy(parameters: orm.Dict) -> orm.Float: - """Convert the `energy` attribute of `parameters` into a Float.""" - from aiida.orm import Float - return Float(parameters.base.attributes.get('energy')) - - -@calcfunction -def get_non_analytical_constants(dielectric: orm.ArrayData, born_charges: orm.ArrayData) -> orm.ArrayData: - """Return a joint ArrayData with dielectric and Born effective charges tensors. - - :param dielectric: ArrayData having an arrayname `dielectric` - :param born_charges: ArrayData having an arrayname `born_charges` - """ - nac_parameters = orm.ArrayData() - nac_parameters.set_array('dielectric', dielectric.get_array('dielectric')) - nac_parameters.set_array('born_charges', born_charges.get_array('born_charges')) - - return nac_parameters - - -@calcfunction -def elaborate_non_analytical_constants( - preprocess_data: PreProcessData, - dielectric=None, - born_charges=None, - nac_parameters=None, -): - """Return the non analytical constants in the primitive cell. - - It uses the unique atoms referring to the supercell matrix. - """ - from aiida.orm import ArrayData - from phonopy.structure.symmetry import symmetrize_borns_and_epsilon - - if nac_parameters is None: - ref_dielectric = dielectric.get_array('dielectric') - ref_born_charges = born_charges.get_array('born_charges') - else: - ref_dielectric = nac_parameters.get_array('dielectric') - ref_born_charges = nac_parameters.get_array('born_charges') - - nacs = symmetrize_borns_and_epsilon( - # nac info - borns=ref_born_charges, - epsilon=ref_dielectric, - # preprocess info - ucell=preprocess_data.get_phonopy_instance().unitcell, - primitive_matrix=preprocess_data.primitive_matrix, - supercell_matrix=preprocess_data.supercell_matrix, - is_symmetry=preprocess_data.is_symmetry, - symprec=preprocess_data.symprec, - ) - - nac_parameters = ArrayData() - nac_parameters.set_array('dielectric', nacs[1]) - nac_parameters.set_array('born_charges', nacs[0]) - - return nac_parameters - - -@calcfunction -def extract_symmetry_info(preprocess_data: PreProcessData): - """Return symmetry info for analysis.""" - return { - 'symprec': orm.Float(preprocess_data.symprec), - 'distinguish_kinds': orm.Bool(preprocess_data.distinguish_kinds), - 'is_symmetry': orm.Bool(preprocess_data.is_symmetry), - } diff --git a/src/aiida_vibroscopy/calculations/spectra_utils.py b/src/aiida_vibroscopy/calculations/spectra_utils.py index 6706f7f..010c4ec 100644 --- a/src/aiida_vibroscopy/calculations/spectra_utils.py +++ b/src/aiida_vibroscopy/calculations/spectra_utils.py @@ -10,6 +10,7 @@ from __future__ import annotations from copy import deepcopy +from typing import Tuple, Union from aiida import orm from aiida.engine import calcfunction @@ -17,6 +18,7 @@ from aiida_phonopy.data.preprocess import PreProcessData from aiida_quantumespresso.data.hubbard_structure import HubbardStructureData import numpy as np +from phonopy import Phonopy from aiida_vibroscopy.common import UNITS @@ -26,6 +28,8 @@ 'compute_raman_space_average', 'compute_raman_susceptibility_tensors', 'compute_polarization_vectors', + 'compute_complex_dielectric', + 'compute_clamped_pockels_tensor', 'get_supercells_for_hubbard', 'elaborate_susceptibility_derivatives', 'generate_vibrational_data_from_forces', @@ -39,9 +43,9 @@ def boson_factor(frequency: float, temperature: float) -> float: def compute_active_modes( - phonopy_instance, + phonopy_instance: Phonopy, degeneracy_tolerance: float = 1.e-5, - nac_direction: None | list[float, float, float] = None, + nac_direction: list[float, float, float] | None = None, selection_rule: str | None = None, sr_thr: float = 1e-4, imaginary_thr: float = -5.0, @@ -51,7 +55,7 @@ def compute_active_modes( Raman and infrared active modes can be extracted using `selection_rule`. :param nac_direction: (3,) shape list, indicating non analytical - direction in fractional reciprocal (unitcell cell) space coordinates + direction in Cartesian coordinates :param selection_rule: str, can be `raman` or `ir`, it uses symmetry in the selection of the modes for a specific type of process :param sr_thr: float, threshold for selection rule (the analytical value is 0) @@ -63,11 +67,21 @@ def compute_active_modes( if selection_rule not in ('raman', 'ir', None): raise ValueError('`selection_rule` can only be `ir` or `raman`.') + q_reduced = None + + # Notation: columns convention; k = reciprocal; x = direct space; (p) = primitive; (u) = unitcell + # C(p) = C(u) * M ==> M = C(u)^-1 * C(p) + # x(p) = M^-1 x(u) ==> k(p) = M^T k(u); + # q_reduced = np.dot(phonopy_instance.primitive_matrix.T, nac_dir) # in reduced/crystal (PRIMITIVE) coordinates + if nac_direction is not None: + q_reduced = np.dot(phonopy_instance.primitive.cell, + nac_direction) / (2. * np.pi) # in reduced/crystal (PRIMITIVE) coordinates + # Step 1 - set the irreducible representations and the phonons - phonopy_instance.set_irreps(q=[0, 0, 0], nac_q_direction=nac_direction, degeneracy_tolerance=degeneracy_tolerance) + phonopy_instance.set_irreps(q=[0, 0, 0], nac_q_direction=q_reduced, degeneracy_tolerance=degeneracy_tolerance) irreps = phonopy_instance.irreps - phonopy_instance.run_qpoints(q_points=[0, 0, 0], nac_q_direction=nac_direction, with_eigenvectors=True) + phonopy_instance.run_qpoints(q_points=[0, 0, 0], nac_q_direction=q_reduced, with_eigenvectors=True) frequencies = phonopy_instance.qpoints.frequencies[0] * UNITS.thz_to_cm eigvectors = phonopy_instance.qpoints.eigenvectors.T.real @@ -161,14 +175,15 @@ def compute_raman_space_average(raman_susceptibility_tensors: np.ndarray) -> tup intensities_hh.append((10 * G0 + 4 * G2) / 30) intensities_hv.append((5 * G1 + 3 * G2) / 30) + return (np.array(intensities_hh), np.array(intensities_hv)) def compute_raman_susceptibility_tensors( - phonopy_instance, + phonopy_instance: Phonopy, raman_tensors: np.ndarray, nlo_susceptibility: np.ndarray = None, - nac_direction: tuple[float, float, float] = lambda: (0, 0, 0), + nac_direction: list[float, float, float] | None = None, use_irreps: bool = True, degeneracy_tolerance: float = 1e-5, sum_rules: bool = False, @@ -176,11 +191,11 @@ def compute_raman_susceptibility_tensors( """Return the Raman susceptibility tensors, frequencies (cm-1) and labels. .. note:: - * Units of Raman susceptibility tensor are (Angstrom/AMU)^(1/2) + * Units of Raman susceptibility tensor are (Angstrom/4pi/AMU)^(1/2) * Unitcell volume for Raman tensor as normalization (in case non-primitive cell was used). :param phonopy_instance: Phonopy instance with non-analytical constants included - :param nac_direction: non-analytical direction in reciprocal space coordinates (unitcell cell) + :param nac_direction: non-analytical direction in Cartesian coordinates :param raman_tensors: dChi/du in Cartesian coordinates (in 1/Angstrom) :param nlo_susceptibility: non linear optical susceptibility in Cartesian coordinates (in pm/V) @@ -192,18 +207,15 @@ def compute_raman_susceptibility_tensors( :return: tuple of numpy.ndarray (Raman susc. tensors, frequencies, labels) """ - nac_direction = np.array(nac_direction) raman_tensors = deepcopy(raman_tensors) - if nac_direction.shape != (3,): - raise ValueError('the array is not of the correct shape') - volume = phonopy_instance.unitcell.volume sqrt_volume = np.sqrt(volume) raman_tensors *= volume - rcell = np.linalg.inv(phonopy_instance.unitcell.cell).T # as rows - q_direction = np.dot(rcell.T, nac_direction) # in Cartesian coordinates + # rcell = np.linalg.inv(phonopy_instance.unitcell.cell) # as columns + # q_cartesian = np.dot(rcell, nac_direction) # in Cartesian coordinates + q_cartesian = np.zeros((3)) if nac_direction is None else np.array(nac_direction) selection_rule = 'raman' if use_irreps else None @@ -213,7 +225,7 @@ def compute_raman_susceptibility_tensors( freqs, neigvs, labels = compute_active_modes( phonopy_instance=phonopy_instance, - nac_direction=nac_direction, + nac_direction=q_cartesian, degeneracy_tolerance=degeneracy_tolerance, selection_rule=selection_rule ) @@ -228,7 +240,7 @@ def compute_raman_susceptibility_tensors( # The contraction is performed over I and k, resulting in (n, a, b) Raman tensors. raman_susceptibility_tensors = np.tensordot(neigvs, raman_tensors, axes=([1, 2], [0, 1])) - if nlo_susceptibility is not None and q_direction.nonzero()[0].tolist(): + if nlo_susceptibility is not None and q_cartesian.nonzero()[0].tolist(): borns = phonopy_instance.nac_params['born'] dielectric = phonopy_instance.nac_params['dielectric'] # -8 pi (Z.q/q.epsilon.q)[I,k] Chi(2).q [a,b] is the correction to dph0. @@ -241,17 +253,17 @@ def compute_raman_susceptibility_tensors( # !!! ---------------------- !!! # Here we can extend to 1/2D models. # !!! ---------------------- !!! - dielectric_term = np.dot(np.dot(dielectric, q_direction), q_direction) + dielectric_term = np.dot(np.dot(dielectric, q_cartesian), q_cartesian) ### DEBUG # print("\n", "================================", "\n") # print("DEBUG") - # print("q dir cart: ", q_direction) + # print("q dir cart: ", q_cartesian) # print("nac: ", nac_direction) ### DEBUG # Z*.q - borns_term_dph0 = np.tensordot(borns, q_direction, axes=(1, 0)) # (num atoms, 3) | (I, k) + borns_term_dph0 = np.tensordot(borns, q_cartesian, axes=(1, 0)) # (num atoms, 3) | (I, k) borns_term = np.tensordot(borns_term_dph0, neigvs, axes=([0, 1], [1, 2])) # (num modes) | (n) ### DEBUG @@ -259,7 +271,7 @@ def compute_raman_susceptibility_tensors( ### DEBUG # Chi(2).q - nlo_term = np.tensordot(nlo_susceptibility, q_direction, axes=(2, 0)) # (3, 3) | (a, b) + nlo_term = np.tensordot(nlo_susceptibility, q_cartesian, axes=(2, 0)) # (3, 3) | (a, b) ### DEBUG # print("Nlo term: ", nlo_term.round(5)) @@ -280,8 +292,8 @@ def compute_raman_susceptibility_tensors( def compute_polarization_vectors( - phonopy_instance, - nac_direction: list[float, float, float] = lambda: [0, 0, 0], + phonopy_instance: Phonopy, + nac_direction: list[float, float, float] | None = None, use_irreps: bool = True, degeneracy_tolerance: float = 1e-5, sum_rules: bool = False, @@ -292,8 +304,7 @@ def compute_polarization_vectors( .. note:: the unite for polarization vectors are in (debey/angstrom)/sqrt(AMU) :param phonopy_instance: Phonopy instance with non-analytical constants included - :param nac_direction: non-analytical direction in fractional coordinates (unitcell cell) - in reciprocal space + :param nac_direction: non-analytical direction in Cartesian coordinates :param use_irreps: whether to use irreducible representations in the selection of modes, defaults to True :param degeneracy_tolerance: degeneracy tolerance @@ -331,6 +342,161 @@ def compute_polarization_vectors( return (pol_vectors, freqs, labels) +def compute_complex_dielectric( + phonopy_instance: Phonopy, + freq_range: Union[str, np.ndarray] = 'auto', + gammas: float | list[float] = 12.0, + nac_direction: None | list[float, float, float] = None, + use_irreps: bool = True, + degeneracy_tolerance: float = 1e-5, + sum_rules: bool = False, +) -> np.ndarray: + """Return the frequency dependent complex dielectric function (tensor). + + :param freq_range: frequency range in cm^-1; set to `auto` for automatic choice + :param gammas: list or single value of broadenings, i.e. full width at half maximum (FWHM) + :param nac_direction: (3,) shape list, indicating non analytical + direction in Cartesian coordinates + :param use_irreps: whether to use irreducible representations + in the selection of modes, defaults to True + :param degeneracy_tolerance: degeneracy tolerance + for irreducible representation + :param sum_rules: whether to apply charge neutrality to effective charges + + :return: (3, 3, num steps) shape :class:`numpy.ndarray`, `num steps` refers to the + number of frequency steps where the complex dielectric function is evaluated + """ + from phonopy import units + from qe_tools import CONSTANTS + + prefactor = 4 * np.pi * units.VaspToCm**2 * 2. * CONSTANTS.ry_to_ev * CONSTANTS.bohr_to_ang + + polarizations, frequencies, _ = compute_polarization_vectors( + phonopy_instance=phonopy_instance, + nac_direction=nac_direction, + use_irreps=use_irreps, + degeneracy_tolerance=degeneracy_tolerance, + sum_rules=sum_rules + ) + + if isinstance(gammas, float): + sigmas = [gammas for _ in frequencies] + elif isinstance(gammas, (list, np.ndarray)): + if len(gammas) != len(frequencies): + raise ValueError("length of `gammas` and number of frequencies don't match") + sigmas = deepcopy(gammas) + else: + sigmas = [float(gammas) for _ in frequencies] + + if isinstance(freq_range, str): + xi = max(0, frequencies.min() - 200) + xf = frequencies.max() + 200 + freq_range = np.arange(xi, xf, 1.) + + polarizations /= UNITS.debey_ang + oscillator = np.zeros((3, 3)) + + oscillators = [] + for pol, freq in zip(polarizations, frequencies): + oscillators.append(np.outer(pol, pol)) + oscillator += np.outer(pol, pol) / (freq * freq) + + oscillators = np.array(oscillators) + + complex_diel = np.zeros((3, 3, freq_range.shape[0]), dtype=np.complex128) + for n, osc in enumerate(oscillators): + for i1 in range(3): + for i2 in range(3): + complex_diel[i1, i2] += ( + np.array(osc[i1, i2]) / ( + +np.array(frequencies[n])**2 # omega_n^2 + - np.array(freq_range)**2 # - omega^2 + - np.array(freq_range) * sigmas[n] * complex(1j) # -i eta_n omega + ) + ) + + diel = phonopy_instance.nac_params['dielectric'] + diel_infinity = np.tensordot(diel, np.ones(freq_range.shape[0]), axes=0) + + return diel_infinity + prefactor * (complex_diel) / phonopy_instance.unitcell.volume + + +def compute_clamped_pockels_tensor( + phonopy_instance: Phonopy, + raman_tensors: np.ndarray, + nlo_susceptibility: np.ndarray, + nac_direction: None | list[float, float, float] = None, + imaginary_thr: float = -5.0 * 1.0e+12, # in Hz + skip_frequencies: int = 3, +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Compute the clamped Pockels tensor in Cartesian coordinates. + + .. note:: Units are in pm/V + + :param nac_direction: non-analytical direction in Cartesian coordinates; + (3,) shape :class:`list` or :class:`numpy.ndarray` + :param degeneracy_tolerance: degeneracy tolerance for irreducible representation + :param imaginary_thr: threshold for activating warnings on negative frequencies (in Hz) + :param skip_frequencies: number of frequencies to not include (i.e. the acoustic modes) + + :return: tuple of (r_ion + r_el, r_el, r_ion), each having (3, 3, 3) shape array + """ + borns = phonopy_instance.nac_params['born'] * UNITS.elementary_charge_si # convert to Coulomb + dielectric = phonopy_instance.nac_params['dielectric'] + + dielectric_inv = np.linalg.inv(dielectric) + raman = raman_tensors * 1.0e10 # convert to 1/m + + q_reduced = None + # Have a look in compute_active_modes for the notation + if nac_direction is not None: + q_reduced = np.dot(phonopy_instance.primitive.cell, + nac_direction) / (2. * np.pi) # in reduced/crystal (PRIMITIVE) coordinates + + phonopy_instance.run_qpoints(q_points=[0, 0, 0], nac_q_direction=q_reduced, with_eigenvectors=True) + frequencies = phonopy_instance.qpoints.frequencies[0] * 1.0e+12 # THz -> Hz + eigvectors = phonopy_instance.qpoints.eigenvectors.T.real + + if frequencies.min() < imaginary_thr: + raise ValueError('Negative frequencies detected.') + + masses = phonopy_instance.masses * UNITS.atomic_mass_si + sqrt_masses = np.array([[np.sqrt(mass)] for mass in masses]) + + shape = (len(frequencies), len(masses), 3) # (modes, atoms, 3) + eigvectors = eigvectors.reshape(shape) + norm_eigvectors = np.array([eigv / sqrt_masses for eigv in eigvectors]) + + # norm_eigvectors shape|indices = (modes, atoms, 3) | (m, a, p) + # raman shape|indices = (atoms, 3, 3, 3) | (a, p, i, j) + # The contraction is performed over a and k, resulting in (m, i, j) raman susceptibility tensors. + alpha = np.tensordot(norm_eigvectors, raman, axes=([1, 2], [0, 1])) + + # borns charges shape|indices = (atoms, 3, 3) | (a, k, p) + # norm_eigvectors shape|indices = (modes, atoms, 3) | (m, a, p) + # polarization vectors shape | indices = (3, modes) | (k, m) + polvec = np.tensordot(borns, norm_eigvectors, axes=([0, 2], [1, 2])) # (k, m) + + ir_contribution = polvec / frequencies**2 # (k, m) + + # sets the first `skip_frequencies` IR contributions to zero (i.e. the acoustic modes) + for i in range(skip_frequencies): + ir_contribution[:, i] = 0 + r_ion_inner = np.tensordot(alpha, ir_contribution, axes=([0], [1])) # (i, j, k) + + r_ion_left = np.dot(dielectric_inv, np.transpose(r_ion_inner, axes=[1, 0, 2])) + r_ion_transposed = np.dot(np.transpose(r_ion_left, axes=[0, 2, 1]), dielectric_inv) + r_ion = -np.transpose(r_ion_transposed, axes=[0, 2, 1]) * 1.0e+12 # pm/V + + r_el_left = np.dot(dielectric_inv, np.transpose(nlo_susceptibility, axes=[1, 0, 2])) + r_el_transposed = -2 * np.dot(np.transpose(r_el_left, axes=[0, 2, 1]), dielectric_inv) + r_el = np.transpose(r_el_transposed, axes=[0, 2, 1]) + + r = r_el + r_ion + + return r, r_el, r_ion + + @calcfunction def get_supercells_for_hubbard( preprocess_data: PreProcessData, diff --git a/src/aiida_vibroscopy/calculations/symmetry.py b/src/aiida_vibroscopy/calculations/symmetry.py index 84a8228..71e3401 100644 --- a/src/aiida_vibroscopy/calculations/symmetry.py +++ b/src/aiida_vibroscopy/calculations/symmetry.py @@ -10,6 +10,7 @@ from __future__ import annotations from copy import deepcopy +from typing import List, Tuple, Union from aiida.orm import TrajectoryData from aiida_phonopy.data import PreProcessData @@ -28,8 +29,8 @@ def tensor_3rd_rank_transformation( - rot: list[tuple[float, float, float]] | np.ndarray, - mat: list[list[tuple[float, float, float]]] | np.ndarray, + rot: Union[list[tuple[float, float, float]], np.ndarray], + mat: Union[list[list[tuple[float, float, float]]], np.ndarray], ) -> np.ndarray: """Tensor transformation. @@ -41,9 +42,9 @@ def tensor_3rd_rank_transformation( def symmetrize_3nd_rank_tensor( - tensor: list[list[tuple[float, float, float]]] | np.ndarray, - symmetry_operations: tuple[list[list[float, float, float]]] | np.ndarray, - lattice: list[tuple[float, float, float]] | np.ndarray, + tensor: Union[list[list[tuple[float, float, float]]], np.ndarray], + symmetry_operations: Union[tuple[list[list[float, float, float]]], np.ndarray], + lattice: Union[list[tuple[float, float, float]], np.ndarray], ) -> np.ndarray: """Symmetrize a 3rd rank tensor using symmetry operations in the lattice. @@ -62,8 +63,8 @@ def symmetrize_3nd_rank_tensor( def take_average_of_dph0( dchi_ph0: np.ndarray, - rotations: list[np.ndarray], - translations: list[np.ndarray], + rotations: List[np.ndarray], + translations: List[np.ndarray], cell: np.ndarray, symprec: float, ) -> np.ndarray: @@ -106,9 +107,9 @@ def symmetrize_susceptibility_derivatives( raman_tensors: np.ndarray, nlo_susceptibility: np.ndarray, ucell: np.ndarray, - primitive_matrix: np.ndarray | None = None, - primitive: np.ndarray | None = None, - supercell_matrix: np.ndarray | None = None, + primitive_matrix: Union[np.ndarray, None] = None, + primitive: Union[np.ndarray, None] = None, + supercell_matrix: Union[np.ndarray, None] = None, symprec: float = 1e-5, is_symmetry: bool = True, ) -> tuple(np.ndarray, np.ndarray): @@ -187,7 +188,7 @@ def symmetrize_susceptibility_derivatives( def get_connected_fields_with_operations( phonopy_instance: Phonopy, field_direction: tuple[int, int, int], -) -> tuple[np.ndarray, np.ndarray, np.ndarray]: +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """Return symmetry equivalent electric field direction (always with zeros ones). :param field_direction: (3,) shape list diff --git a/src/aiida_vibroscopy/cli/__init__.py b/src/aiida_vibroscopy/cli/__init__.py new file mode 100644 index 0000000..fc3e828 --- /dev/null +++ b/src/aiida_vibroscopy/cli/__init__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# pylint: disable=wrong-import-position +"""Module for the command line interface.""" +from aiida.cmdline.groups import VerdiCommandGroup +from aiida.cmdline.params import options, types +import click + + +@click.group('aiida-vibroscopy', cls=VerdiCommandGroup, context_settings={'help_option_names': ['-h', '--help']}) +@options.PROFILE(type=types.ProfileParamType(load_profile=True), expose_value=False) +def cmd_root(): + """CLI for the `aiida-vibroscopy` plugin.""" + + +from .workflows import cmd_launch diff --git a/src/aiida_vibroscopy/cli/utils/__init__.py b/src/aiida_vibroscopy/cli/utils/__init__.py new file mode 100644 index 0000000..0e79dab --- /dev/null +++ b/src/aiida_vibroscopy/cli/utils/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +"""Utilities for the command line interface.""" +# pylint: disable=cyclic-import,unused-import,wrong-import-position,import-error diff --git a/src/aiida_vibroscopy/cli/utils/defaults.py b/src/aiida_vibroscopy/cli/utils/defaults.py new file mode 100644 index 0000000..d3f0f3e --- /dev/null +++ b/src/aiida_vibroscopy/cli/utils/defaults.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +"""Module with utitlies for the CLI to generate default values.""" + + +def get_structure(): + """Return a `StructureData` representing bulk silicon. + + The database will first be queried for the existence of a bulk silicon crystal. If this is not the case, one is + created and stored. This function should be used as a default for CLI options that require a `StructureData` node. + This way new users can launch the command without having to construct or import a structure first. This is the + reason that we hardcode a bulk silicon crystal to be returned. More flexibility is not required for this purpose. + + :return: a `StructureData` representing bulk silicon + """ + from aiida.orm import QueryBuilder, StructureData + from ase.spacegroup import crystal + + # Filters that will match any elemental Silicon structure with 2 or less sites in total + filters = { + 'attributes.sites': { + 'of_length': 2 + }, + 'attributes.kinds': { + 'of_length': 1 + }, + 'attributes.kinds.0.symbols.0': 'Si' + } + + builder = QueryBuilder().append(StructureData, filters=filters) + structure = builder.first(flat=True) + + if not structure: + alat = 5.43 + ase_structure = crystal( + 'Si', + [(0, 0, 0)], + spacegroup=227, + cellpar=[alat, alat, alat, 90, 90, 90], + primitive_cell=True, + ) + structure = StructureData(ase=ase_structure) + structure.store() + + return structure.uuid diff --git a/src/aiida_vibroscopy/cli/utils/display.py b/src/aiida_vibroscopy/cli/utils/display.py new file mode 100644 index 0000000..ba5538b --- /dev/null +++ b/src/aiida_vibroscopy/cli/utils/display.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +"""Module with display utitlies for the CLI.""" +import os + +import click + + +def echo_process_results(node): + """Display a formatted table of the outputs registered for the given process node. + + :param node: the `ProcessNode` of a terminated process + """ + from aiida.common.links import LinkType + + class_name = node.process_class.__name__ + outputs = node.base.links.get_outgoing(link_type=(LinkType.CREATE, LinkType.RETURN)).all() + + if hasattr(node, 'dry_run_info'): + # It is a dry-run: get the information and print it + rel_path = os.path.relpath(node.dry_run_info['folder']) + click.echo(f"-> Files created in folder '{rel_path}'") + click.echo(f"-> Submission script filename: '{node.dry_run_info['script_filename']}'") + return + + if node.is_finished and node.exit_message: + state = f'{node.process_state.value} [{node.exit_status}] `{node.exit_message}`' + elif node.is_finished: + state = f'{node.process_state.value} [{node.exit_status}]' + else: + state = node.process_state.value + + click.echo(f'{class_name}<{node.pk}> terminated with state: {state}') + + if not outputs: + click.echo(f'{class_name}<{node.pk}> registered no outputs') + return + + click.echo(f"\n{'Output link':25s} Node pk and type") + click.echo(f"{'-' * 60}") + + for triple in sorted(outputs, key=lambda triple: triple.link_label): + click.echo(f'{triple.link_label:25s} {triple.node.__class__.__name__}<{triple.node.pk}> ') diff --git a/src/aiida_vibroscopy/cli/utils/launch.py b/src/aiida_vibroscopy/cli/utils/launch.py new file mode 100644 index 0000000..93ab138 --- /dev/null +++ b/src/aiida_vibroscopy/cli/utils/launch.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +"""Module with launch utitlies for the CLI.""" +import click + +from .display import echo_process_results + + +def launch_process(process, daemon, **inputs): + """Launch a process with the given inputs. + + If not sent to the daemon, the results will be displayed after the calculation finishes. + + :param process: the process class + :param daemon: boolean, if True will submit to the daemon instead of running in current interpreter + :param inputs: inputs for the process + """ + from aiida.engine import Process, ProcessBuilder, launch + + if isinstance(process, ProcessBuilder): + process_name = process.process_class.__name__ + elif issubclass(process, Process): + process_name = process.__name__ + else: + raise TypeError(f'invalid type for process: {process}') + + if daemon: + node = launch.submit(process, **inputs) + click.echo( + f""" + Submitted {process_name} to the daemon. + Information of the launched process: + * PK :\t{node.pk}> + * UUID:\t{node.uuid}> + Record this information for later usage, or put this node in a Group. + """ + ) + else: + if inputs.get('metadata', {}).get('dry_run', False): + click.echo(f'Running a dry run for {process_name}...') + else: + click.echo(f'Running a {process_name}...') + _, node = launch.run_get_node(process, **inputs) + echo_process_results(node) diff --git a/src/aiida_vibroscopy/cli/utils/options.py b/src/aiida_vibroscopy/cli/utils/options.py new file mode 100644 index 0000000..ba318a4 --- /dev/null +++ b/src/aiida_vibroscopy/cli/utils/options.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +"""Pre-defined overridable options for commonly used command line interface parameters.""" +# pylint: disable=too-few-public-methods,import-error +from aiida.cmdline.params import types +from aiida.cmdline.params.options import OverridableOption +from aiida.cmdline.utils import decorators +from aiida.common import exceptions +import click + +from . import validate + + +class PseudoFamilyType(types.GroupParamType): + """Subclass of `GroupParamType` in order to be able to print warning with instructions.""" + + def __init__(self, pseudo_types=None, **kwargs): + """Construct a new instance.""" + super().__init__(**kwargs) + self._pseudo_types = pseudo_types + + @decorators.with_dbenv() + def convert(self, value, param, ctx): + """Convert the value to actual pseudo family instance.""" + try: + group = super().convert(value, param, ctx) + except click.BadParameter: + try: + from aiida.orm import load_group + load_group(value) + except exceptions.NotExistent: # pylint: disable=try-except-raise + raise + + raise click.BadParameter( # pylint: disable=raise-missing-from + f'`{value}` is not of a supported pseudopotential family type.\nTo install a supported ' + 'pseudofamily, use the `aiida-pseudo` plugin. See the following link for detailed instructions:\n\n' + ' https://github.com/aiidateam/aiida-quantumespresso#pseudopotentials' + ) + + if self._pseudo_types is not None and group.pseudo_type not in self._pseudo_types: + pseudo_types = ', '.join(self._pseudo_types) + raise click.BadParameter( + f'family `{group.label}` contains pseudopotentials of the wrong type `{group.pseudo_type}`.\nOnly the ' + f'following types are supported: {pseudo_types}' + ) + + return group + + +PW_CODE = OverridableOption( + '--pw', + 'pw_code', + type=types.CodeParamType(entry_point='quantumespresso.pw'), + required=True, + help='The code to use for the pw.x executable.' +) + +PHONOPY_CODE = OverridableOption( + '--phonopy', + 'phonopy_code', + type=types.CodeParamType(entry_point='phonopy.phonopy'), + required=True, + help='The code to use for the phonopy executable.' +) + +PSEUDO_FAMILY = OverridableOption( + '-F', + '--pseudo-family', + type=PseudoFamilyType(sub_classes=('aiida.groups:pseudo.family',), pseudo_types=('pseudo.upf',)), + required=False, + help='Select a pseudopotential family, identified by its label.' +) + +STRUCTURE = OverridableOption( + '-S', + '--structure', + type=types.DataParamType(sub_classes=('aiida.data:core.structure',)), + help='A StructureData node identified by its ID or UUID.' +) + +KPOINTS_MESH = OverridableOption( + '-k', + '--kpoints-mesh', + 'kpoints_mesh', + nargs=6, + type=click.Tuple([int, int, int, float, float, float]), + show_default=True, + callback=validate.validate_kpoints_mesh, + help='The number of points in the kpoint mesh along each basis vector and the offset. ' + 'Example: `-k 2 2 2 0 0 0`. Specify `0.5 0.5 0.5` for the offset if you want to result ' + 'in the equivalent Quantum ESPRESSO pw.x `1 1 1` shift.' +) + +PARENT_FOLDER = OverridableOption( + '-P', + '--parent-folder', + 'parent_folder', + type=types.DataParamType(sub_classes=('aiida.data:core.remote',)), + show_default=True, + required=False, + help='A parent remote folder node identified by its ID or UUID.' +) + +DAEMON = OverridableOption( + '-D', + '--daemon', + is_flag=True, + default=True, + show_default=True, + help='Submit the process to the daemon instead of running it and waiting for it to finish.' +) + +OVERRIDES = OverridableOption( + '-o', + '--overrides', + type=click.File('r'), + required=False, + help='The filename or filepath containing the overrides, in YAML format.' +) + +PROTOCOL = OverridableOption( + '-p', + '--protocol', + type=click.STRING, + required=False, + help='Select the protocol that defines the accuracy of the calculation.' +) diff --git a/src/aiida_vibroscopy/cli/utils/validate.py b/src/aiida_vibroscopy/cli/utils/validate.py new file mode 100644 index 0000000..e729ee6 --- /dev/null +++ b/src/aiida_vibroscopy/cli/utils/validate.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +"""Utility functions for validation of command line interface parameter inputs.""" +from aiida.cmdline.utils import decorators +import click + + +@decorators.with_dbenv() +def validate_kpoints_mesh(ctx, param, value): + """Command line option validator for a kpoints mesh tuple. + + The value should be a tuple of three positive integers out of which a KpointsData object will be created with a mesh + equal to the tuple. + + :param ctx: internal context of the click.command + :param param: the click Parameter, i.e. either the Option or Argument to which the validator is hooked up + :param value: a tuple of three positive integers + :returns: a KpointsData instance + """ + # pylint: disable=unused-argument + from aiida.orm import KpointsData + + if not value: + return None + + if any(not isinstance(integer, int) for integer in value[:3]) or any(int(i) <= 0 for i in value[:3]): + raise click.BadParameter('all values of the tuple should be positive greater than zero integers') + + try: + kpoints = KpointsData() + kpoints.set_kpoints_mesh(value[:3], value[3:]) + except ValueError as exception: + raise click.BadParameter(f'failed to create a KpointsData mesh out of {value}\n{exception}') + + return kpoints diff --git a/src/aiida_vibroscopy/cli/workflows/__init__.py b/src/aiida_vibroscopy/cli/workflows/__init__.py new file mode 100644 index 0000000..5b69e5b --- /dev/null +++ b/src/aiida_vibroscopy/cli/workflows/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# pylint: disable=cyclic-import,reimported,unused-import,wrong-import-position,import-error +"""Module with CLI commands for the various work chain implementations.""" +from .. import cmd_root + + +@cmd_root.group('launch') +def cmd_launch(): + """Launch workflows.""" + + +from .dielectric.base import launch_workflow +from .phonons.base import launch_workflow +from .phonons.harmonic import launch_workflow +# Import the sub commands to register them with the CLI +from .spectra.iraman import launch_workflow diff --git a/src/aiida_vibroscopy/cli/workflows/dielectric/__init__.py b/src/aiida_vibroscopy/cli/workflows/dielectric/__init__.py new file mode 100644 index 0000000..5945fc4 --- /dev/null +++ b/src/aiida_vibroscopy/cli/workflows/dielectric/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# pylint: disable=cyclic-import,unused-import,wrong-import-position,import-error +"""Module with CLI commands for various dielectric workflows.""" diff --git a/src/aiida_vibroscopy/cli/workflows/dielectric/base.py b/src/aiida_vibroscopy/cli/workflows/dielectric/base.py new file mode 100755 index 0000000..40a0fcf --- /dev/null +++ b/src/aiida_vibroscopy/cli/workflows/dielectric/base.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +"""Command line scripts to launch a `DielectricWorkChain` for testing and demonstration purposes.""" +# pylint: disable=import-error +from aiida.cmdline.utils import decorators +import click +import yaml + +from .. import cmd_launch +from ...utils import defaults, launch, options + + +@cmd_launch.command('dielectric') +@options.PW_CODE() +@options.STRUCTURE(default=defaults.get_structure) +@options.PROTOCOL(type=click.Choice(['fast', 'moderate', 'precise']), default='moderate', show_default=True) +@options.PSEUDO_FAMILY() +@options.KPOINTS_MESH(show_default=False) +@options.OVERRIDES() +@options.DAEMON() +@decorators.with_dbenv() +def launch_workflow(pw_code, structure, protocol, pseudo_family, kpoints_mesh, overrides, daemon): + """Run an `DielectricWorkChain`. + + It computes dielectric, Born charges, Raman and non-linear optical susceptibility + tensors for a given structure. + """ + from aiida.plugins import WorkflowFactory # pyliny: disable=import-error + + entry_point_name = 'vibroscopy.dielectric' + + if overrides: + overrides = yaml.safe_load(overrides) + + if pseudo_family: + if overrides: + overrides.setdefault('scf', {})['pseudo_family'] = pseudo_family.label + else: + overrides = { + 'scf': { + 'pseudo_family': pseudo_family.label + }, + } + + builder = WorkflowFactory(entry_point_name).get_builder_from_protocol( + code=pw_code, + structure=structure, + protocol=protocol, + overrides=overrides, + ) + + if kpoints_mesh: + builder.pop('kpoints_parallel_distance') + builder.scf.pop('kpoints_distance') + builder.scf.kpoints = kpoints_mesh + + launch.launch_process(builder, daemon) diff --git a/src/aiida_vibroscopy/cli/workflows/phonons/__init__.py b/src/aiida_vibroscopy/cli/workflows/phonons/__init__.py new file mode 100644 index 0000000..3cb9240 --- /dev/null +++ b/src/aiida_vibroscopy/cli/workflows/phonons/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# pylint: disable=cyclic-import,unused-import,wrong-import-position,import-error +"""Module with CLI commands for various phonon workflows.""" diff --git a/src/aiida_vibroscopy/cli/workflows/phonons/base.py b/src/aiida_vibroscopy/cli/workflows/phonons/base.py new file mode 100755 index 0000000..be7d384 --- /dev/null +++ b/src/aiida_vibroscopy/cli/workflows/phonons/base.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +"""Command line scripts to launch a `PhononWorkChain` for testing and demonstration purposes.""" +from aiida.cmdline.utils import decorators +import click +import yaml + +from .. import cmd_launch +from ...utils import defaults, launch, options + + +@cmd_launch.command('phonon') +@options.PW_CODE() +@options.STRUCTURE(default=defaults.get_structure) +@options.PROTOCOL(type=click.Choice(['fast', 'moderate', 'precise']), default='moderate', show_default=True) +@options.PSEUDO_FAMILY() +@options.KPOINTS_MESH(show_default=False) +@options.PHONOPY_CODE(required=False) +@options.OVERRIDES() +@options.DAEMON() +@decorators.with_dbenv() +def launch_workflow(pw_code, structure, protocol, pseudo_family, kpoints_mesh, phonopy_code, overrides, daemon): + """Run an `PhononWorkChain`. + + It computes the force constants in the harmonic approximation. + + .. note:: this workflow does NOT computer non-analytical constants (dielectric and + Born effective charges tensors). Only the finite displacements of atoms. + """ + from aiida.plugins import WorkflowFactory + + entry_point_name = 'vibroscopy.phonons.phonon' + + if overrides: + overrides = yaml.safe_load(overrides) + + if pseudo_family: + if overrides: + overrides.setdefault('scf', {})['pseudo_family'] = pseudo_family.label + else: + overrides = { + 'scf': { + 'pseudo_family': pseudo_family.label + }, + } + + builder = WorkflowFactory(entry_point_name).get_builder_from_protocol( + pw_code=pw_code, + structure=structure, + protocol=protocol, + phonopy_code=phonopy_code, + overrides=overrides, + ) + + if kpoints_mesh: + builder.scf.pop('kpoints_distance') + builder.scf.kpoints = kpoints_mesh + + launch.launch_process(builder, daemon) diff --git a/src/aiida_vibroscopy/cli/workflows/phonons/harmonic.py b/src/aiida_vibroscopy/cli/workflows/phonons/harmonic.py new file mode 100755 index 0000000..3d1a905 --- /dev/null +++ b/src/aiida_vibroscopy/cli/workflows/phonons/harmonic.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +"""Command line scripts to launch a `HarmonicWorkChain` for testing and demonstration purposes.""" +from aiida.cmdline.utils import decorators +import click +import yaml + +from .. import cmd_launch +from ...utils import defaults, launch, options + + +@cmd_launch.command('harmonic') +@options.PW_CODE() +@options.STRUCTURE(default=defaults.get_structure) +@options.PROTOCOL(type=click.Choice(['fast', 'moderate', 'precise']), default='moderate', show_default=True) +@options.PSEUDO_FAMILY() +@options.KPOINTS_MESH(show_default=False) +@options.OVERRIDES() +@options.PHONOPY_CODE(required=False) +@options.DAEMON() +@decorators.with_dbenv() +def launch_workflow(pw_code, structure, protocol, pseudo_family, kpoints_mesh, overrides, phonopy_code, daemon): + """Run a `HarmonicWorkChain`. + + It computes force constants in the harmonic approximation, + dielectric, Born charges, Raman and non-linear optical susceptibility tensors, + to account for non-analytical behaviour of the dynamical matrix at small q-vectors. + + The output can then be used to quickly post-process and get phonons related properties, + such as IR absorption/reflectivity and Raman spectra in different experimental settings, + phonon dispersion . + """ + from aiida.plugins import WorkflowFactory + + entry_point_name = 'vibroscopy.phonons.harmonic' + + if overrides: + overrides = yaml.safe_load(overrides) + + if pseudo_family: + if overrides: + overrides.setdefault('dielectric', {}).setdefault('scf', {})['pseudo_family'] = pseudo_family.label + overrides.setdefault('phonon', {}).setdefault('scf', {})['pseudo_family'] = pseudo_family.label + else: + overrides = { + 'dielectric': { + 'scf': { + 'pseudo_family': pseudo_family.label + }, + }, + 'phonon': { + 'scf': { + 'pseudo_family': pseudo_family.label + }, + }, + } + + builder = WorkflowFactory(entry_point_name).get_builder_from_protocol( + pw_code=pw_code, + structure=structure, + protocol=protocol, + overrides=overrides, + phonopy_code=phonopy_code, + ) + + if kpoints_mesh: + builder.dielectric.pop('kpoints_parallel_distance') + builder.dielectric.scf.pop('kpoints_distance') + builder.dielectric.scf.kpoints = kpoints_mesh + + builder.phonon.scf.pop('kpoints_distance') + builder.phonon.scf.kpoints = kpoints_mesh + + launch.launch_process(builder, daemon) diff --git a/src/aiida_vibroscopy/cli/workflows/spectra/__init__.py b/src/aiida_vibroscopy/cli/workflows/spectra/__init__.py new file mode 100644 index 0000000..5c40094 --- /dev/null +++ b/src/aiida_vibroscopy/cli/workflows/spectra/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# pylint: disable=cyclic-import,unused-import,wrong-import-position,import-error +"""Module with CLI commands for various spectra workflows.""" diff --git a/src/aiida_vibroscopy/cli/workflows/spectra/iraman.py b/src/aiida_vibroscopy/cli/workflows/spectra/iraman.py new file mode 100755 index 0000000..b18d748 --- /dev/null +++ b/src/aiida_vibroscopy/cli/workflows/spectra/iraman.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +"""Command line scripts to launch a `IRamanSpectraWorkChain` for testing and demonstration purposes.""" +from aiida.cmdline.utils import decorators +import click +import yaml + +from .. import cmd_launch +from ...utils import defaults, launch, options + + +@cmd_launch.command('iraman-spectra') +@options.PW_CODE() +@options.STRUCTURE(default=defaults.get_structure) +@options.PROTOCOL(type=click.Choice(['fast', 'moderate', 'precise']), default='moderate', show_default=True) +@options.PSEUDO_FAMILY() +@options.KPOINTS_MESH(show_default=False) +@options.OVERRIDES() +@options.DAEMON() +@decorators.with_dbenv() +def launch_workflow(pw_code, structure, protocol, pseudo_family, kpoints_mesh, overrides, daemon): + """Run an `IRamanSpectraWorkChain`. + + It computes force constants, dielectric, Born charges, Raman and non-linear optical + susceptibility tensors via finite displacements and finite fields. The output can then + be used to quickly post-process and get the IR absorption/reflectivity and Raman spectra + in different experimental settings. + """ + from aiida.plugins import WorkflowFactory + + if overrides: + overrides = yaml.safe_load(overrides) + + if pseudo_family: + if overrides: + overrides.setdefault('dielectric', {}).setdefault('scf', {})['pseudo_family'] = pseudo_family.label + overrides.setdefault('phonon', {}).setdefault('scf', {})['pseudo_family'] = pseudo_family.label + else: + overrides = { + 'dielectric': { + 'scf': { + 'pseudo_family': pseudo_family.label + }, + }, + 'phonon': { + 'scf': { + 'pseudo_family': pseudo_family.label + }, + }, + } + + builder = WorkflowFactory('vibroscopy.spectra.iraman').get_builder_from_protocol( + code=pw_code, + structure=structure, + protocol=protocol, + overrides=overrides, + ) + + if kpoints_mesh: + builder.dielectric.pop('kpoints_parallel_distance') + builder.dielectric.scf.pop('kpoints_distance') + builder.dielectric.scf.kpoints = kpoints_mesh + + builder.phonon.scf.pop('kpoints_distance') + builder.phonon.scf.kpoints = kpoints_mesh + + launch.launch_process(builder, daemon) diff --git a/src/aiida_vibroscopy/common/constants.py b/src/aiida_vibroscopy/common/constants.py index 0574878..6eab6c2 100644 --- a/src/aiida_vibroscopy/common/constants.py +++ b/src/aiida_vibroscopy/common/constants.py @@ -32,7 +32,8 @@ # Lastly, we have the Born charge, corresponding to an electronic charge in atomic units. # This means that in order to obtain the units of Angstrom and Volt, we multiply by (Ha->eV * Bohr->Ang). # The exrta 'e' comes from the Ang/V = e * (Ang/eV) of Chi(2). - nlo_conversion=0.08 * np.pi * 2.0 * CONSTANTS.ry_to_ev * CONSTANTS.bohr_to_ang, + nlo_conversion=0.02 * + (2.0 * CONSTANTS.ry_to_ev * CONSTANTS.bohr_to_ang), # we remove 4pi and put it back in the cross sections cm_to_kelvin=units.CmToEv / units.Kb, thz_to_cm=units.THzToCm, # THz to cm^-1 debey_ang=1.0 / 0.2081943, # 1 Debey = 0.2081943 e * Angstrom ==> e = (1/0.2081943) D/A @@ -41,8 +42,13 @@ # > sterad^-1 cm^-2 < # The intensity must be computed using frequencies in cm^-1 and normalized eigenvectors # by atomic masses expressed in atomic mass unit (Dalton). + # IMPORTANT: still misss the units from the Dirac delta raman_xsection=1.0e24 * 1.054571817e-34 / - (2.0 * units.SpeedOfLight**4 * units.AMU * units.THzToCm**3 * 4.0 * np.pi) + (2.0 * units.SpeedOfLight**4 * units.AMU * + units.THzToCm**3), # removed 1/4pi due to convention on Chi2 for the correction + elementary_charge_si=1.602176634e-19, # elementary charge in Coulomb + electron_mass_si=units.Me, # electron mass in kg + atomic_mass_si=units.AMU, # atomic mass unit in kg # to be defined: # * kelvin to eV # * nm to eV diff --git a/src/aiida_vibroscopy/data/vibro_mixin.py b/src/aiida_vibroscopy/data/vibro_mixin.py index 2d5a0a3..af7df7d 100644 --- a/src/aiida_vibroscopy/data/vibro_mixin.py +++ b/src/aiida_vibroscopy/data/vibro_mixin.py @@ -9,6 +9,8 @@ """Mixin for aiida-vibroscopy DataTypes.""" from __future__ import annotations +from typing import Union + import numpy as np from aiida_vibroscopy.calculations.spectra_utils import ( @@ -17,6 +19,7 @@ compute_raman_space_average, compute_raman_susceptibility_tensors, ) +from aiida_vibroscopy.common import UNITS from aiida_vibroscopy.utils.integration.lebedev import LebedevScheme from aiida_vibroscopy.utils.spectra import raman_prefactor @@ -63,9 +66,7 @@ def set_raman_tensors(self, raman_tensors: list | np.ndarray): with Raman tensors we mean :math:`\frac{1}{\Omega}\frac{\partial \chi}{\partial u}` - .. note:: - * Units in 1/Angstrom, normalized using the UNIT cell volume. * The shape should match the primitive cell. * Indices are as follows: @@ -74,7 +75,6 @@ def set_raman_tensors(self, raman_tensors: list | np.ndarray): 3. Polarization index (i.e. referring to electric field derivative). 4. Same as 3. - :param raman_tensors: (number of atoms in the primitive cell, 3, 3, 3) shape array :raises: * TypeError: if the format is not compatible or of the correct type @@ -152,7 +152,7 @@ def run_active_modes( Inputs as in :func:`~aiida_vibroscopy.calculations.spectra_utils.compute_active_modes` :param nac_direction: (3,) shape list, indicating non analytical - direction in fractional reciprocal (primitive cell) space coordinates + direction in Cartesian coordinates :param selection_rule: str, can be `raman` or `ir`; it uses symmetry in the selection of the modes for a specific type of process. @@ -185,7 +185,7 @@ def run_active_modes( def run_raman_susceptibility_tensors( self, - nac_direction: tuple[float, float, float] = lambda: [0, 0, 0], + nac_direction: tuple[float, float, float] | None = None, with_nlo: bool = True, use_irreps: bool = True, degeneracy_tolerance: float = 1e-5, @@ -201,8 +201,8 @@ def run_raman_susceptibility_tensors( * Raman susceptibility tensors: Anstrom/AMU * Frequencies: cm-1 - :param nac_direction: non-analytical direction in fractional coordinates (unitcell cell) - in reciprocal space; (3,) shape list or numpy.ndarray + :param nac_direction: non-analytical direction in Cartesian coordinates; + (3,) shape list or numpy.ndarray :param with_nlo: whether to use or not non-linear optical susceptibility correction (Froehlich term), defaults to True :param use_irreps: whether to use irreducible representations @@ -221,11 +221,6 @@ def run_raman_susceptibility_tensors( :return: tuple of numpy.ndarray (Raman susc. tensors, frequencies, irreps labels) """ - try: - nac_direction = nac_direction() - except TypeError: - pass - if not isinstance(with_nlo, bool) or not isinstance(use_irreps, bool) or not isinstance(sum_rules, bool): raise TypeError('the input is not of the correct type') @@ -255,7 +250,7 @@ def run_raman_susceptibility_tensors( def run_polarization_vectors( self, - nac_direction: tuple[float, float, float] = lambda: [0, 0, 0], + nac_direction: tuple[float, float, float] | None = None, use_irreps: bool = True, degeneracy_tolerance: float = 1e-5, asr_sum_rules: bool = False, @@ -272,8 +267,8 @@ def run_polarization_vectors( * Frequencies: cm-1 - :param nac_direction: non-analytical direction in fractional coordinates (unitcell cell) - in reciprocal space; space(3,) shape :class:`list` or :class:`numpy.ndarray` + :param nac_direction: non-analytical direction in Cartesian coordinates; + (3,) shape :class:`list` or :class:`numpy.ndarray` :param use_irreps: whether to use irreducible representations in the selection of modes, defaults to True :param asr_sum_rules: whether to apply acoustic sum rules to the force constants @@ -290,10 +285,6 @@ def run_polarization_vectors( :return: tuple of :class:`numpy.ndarray` (polarization vectors, frequencies, irreps labels) """ - try: - nac_direction = nac_direction() - except TypeError: - pass if not isinstance(use_irreps, bool) or not isinstance(asr_sum_rules, bool): raise TypeError('the input is not of the correct type') @@ -334,10 +325,10 @@ def run_single_crystal_raman_intensities( * Frequencies: cm-1 :param pol_incoming: light polarization vector of the incoming light - (laser) in crystal/fractional coordinates of the unitcell cell; + (laser) in Cartesian coordinates; :class:`list` or :class:`numpy.ndarray` of shape (3,) :param pol_outgoing: light polarization vector of the outgoing light - (scattered) in crystal/fractional coordinates of the unitcell cell; + (scattered) in Cartesian coordinates; :class:`list` or :class:`numpy.ndarray` of shape (3,) :param frequency_laser: laser frequency in nanometers :param temperature: temperature in Kelvin @@ -348,7 +339,7 @@ def run_single_crystal_raman_intensities( * with_nlo: whether to use or not non-linear optical susceptibility correction (Froehlich term), defaults to True * nac_direction: - non-analytical direction in reciprocal space coordinates (unitcell cell) + non-analytical direction in Cartesian coordinates * use_irreps: whether to use irreducible representations in the selection of modes, defaults to True; bool, optional @@ -378,9 +369,11 @@ def run_single_crystal_raman_intensities( if pol_incoming_crystal.shape != (3,) or pol_outgoing_crystal.shape != (3,): raise ValueError('the array is not of the correct shape') - cell = self.get_phonopy_instance().unitcell.cell - pol_incoming_cart = np.dot(cell.T, pol_incoming_crystal) # in Cartesian coordinates - pol_outgoing_cart = np.dot(cell.T, pol_outgoing_crystal) # in Cartesian coordinates + # cell = self.get_phonopy_instance().unitcell.cell + # pol_incoming_cart = np.dot(cell.T, pol_incoming_crystal) # in Cartesian coordinates + # pol_outgoing_cart = np.dot(cell.T, pol_outgoing_crystal) # in Cartesian coordinates + pol_incoming_cart = pol_incoming_crystal # in Cartesian coordinates + pol_outgoing_cart = pol_outgoing_crystal # in Cartesian coordinates raman_susceptibility_tensors, freqs, labels = self.run_raman_susceptibility_tensors(**kwargs) @@ -420,7 +413,7 @@ def run_powder_raman_intensities( * with_nlo: whether to use or not non-linear optical susceptibility correction (Froehlich term), defaults to True * nac_direction: - non-analytical direction in reciprocal space coordinates (unitcell cell) + non-analytical direction in Cartesian coordinates * use_irreps: whether to use irreducible representations in the selection of modes, defaults to True; bool, optional @@ -448,7 +441,7 @@ def run_powder_raman_intensities( raman_hh, raman_hv = compute_raman_space_average(raman_susceptibility_tensors=raman_susceptibility_tensors) else: - cell = self.get_phonopy_instance().unitcell.cell + # cell = self.get_phonopy_instance().unitcell.cell scheme = LebedevScheme.from_order(quadrature_order) points = scheme.points.T @@ -460,9 +453,9 @@ def run_powder_raman_intensities( kwargs.pop('nac_direction', None) for q, ws in zip(points, weights): - q_crystal = np.dot(cell, q) # in reciprocal fractional/crystal coordinates + # q_crystal = np.dot(cell, q) # in reciprocal fractional/crystal coordinates q_tensors, q_freqs, q_labels = self.run_raman_susceptibility_tensors( - nac_direction=q_crystal, + nac_direction=q, **kwargs, ) @@ -486,13 +479,13 @@ def run_single_crystal_ir_intensities(self, pol_incoming: tuple[float, float, fl * Frequencies: cm^-1 :param pol_incoming: light polarization vector of the - incident beam light in crystal coordinates of the unitcell cell; + incident beam light in Cartesian coordinates; :class:`list` or :class:`numpy.ndarray` of shape (3,) :param kwargs: keys of :func:`~aiida_vibroscopy.data.vibro_mixing.VibrationalMixin.compute_polarization_vectors` method * nac_direction: - non-analytical direction in reciprocal space coordinates (unitcell cell) + non-analytical direction in Cartesian coordinates * use_irreps: whether to use irreducible representations in the selection of modes, defaults to True @@ -527,8 +520,9 @@ def run_single_crystal_ir_intensities(self, pol_incoming: tuple[float, float, fl if pol_incoming_crystal.shape != (3,): raise ValueError('the array is not of the correct shape') - cell = self.get_phonopy_instance().unitcell.cell - pol_incoming_cart = np.dot(cell.T, pol_incoming_crystal) # in Cartesian coordinates + # cell = self.get_phonopy_instance().unitcell.cell + # pol_incoming_cart = np.dot(cell.T, pol_incoming_crystal) # in Cartesian coordinates + pol_incoming_cart = pol_incoming_crystal # in Cartesian coordinates pol_vectors, freqs, labels = self.run_polarization_vectors(**kwargs) @@ -551,7 +545,7 @@ def run_powder_ir_intensities(self, quadrature_order: int | None = None, **kwarg :func:`~aiida_vibroscopy.data.vibro_mixing.VibrationalMixin.compute_polarization_vectors` method * nac_direction: - non-analytical direction in reciprocal space coordinates (unitcell cell) + non-analytical direction in Cartesian coordinates * use_irreps: whether to use irreducible representations in the selection of modes, defaults to True; bool, optional @@ -591,9 +585,9 @@ def run_powder_ir_intensities(self, quadrature_order: int | None = None, **kwarg kwargs.pop('nac_direction', None) for q, ws in zip(points, weights): - cell = self.get_phonopy_instance().unitcell.cell - q_crystal = np.dot(cell.T, q) # in reciprocal fractional/Crystal coordinates - q_pol, q_freqs, q_labels = self.run_polarization_vectors(**kwargs, **{'nac_direction': q_crystal}) + # cell = self.get_phonopy_instance().unitcell.cell + # q_crystal = np.dot(cell, q) # in reciprocal fractional/Crystal coordinates + q_pol, q_freqs, q_labels = self.run_polarization_vectors(**kwargs, **{'nac_direction': q}) for pol, f, l in zip(q_pol, q_freqs, q_labels): ir_intensities.append(ws * np.dot(pol, pol)) @@ -602,8 +596,127 @@ def run_powder_ir_intensities(self, quadrature_order: int | None = None, **kwarg return (np.array(ir_intensities) / np.array(freqs), np.array(freqs), labels) + def run_complex_dielectric_function( + self, + freq_range: Union[str, np.ndarray] = 'auto', + gammas: float | list[float] = 12.0, + nac_direction: None | list[float, float, float] = None, + use_irreps: bool = True, + degeneracy_tolerance: float = 1e-5, + sum_rules: bool = False, + **kwargs, + ) -> np.ndarray: + """Return the frequency dependent complex dielectric function (tensor). + + :param freq_range: frequency range in cm^-1; set to `auto` for automatic choice + :param gammas: list or single value of broadenings, i.e. full width at half maximum (FWHM) + :param nac_direction: (3,) shape list, indicating non analytical + direction in Cartesian coordinates + :param use_irreps: whether to use irreducible representations + in the selection of modes, defaults to True + :param degeneracy_tolerance: degeneracy tolerance + for irreducible representation + :param sum_rules: whether to apply charge neutrality to effective charges + :param kwargs: see also the :func:`~aiida_phonopy.data.phonopy.get_phonopy_instance` method + + * subtract_residual_forces: + whether or not subract residual forces (if set); + bool, defaults to False + * symmetrize_nac: + whether or not to symmetrize the nac parameters + using point group symmetry; bool, defaults to self.is_symmetry + + :return: (3, 3, num steps) shape :class:`numpy.ndarray`, `num steps` refers to the + number of frequency steps where the complex dielectric function is evaluated + """ + from aiida_vibroscopy.calculations.spectra_utils import compute_complex_dielectric + + phonopy_instance = self.get_phonopy_instance(**kwargs) + + if phonopy_instance.force_constants is None: + phonopy_instance.produce_force_constants() + + return compute_complex_dielectric( + phonopy_instance=phonopy_instance, + freq_range=freq_range, + gammas=gammas, + nac_direction=nac_direction, + use_irreps=use_irreps, + degeneracy_tolerance=degeneracy_tolerance, + sum_rules=sum_rules, + ) + + def run_normal_reflectivity_spectrum(self, q_direction: int, **kwargs) -> np.ndarray: + """Return the normal reflectivity spectrum in the infrared regime. + + :param q_direction: orthogonal direction index of the complex dielectric function tensor probed + :param kwargs: see the arguments of + :func:`~aiida_vibroscopy.data.vibro_mixing.VibrationalMixin.run_complex_dielectric_function` + :return: (frequency points, reflectance value) shape :class:`numpy.ndarray` + """ + complex_diel = self.run_complex_dielectric_function(**kwargs) + q_eps_q = np.tensordot(q_direction, np.tensordot(complex_diel, q_direction, (1, 0)), (0, 0)) + return np.abs((np.sqrt(q_eps_q) - 1) / (np.sqrt(q_eps_q) + 1))**2 + @staticmethod def get_available_quadrature_order_schemes(): """Return the available orders for quadrature integration on the nac direction unitary sphere.""" from aiida_vibroscopy.utils.integration.lebedev import get_available_quadrature_order_schemes get_available_quadrature_order_schemes() + + def run_clamped_pockels_tensor( + self, + nac_direction: tuple[float, float, float] = lambda: [0, 0, 0], + imaginary_thr: float = -5.0 / UNITS.thz_to_cm, + skip_frequencies: int = 3, + asr_sum_rules: bool = False, + symmetrize_fc: bool = False, + **kwargs, + ) -> np.ndarray: + """Compute the clamped Pockels tensor in Cartesian coordinates. + + .. note:: Units are in pm/V + + :param nac_direction: non-analytical direction in Cartesian coordinates; + (3,) shape :class:`list` or :class:`numpy.ndarray` + :param degeneracy_tolerance: degeneracy tolerance for irreducible representation + :param imaginary_thr: threshold for activating warnings on negative frequencies (in Hz) + :param skip_frequencies: number of frequencies to not include (i.e. the acoustic modes) + :param asr_sum_rules: whether to apply acoustic sum rules to the force constants + :param symmetrize_fc: whether to symmetrize the force constants using space group + :param kwargs: see also the :func:`~aiida_phonopy.data.phonopy.get_phonopy_instance` method + + * subtract_residual_forces: + whether or not subract residual forces (if set); + bool, defaults to False + * symmetrize_nac: + whether or not to symmetrize the nac parameters + using point group symmetry; bool, defaults to self.is_symmetry + + :return: tuple of (r_ion + r_el, r_el, r_ion), each having (3, 3, 3) shape array + """ + from aiida_vibroscopy.calculations.spectra_utils import compute_clamped_pockels_tensor + + if not isinstance(symmetrize_fc, bool) or not isinstance(asr_sum_rules, bool): + raise TypeError('the input is not of the correct type') + + phonopy_instance = self.get_phonopy_instance(**kwargs) + + if phonopy_instance.force_constants is None: + phonopy_instance.produce_force_constants() + + if asr_sum_rules: + phonopy_instance.symmetrize_force_constants() + if symmetrize_fc: + phonopy_instance.symmetrize_force_constants_by_space_group() + + results = compute_clamped_pockels_tensor( + phonopy_instance=phonopy_instance, + raman_tensors=self.raman_tensors, + nlo_susceptibility=self.nlo_susceptibility, + nac_direction=nac_direction, + imaginary_thr=imaginary_thr, + skip_frequencies=skip_frequencies, + ) + + return results diff --git a/src/aiida_vibroscopy/utils/broadenings.py b/src/aiida_vibroscopy/utils/broadenings.py index f7960f4..682698a 100644 --- a/src/aiida_vibroscopy/utils/broadenings.py +++ b/src/aiida_vibroscopy/utils/broadenings.py @@ -98,7 +98,7 @@ def voigt_profile(x_range: np.ndarray, peak: float, intensity: float, gamma_lore eta_I = 0. eta_P = 0. # - for index, _ in enumerate(list_a): + for index, i in enumerate(list_a): i = index #fortran convention w_G = w_G - rho * list_a[index] * (rho**i) w_L = w_L - (1. - rho) * list_b[index] * (rho**i) diff --git a/src/aiida_vibroscopy/utils/validation.py b/src/aiida_vibroscopy/utils/validation.py index 8b4c376..48057dd 100644 --- a/src/aiida_vibroscopy/utils/validation.py +++ b/src/aiida_vibroscopy/utils/validation.py @@ -9,11 +9,11 @@ """Validation function utilities.""" from __future__ import annotations -__all__ = ('validate_tot_magnetization', 'validate_matrix', 'validate_positive', 'validate_nac') +__all__ = ('validate_tot_magnetization', 'validate_matrix', 'validate_positive') def validate_tot_magnetization(tot_magnetization: float, thr: float = 0.2) -> bool: - """Round the total magnetization input and return true if within threshold. + """Round the total magnetization input and return true if outside the threshold. This is needed because 'tot_magnetization' must be an integer in the aiida-quantumespresso input parameters. """ @@ -53,10 +53,10 @@ def validate_positive(value, _): return 'specified value is negative.' -def validate_nac(value, _): - """Validate that `value` is a valid non-analytical ArrayData input.""" - try: - value.get_array('dielectric') - value.get_array('born_charges') - except KeyError: - return 'data does not contain `dieletric` and/or `born_charges` arraynames.' +# def validate_nac(value, _): +# """Validate that `value` is a valid non-analytical ArrayData input.""" +# try: +# value.get_array('dielectric') +# value.get_array('born_charges') +# except KeyError: +# return 'data does not contain `dieletric` and/or `born_charges` arraynames.' diff --git a/src/aiida_vibroscopy/workflows/dielectric/base.py b/src/aiida_vibroscopy/workflows/dielectric/base.py index c6a5097..5ac115a 100644 --- a/src/aiida_vibroscopy/workflows/dielectric/base.py +++ b/src/aiida_vibroscopy/workflows/dielectric/base.py @@ -144,7 +144,7 @@ def define(cls, spec): non_db=True, validator=cls._validate_properties, help=( - 'Valid inputs are: \n \n * '.join(f'{flag_name}' for flag_name in cls._AVAILABLE_PROPERTIES) + 'Valid inputs are:'+'\n * '.join(f'{flag_name}' for flag_name in cls._AVAILABLE_PROPERTIES) ) ) spec.input( @@ -185,7 +185,8 @@ def define(cls, spec): ) spec.input( 'central_difference.diagonal_scale', valid_type=orm.Float, default=lambda: orm.Float(1/np.sqrt(2)), - help='Scaling factor for electric fields non parallel to cartesiaan axis (i.e. E --> scale*E).', + help=('Scaling factor for electric fields non-parallel to cartesiaan axis (e.g. scale*(E,E,O)). ' + '1/Sqrt(2) guarantees the same norm in all directions (recommended).'), validator=validate_positive, ) spec.input( diff --git a/src/aiida_vibroscopy/workflows/dielectric/numerical_derivatives.py b/src/aiida_vibroscopy/workflows/dielectric/numerical_derivatives.py index d5f079f..af4743c 100644 --- a/src/aiida_vibroscopy/workflows/dielectric/numerical_derivatives.py +++ b/src/aiida_vibroscopy/workflows/dielectric/numerical_derivatives.py @@ -49,15 +49,15 @@ class NumericalDerivativesWorkChain(WorkChain): To understand, let's review the approach.In central differencs approach we need the evaluation of the function at the value we want - the derivative (in our case at :math:`\mathcal{E}=0`, + the derivative (in our case at :math:`\\mathcal{E}=0`, E is the electric field), and at displaced positions from this value. The evaluation of the function at these points will have weights (or coefficients), which depend on order and accuracy. For example: - - :math:`\frac{df}{dx} = \frac{ 0.5 \cdot f(+1.0 \cdot h) -0.5 \cdot f(-1.0 \cdot h) }{h} +\mathcal{O}(h^2)` - - :math:`\frac{d^2 f}{dx^2} = \frac{ 1.0 \cdot f(+1.0 \cdot h) -2.0 \cdot f(0. \cdot h) +1.0 \cdot f(-1.0 \cdot h) }{h^2} +\mathcal{O}(h^2)` + - :math:`\\frac{df}{dx} = \\frac{ 0.5 \\cdot f(+1.0 \\cdot h) -0.5 \\cdot f(-1.0 \\cdot h) }{h} +\\mathcal{O}(h^2)` + - :math:`\\frac{d^2 f}{dx^2} = \\frac{ 1.0 \\cdot f(+1.0 \\cdot h) -2.0 \\cdot f(0. \\cdot h) +1.0 \\cdot f(-1.0 \\cdot h) }{h^2} +\\mathcal{O}(h^2)` Referring to the coefficients for each step as :math:`c_i`, where `i` is an integer, our convention is @@ -71,7 +71,7 @@ class NumericalDerivativesWorkChain(WorkChain): | ... This way to creating an analogous of an array with - coefficients :math:`[c_1,c_{-1},c_2,c_{-2}, \dots]`. + coefficients :math:`[c_1,c_{-1},c_2,c_{-2}, \\dots]`. These dictionaries are going to be put as sub-dictionary in a general `data` dictionary. Each sub-dict @@ -123,7 +123,7 @@ def define(cls, spec): validator=validate_positive, ) spec.input( - 'central_difference.diagonal_scale', valid_type=orm.Float, default=lambda: orm.Float(1/np.sqrt(2)), + 'central_difference.diagonal_scale', valid_type=orm.Float, default=lambda: orm.Float(1./np.sqrt(2)), help='Scaling factor for electric fields non parallel to cartesiaan axis (i.e. E --> scale*E).', validator=validate_positive, ) diff --git a/src/aiida_vibroscopy/workflows/phonons/base.py b/src/aiida_vibroscopy/workflows/phonons/base.py index 047ab89..ecfd191 100644 --- a/src/aiida_vibroscopy/workflows/phonons/base.py +++ b/src/aiida_vibroscopy/workflows/phonons/base.py @@ -14,7 +14,7 @@ from aiida import orm from aiida.common.extendeddicts import AttributeDict from aiida.common.lang import type_check -from aiida.engine import WorkChain, calcfunction, if_ +from aiida.engine import WorkChain, calcfunction, if_, while_ from aiida.plugins import CalculationFactory, DataFactory, WorkflowFactory from aiida_quantumespresso.calculations.functions.create_kpoints_from_distance import create_kpoints_from_distance from aiida_quantumespresso.workflows.protocols.utils import ProtocolMixin @@ -133,6 +133,10 @@ def define(cls, spec): 'settings.sleep_submission_time', valid_type=(int, float), non_db=True, default=3.0, help='Time in seconds to wait before submitting subsequent displaced structure scf calculations.', ) + spec.input( + 'settings.max_concurrent_base_workchains', valid_type=int, non_db=True, default=20, + help='Maximum number of concurrent running `PwBaseWorkChain`.' + ) spec.input( 'clean_workdir', valid_type=orm.Bool, default=lambda: orm.Bool(False), help='If `True`, work directories of all called calculation will be cleaned at the end of execution.' @@ -143,7 +147,10 @@ def define(cls, spec): cls.set_reference_kpoints, cls.run_base_supercell, cls.inspect_base_supercell, - cls.run_forces, + cls.run_supercells, + while_(cls.should_run_forces)( + cls.run_forces, + ), cls.inspect_all_runs, cls.set_phonopy_data, if_(cls.should_run_phonopy)( @@ -289,7 +296,7 @@ def set_ctx_variables(self): """Set `is_magnetic` and hubbard-related context variables.""" parameters = self.inputs.scf.pw.parameters.get_dict() nspin = parameters.get('SYSTEM', {}).get('nspin', 1) - self.ctx.is_magnetic = (nspin != 1) + self.ctx.is_magnetic = nspin != 1 self.ctx.is_insulator = True self.ctx.plus_hubbard = False self.ctx.old_plus_hubbard = False @@ -379,8 +386,8 @@ def inspect_base_supercell(self): fermi_energy = parameters.fermi_energy self.ctx.is_insulator, _ = orm.find_bandgap(bands, fermi_energy=fermi_energy) - def run_forces(self): - """Run an scf for each supercell with displacements.""" + def run_supercells(self): + """Run supercell with displacements.""" if self.ctx.plus_hubbard or self.ctx.old_plus_hubbard: supercells = get_supercells_for_hubbard( preprocess_data=self.ctx.preprocess_data, @@ -389,12 +396,27 @@ def run_forces(self): else: supercells = self.ctx.preprocess_data.calcfunctions.get_supercells_with_displacements() + self.ctx.supercells = [] + for key, value in supercells.items(): + self.ctx.supercells.append((key, value)) + self.out('supercells', supercells) + def should_run_forces(self): + """Whether to run or not forces.""" + return len(self.ctx.supercells) > 0 + + def run_forces(self): + """Run an scf for each supercell with displacements.""" base_key = f'{self._RUN_PREFIX}_0' base_out = self.ctx[base_key].outputs - for key, supercell in supercells.items(): + n_base_parallel = self.inputs.settings.max_concurrent_base_workchains + if self.inputs.settings.max_concurrent_base_workchains < 0: + n_base_parallel = len(self.ctx.supercells) + + for _ in self.ctx.supercells[:n_base_parallel]: + key, supercell = self.ctx.supercells.pop(0) num = key.split('_')[-1] label = f'{self._RUN_PREFIX}_{num}' @@ -451,7 +473,7 @@ def inspect_all_runs(self): else: self.report( f'PwBaseWorkChain with failed' - 'with exit status {workchain.exit_status}' + f'with exit status {workchain.exit_status}' ) failed_runs.append(workchain.pk) diff --git a/src/aiida_vibroscopy/workflows/phonons/harmonic.py b/src/aiida_vibroscopy/workflows/phonons/harmonic.py index 5d30062..5d038b6 100644 --- a/src/aiida_vibroscopy/workflows/phonons/harmonic.py +++ b/src/aiida_vibroscopy/workflows/phonons/harmonic.py @@ -291,6 +291,7 @@ def run_phonon(self): key = 'phonon' inputs = AttributeDict(self.exposed_inputs(PhononWorkChain, namespace=key)) inputs.scf.pw.structure = self.inputs.structure + inputs.symmetry = self.inputs.symmetry inputs.metadata.call_link_label = key future = self.submit(PhononWorkChain, **inputs) @@ -302,6 +303,7 @@ def run_dielectric(self): key = 'dielectric' inputs = AttributeDict(self.exposed_inputs(DielectricWorkChain, namespace=key)) inputs.scf.pw.structure = self.inputs.structure + inputs.symmetry = self.inputs.symmetry if self.inputs.settings.use_primitive_cell: inputs.scf.pw.structure = self.ctx.preprocess_data.calcfunctions.get_primitive_cell() diff --git a/tests/calculations/chi2_BTO.npy b/tests/calculations/chi2_BTO.npy new file mode 100644 index 0000000000000000000000000000000000000000..17f011407e5c8b8063fe4c2979099296f7733b78 GIT binary patch literal 344 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I%oItn19siRPz)uWeRK~@ z9>&)R1It_DQqKoAzhRHU+|?lS_p5RRRe{Wh>4)+4!Ri)R<{iwN|M`!R1JqdzSO0@R gz9mfkoX2NTu3;+NC literal 0 HcmV?d00001 diff --git a/tests/calculations/phonopy_BTO.yaml b/tests/calculations/phonopy_BTO.yaml new file mode 100644 index 0000000..d5dd9e8 --- /dev/null +++ b/tests/calculations/phonopy_BTO.yaml @@ -0,0 +1,232 @@ +phonopy: + version: "2.20.0" + frequency_unit_conversion_factor: 15.633302 + symmetry_tolerance: 1.00000e-05 + +space_group: + type: "R3m" + number: 160 + Hall_symbol: "R 3 -2\"" + +primitive_matrix: +- [ 1.000000000000000, 0.000000000000000, 0.000000000000000 ] +- [ 0.000000000000000, 1.000000000000000, 0.000000000000000 ] +- [ -0.000000000000000, 0.000000000000000, 1.000000000000000 ] + +supercell_matrix: +- [ 1, 0, 0 ] +- [ 0, 1, 0 ] +- [ 0, 0, 1 ] + +primitive_cell: + lattice: + - [ 2.828233390000000, -1.632881309999999, 2.317152100000000 ] # a + - [ -0.000000000000000, 3.265762620000000, 2.317152100000000 ] # b + - [ -2.828233390000000, -1.632881309999999, 2.317152100000000 ] # c + points: + - symbol: Ti # 1 + coordinates: [ 0.485874881699997, 0.485874881699997, 0.485874881699997 ] + mass: 47.867000 + - symbol: O # 2 + coordinates: [ 0.511802210799999, 0.511802210799994, 0.020713059299988 ] + mass: 15.999400 + - symbol: O # 3 + coordinates: [ 0.511802210799992, 0.020713059299997, 0.511802210799992 ] + mass: 15.999400 + - symbol: O # 4 + coordinates: [ 0.020713059299988, 0.511802210799994, 0.511802210799999 ] + mass: 15.999400 + - symbol: Ba # 5 + coordinates: [ 0.998107737500000, 0.998107737500000, 0.998107737500000 ] + mass: 137.327000 + reciprocal_lattice: # without 2pi + - [ 0.176788804547704, -0.102069063835795, 0.143854748824358 ] # a* + - [ 0.000000000000000, 0.204138127671590, 0.143854748824358 ] # b* + - [ -0.176788804547704, -0.102069063835795, 0.143854748824358 ] # c* + +unit_cell: + lattice: + - [ 2.828233390000000, -1.632881310000000, 2.317152100000000 ] # a + - [ 0.000000000000000, 3.265762620000000, 2.317152100000000 ] # b + - [ -2.828233390000000, -1.632881310000000, 2.317152100000000 ] # c + points: + - symbol: Ti # 1 + coordinates: [ 0.485874881699997, 0.485874881699997, 0.485874881699997 ] + mass: 47.867000 + reduced_to: 1 + - symbol: O # 2 + coordinates: [ 0.511802210799999, 0.511802210799994, 0.020713059299988 ] + mass: 15.999400 + reduced_to: 2 + - symbol: O # 3 + coordinates: [ 0.511802210799992, 0.020713059299997, 0.511802210799992 ] + mass: 15.999400 + reduced_to: 3 + - symbol: O # 4 + coordinates: [ 0.020713059299988, 0.511802210799994, 0.511802210799999 ] + mass: 15.999400 + reduced_to: 4 + - symbol: Ba # 5 + coordinates: [ -0.001892262500000, -0.001892262500000, -0.001892262500000 ] + mass: 137.327000 + reduced_to: 5 + +supercell: + lattice: + - [ 2.828233390000000, -1.632881310000000, 2.317152100000000 ] # a + - [ 0.000000000000000, 3.265762620000000, 2.317152100000000 ] # b + - [ -2.828233390000000, -1.632881310000000, 2.317152100000000 ] # c + points: + - symbol: Ti # 1 + coordinates: [ 0.485874881699997, 0.485874881699997, 0.485874881699997 ] + mass: 47.867000 + reduced_to: 1 + - symbol: O # 2 + coordinates: [ 0.511802210799999, 0.511802210799994, 0.020713059299988 ] + mass: 15.999400 + reduced_to: 2 + - symbol: O # 3 + coordinates: [ 0.511802210799992, 0.020713059299997, 0.511802210799992 ] + mass: 15.999400 + reduced_to: 3 + - symbol: O # 4 + coordinates: [ 0.020713059299988, 0.511802210799994, 0.511802210799999 ] + mass: 15.999400 + reduced_to: 4 + - symbol: Ba # 5 + coordinates: [ 0.998107737500000, 0.998107737500000, 0.998107737500000 ] + mass: 137.327000 + reduced_to: 5 + +nac: + born_effective_charge: + - # 1 (Ti) + - [ 6.373865475363008, 0.000000000000000, 0.000000000000000 ] + - [ -0.000000000000000, 6.373865475363008, -0.000000000000000 ] + - [ 0.000000000000000, -0.000000000000000, 5.290820424211163 ] + - # 2 (O) + - [ -3.611509378547914, -0.965953271260393, 1.191583321899472 ] + - [ -0.965953272387218, -2.496122615523766, 0.687960952061796 ] + - [ 1.009727966344990, 0.582966712837546, -2.673797013836315 ] + - # 3 (O) + - [ -1.938429234011692, 0.000000000000000, 0.000000000000000 ] + - [ 0.000000000000000, -4.169202760059988, -1.375921904123592 ] + - [ 0.000000000000000, -1.165933425675092, -2.673797013836315 ] + - # 4 (O) + - [ -3.611509378547914, 0.965953271260393, -1.191583321899472 ] + - [ 0.965953272387218, -2.496122615523766, 0.687960952061796 ] + - [ -1.009727966344990, 0.582966712837546, -2.673797013836315 ] + - # 5 (Ba) + - [ 2.787582515744512, -0.000000000000000, -0.000000000000000 ] + - [ 0.000000000000000, 2.787582515744512, 0.000000000000000 ] + - [ 0.000000000000000, -0.000000000000000, 2.730570617297783 ] + dielectric_constant: + - [ 5.854114531326300, 0.000000000000000, -0.000000000000000 ] + - [ -0.000000000000000, 5.854114531326300, -0.000000000000000 ] + - [ 0.000000000000000, -0.000000000000000, 5.353224056590000 ] + unit_conversion_factor: 14.399652 + +force_constants: + format: "full" + shape: [ 5, 5 ] + elements: + - # (1, 1) + - [ 7.803908830786140, -0.000000000000000, -0.000000000000000 ] + - [ -0.000000000000000, 7.803908829808064, -0.000000000000000 ] + - [ -0.000000000000000, -0.000000000000000, 10.038115969545229 ] + - # (1, 2) + - [ -1.273261932926982, 0.216043730094502, -0.248630370855589 ] + - [ 0.216043730019048, -1.522727745827036, -0.143546811542143 ] + - [ 0.690467341171053, 0.398641505565644, -2.184987743436668 ] + - # (1, 3) + - [ -1.647460651691167, 0.000000000863855, -0.000000000072468 ] + - [ 0.000000000917476, -1.148529027275805, 0.287093622958768 ] + - [ 0.000000000290330, -0.797283010628421, -2.184987743436667 ] + - # (1, 4) + - [ -1.273261934469659, -0.216043731038789, 0.248630370928057 ] + - [ -0.216043730856093, -1.522727744284358, -0.143546811416625 ] + - [ -0.690467341461382, 0.398641505062777, -2.184987743436667 ] + - # (1, 5) + - [ -3.609924311698332, 0.000000000080432, 0.000000000000000 ] + - [ -0.000000000080432, -3.609924312420865, -0.000000000000000 ] + - [ -0.000000000000000, 0.000000000000000, -3.483152739235227 ] + - # (2, 1) + - [ -1.273261932926982, 0.216043730019048, 0.690467341171053 ] + - [ 0.216043730094502, -1.522727745827036, 0.398641505565644 ] + - [ -0.248630370855589, -0.143546811542143, -2.184987743436668 ] + - # (2, 2) + - [ 8.483370305222660, 1.776974542376888, -1.958390128082150 ] + - [ 1.776974542376888, 6.431496842451849, -1.130677067352838 ] + - [ -1.958390128082150, -1.130677067352838, 6.965008966327693 ] + - # (2, 3) + - [ -1.685950876437615, -2.033666474137681, 1.354881965690746 ] + - [ -1.465600246386893, -3.706253458399304, -1.139296905932688 ] + - [ 1.664101046058503, -0.603713747518442, -2.006650676507376 ] + - # (2, 4) + - [ -4.716404754695410, 0.284033112776078, -0.309219080652899 ] + - [ -0.284033114965023, -0.675799583682251, 1.743010653269560 ] + - [ 0.309219080667572, 1.743010653607285, -2.006650676507376 ] + - # (2, 5) + - [ -0.807752741162654, -0.243384911034334, 0.222259901873250 ] + - [ -0.243384911119474, -0.526716054543258, 0.128321814450322 ] + - [ 0.233700372211665, 0.134926972806138, -0.766719869876274 ] + - # (3, 1) + - [ -1.647460651691167, 0.000000000917476, 0.000000000290330 ] + - [ 0.000000000863855, -1.148529027275805, -0.797283010628421 ] + - [ -0.000000000072468, 0.287093622958768, -2.184987743436667 ] + - # (3, 2) + - [ -1.685950876437615, -1.465600246386893, 1.664101046058503 ] + - [ -2.033666474137681, -3.706253458399304, -0.603713747518442 ] + - [ 1.354881965690746, -1.139296905932688, -2.006650676507376 ] + - # (3, 3) + - [ 5.405560116941441, -0.000000002691381, -0.000000000280844 ] + - [ -0.000000002691381, 9.509307027153861, 2.261354134219239 ] + - [ -0.000000000280844, 2.261354134219239, 6.965008966327692 ] + - # (3, 4) + - [ -1.685950878333296, 1.465600247481365, -1.664101046358318 ] + - [ 2.033666475232152, -3.706253456503623, -0.603713747674598 ] + - [ -1.354881965405605, -1.139296905751118, -2.006650676507375 ] + - # (3, 5) + - [ -0.386197710479364, 0.000000000679434, 0.000000000290330 ] + - [ 0.000000000733055, -0.948271084975129, -0.256643628397778 ] + - [ 0.000000000068170, -0.269853945494201, -0.766719869876273 ] + - # (4, 1) + - [ -1.273261934469659, -0.216043730856093, -0.690467341461382 ] + - [ -0.216043731038789, -1.522727744284358, 0.398641505062777 ] + - [ 0.248630370928057, -0.143546811416625, -2.184987743436667 ] + - # (4, 2) + - [ -4.716404754695410, -0.284033114965023, 0.309219080667572 ] + - [ 0.284033112776078, -0.675799583682251, 1.743010653607285 ] + - [ -0.309219080652899, 1.743010653269560, -2.006650676507376 ] + - # (4, 3) + - [ -1.685950878333296, 2.033666475232152, -1.354881965405605 ] + - [ 1.465600247481365, -3.706253456503623, -1.139296905751118 ] + - [ -1.664101046358318, -0.603713747674598, -2.006650676507375 ] + - # (4, 4) + - [ 8.483370309884270, -1.776974539685505, 1.958390128362995 ] + - [ -1.776974539685505, 6.431496837790240, -1.130677066866400 ] + - [ 1.958390128362995, -1.130677066866400, 6.965008966327692 ] + - # (4, 5) + - [ -0.807752742385904, 0.243384910274468, -0.222259902163580 ] + - [ 0.243384910466851, -0.526716053320007, 0.128321813947456 ] + - [ -0.233700372279835, 0.134926972688063, -0.766719869876273 ] + - # (5, 1) + - [ -3.609924311698332, -0.000000000080432, -0.000000000000000 ] + - [ 0.000000000080432, -3.609924312420865, 0.000000000000000 ] + - [ 0.000000000000000, -0.000000000000000, -3.483152739235227 ] + - # (5, 2) + - [ -0.807752741162654, -0.243384911119474, 0.233700372211665 ] + - [ -0.243384911034334, -0.526716054543258, 0.134926972806138 ] + - [ 0.222259901873250, 0.128321814450322, -0.766719869876274 ] + - # (5, 3) + - [ -0.386197710479364, 0.000000000733055, 0.000000000068170 ] + - [ 0.000000000679434, -0.948271084975129, -0.269853945494201 ] + - [ 0.000000000290330, -0.256643628397778, -0.766719869876273 ] + - # (5, 4) + - [ -0.807752742385904, 0.243384910466851, -0.233700372279835 ] + - [ 0.243384910274468, -0.526716053320007, 0.134926972688063 ] + - [ -0.222259902163580, 0.128321813947456, -0.766719869876273 ] + - # (5, 5) + - [ 5.611627505726254, 0.000000000000000, 0.000000000000000 ] + - [ 0.000000000000000, 5.611627505259259, 0.000000000000000 ] + - [ 0.000000000000000, 0.000000000000000, 5.783312348864047 ] diff --git a/tests/calculations/raman_BTO.npy b/tests/calculations/raman_BTO.npy new file mode 100644 index 0000000000000000000000000000000000000000..8b38ebe72a5c7ed2844d5b72458e16e45d02f7b2 GIT binary patch literal 1208 zcmbW0ZAep57{~80Bl{3IEy@y>QCDr1B`dX~N1;}}qRB>5WV2o8Y`K^*_QscD6RQiy)3=*4?pgoM8I=fdIt{Lg(j&-vZkSfI-< zEKzWFu8ubf)hBFxnu-VIDZE<6ms@RTY=){*tIa6Lb*;ftEiBZlD-5TF1s|_btCGo+ zpjKT_{fAb%nq`%M9S+6z>2?nqO3HsVlpBQ1fBhn>43uM)Ni2UmB14VRPb>>^{Hdv9lr%p5JMa8R3TbtZd~`mt<>wo)sq{}K1N}+_PNqsozP5k=Fev+ z@8;Zpff%nl>3K$Oqyc?0txxN`au2mpKZ?1O0&*`|faP0oK_QWQ#8YlM;886BYQeLQ$^7>48A(k`0lo#vA zcpX^6D~s@9JWvYt|KPRAkIlt+9s2RzzSq+Pm#0lQVr))SOg;V$l8YC4h0~ny*hs&7 zj$`kW`c8?gkwA}uX{4HNQ_Vtul+khUvfMwKY>?8ar{|D1#YR0ne6or1ij)54l?c{U YUBo$geKv)x<8+<%@jY#WuF(Sc2Mk0R=l}o! literal 0 HcmV?d00001 diff --git a/tests/calculations/test_numerical_derivatives.py b/tests/calculations/test_numerical_derivatives.py new file mode 100644 index 0000000..433c099 --- /dev/null +++ b/tests/calculations/test_numerical_derivatives.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +################################################################################# +# Copyright (c), All rights reserved. # +# This file is part of the AiiDA-Vibroscopy code. # +# # +# The code is hosted on GitHub at https://github.com/bastonero/aiida-vibroscopy # +# For further information on the license, see the LICENSE.txt file # +################################################################################# +"""Tests for :mod:`calculations.spectra_utils`.""" +# yapf:disable +import numpy as np +import pytest + +DEBUG = False + + +@pytest.fixture +def generate_phonopy_instance(): + """Return AlAs Phonopy instance. + + It contains: + - force constants in 2x2x2 supercell + - born charges and dielectric tensors + - symmetry info + """ + + def _generate_phonopy_instance(): + """Return AlAs Phonopy instance.""" + import os + + import phonopy + + filename = 'phonopy_AlAs.yaml' + basepath = os.path.dirname(os.path.abspath(__file__)) + phyaml = os.path.join(basepath, filename) + + return phonopy.load(phyaml) + + return _generate_phonopy_instance + + +@pytest.fixture +def generate_trajectory(): + """Return a `TrajectoryData` node.""" + + def _generate_trajectory(scale=1, index=2): + """Return a `TrajectoryData` with AlAs data.""" + from aiida.orm import TrajectoryData + import numpy as np + + node = TrajectoryData() + if index == 2: + polarization = scale * np.array([[-4.88263729e-09, 6.84208048e-09, 1.67517339e-01]]) + forces = scale * np.array([[ + [-0.00000000e+00, -0.00000000e+00, 1.95259855e-02], [-0.00000000e+00, 0.00000000e+00, 1.95247000e-02], + [-0.00000000e+00, -0.00000000e+00, 1.95247000e-02], [-0.00000000e+00, 0.00000000e+00, 1.95262427e-02], + [-1.25984053e-05, -1.25984053e-05, -1.95383268e-02], [-1.31126259e-05, 1.31126259e-05, -1.95126158e-02], + [1.28555156e-05, 1.25984053e-05, -1.95383268e-02], [1.31126259e-05, -1.31126259e-05, -1.95126158e-02] + ]]) + if index == 3: + polarization = scale * np.array([[9.55699034e-05, 1.18453183e-01, 1.18453160e-01]]) + forces = scale * np.array([ + [[-1.82548322e-05, 1.38068238e-02, 1.38068238e-02], [-1.82548322e-05, 1.38060524e-02, 1.38060524e-02], + [-1.82548322e-05, 1.38070809e-02, 1.38060524e-02], [-1.82548322e-05, 1.38060524e-02, 1.38070809e-02], + [5.39931655e-06, -1.38194222e-02, -1.38194222e-02], [5.14220624e-06, -1.37937111e-02, -1.37937111e-02], + [3.11103478e-05, -1.37937111e-02, -1.38194222e-02], [3.11103478e-05, -1.38194222e-02, -1.37937111e-02]] + ]) + + node.set_array('forces', forces) + node.set_array('electronic_dipole_cartesian_axes', polarization) + + stepids = np.array([1]) + times = stepids * 0.0 + cells = np.array([5.62475444 * np.eye(3)]) + positions = np.array([[ + [0.,0.,0.], + [0.,2.81237722,2.81237722], + [2.81237722,0.,2.81237722], + [2.81237722,2.81237722,0.], + [1.40621634,1.40621634,1.40621634], + [1.40621634,4.21853809,4.21853809], + [4.21853809,4.21853809,1.40621634], + [4.21853809,1.40621634,4.21853809], + ]]) + symbols = ['Al', 'Al', 'Al', 'Al', 'As', 'As', 'As', 'As'] + node.set_trajectory(stepids=stepids, cells=cells, symbols=symbols, positions=positions, times=times) + + return node.store() + + return _generate_trajectory + + +def test_compute_tensors(generate_phonopy_instance, generate_trajectory): + """Test the functions for computing tensors.""" + from aiida.orm import Float, Int + from aiida_phonopy.data.preprocess import PreProcessData + from qe_tools import CONSTANTS as C + + from aiida_vibroscopy.calculations.numerical_derivatives_utils import ( + compute_nac_parameters, + compute_susceptibility_derivatives, + ) + + ph = generate_phonopy_instance() + diagonal_scale = 1 / np.sqrt(2) + volume_au = ph.unitcell.volume / C.bohr_to_ang**3 + preprocess_data = PreProcessData(phonopy_atoms=ph.unitcell) + + accuracy = 2 + data = {'field_index_2': {'0': generate_trajectory(index=2)}, 'field_index_3': {'0': generate_trajectory(index=3)}} + + bec_ref = data['field_index_2']['0'].get_array('forces')[0, 0, 2] / 2.5e-4 / np.sqrt(2) * C.bohr_to_ang / C.ry_to_ev + diel_ref = 4. * np.pi * data['field_index_2']['0'].get_array('electronic_dipole_cartesian_axes')[ + 0, 2] / 2.5e-4 / volume_au + 1 + + raman_ref = data['field_index_3']['0'].get_array('forces')[ + 0, 0, 0] / (diagonal_scale * 2.5e-4)**2 / C.ry_to_ev / volume_au + + results = compute_nac_parameters(preprocess_data, Float(2.5e-4), Int(accuracy), **data) + for i in range(3): + bec = results['numerical_accuracy_2'].get_array('born_charges')[0, i, i] + diel = results['numerical_accuracy_2'].get_array('dielectric')[i, i] + assert np.abs(bec - bec_ref) < 1e-4 + assert np.abs(diel - diel_ref) < 1e-4 + if i != 2: + bec = results['numerical_accuracy_2'].get_array('born_charges')[0, i, 2] + diel = results['numerical_accuracy_2'].get_array('dielectric')[i, 2] + assert np.abs(bec) < 1e-5 + assert np.abs(diel) < 1e-5 + + results = compute_susceptibility_derivatives( + preprocess_data, Float(2.5e-4), Float(diagonal_scale), Int(accuracy), **data + ) + raman = results['numerical_accuracy_2'].get_array('raman_tensors')[0, 0, 1, 2] + assert np.abs(raman - raman_ref) < 1e-4 + + accuracy = 2 + data = { + 'field_index_0': { + '0': generate_trajectory(index=2), + '1': generate_trajectory(scale=-1, index=2) + }, + 'field_index_1': { + '0': generate_trajectory(index=2), + '1': generate_trajectory(scale=-1, index=2) + }, + 'field_index_2': { + '0': generate_trajectory(index=2), + '1': generate_trajectory(scale=-1, index=2) + }, + 'field_index_3': { + '0': generate_trajectory(index=3), + '1': generate_trajectory(scale=-1, index=3) + }, + 'field_index_4': { + '0': generate_trajectory(index=3), + '1': generate_trajectory(scale=-1, index=3) + }, + 'field_index_5': { + '0': generate_trajectory(index=3), + '1': generate_trajectory(scale=-1, index=3) + }, + } + + preprocess_data = PreProcessData(phonopy_atoms=ph.unitcell, is_symmetry=False, symprec=1e-4) + compute_nac_parameters(preprocess_data, Float(2.5e-4), Int(accuracy), **data) + compute_susceptibility_derivatives(preprocess_data, Float(2.5e-4), Float(np.sqrt(2)), Int(accuracy), **data) + + +@pytest.mark.parametrize( + 'inputs,result', ( + ((2, 1), [1 / 2, 0.]), + ((4, 1), [2 / 3, -1 / 12, 0.]), + ((8, 1), [4 / 5, -1 / 5, 4 / 105, -1 / 280, 0.]), + ((2, 2), [1., -2.]), + ((4, 2), [4 / 3, -1 / 12, -5 / 2]), + ((8, 2), [8 / 5, -1 / 5, 8 / 315, -1 / 560, -205 / 72]), + ) +) +def test_get_central_derivatives_coefficients(inputs, result): + """Test the numerical derivatives coefficients. + + Exact values taken from Wikipedia: https://en.wikipedia.org/wiki/Finite_difference_coefficient + """ + import numpy as np + + from aiida_vibroscopy.calculations.numerical_derivatives_utils import get_central_derivatives_coefficients + + array1 = np.array(get_central_derivatives_coefficients(*inputs)) + array2 = np.array(result) + assert np.abs(array1 - array2).max() < 1.0e-10 diff --git a/tests/calculations/test_spectra.py b/tests/calculations/test_spectra.py index d22b77e..b146f22 100644 --- a/tests/calculations/test_spectra.py +++ b/tests/calculations/test_spectra.py @@ -7,6 +7,7 @@ # For further information on the license, see the LICENSE.txt file # ################################################################################# """Tests for :mod:`calculations.spectra_utils`.""" +# yapf:disable import numpy as np import pytest @@ -23,19 +24,17 @@ def generate_phonopy_instance(): - symmetry info """ - def _generate_phonopy_instance(): + def _generate_phonopy_instance(name='AlAs'): """Return AlAs Phonopy instance.""" import os import phonopy - filename = 'phonopy_AlAs.yaml' + filename = f'phonopy_{name}.yaml' basepath = os.path.dirname(os.path.abspath(__file__)) phyaml = os.path.join(basepath, filename) - ph = phonopy.load(phyaml) - - return ph + return phonopy.load(phyaml) return _generate_phonopy_instance @@ -51,46 +50,56 @@ def generate_third_rank_tensors(): def _generate_third_rank_tensors(): """Return AlAs Phonopy instance.""" - chi2 = np.array([[[-1.42547451e-50, -4.81482486e-35, 1.36568821e-14], - [-4.81482486e-35, 0.00000000e+00, 4.24621905e+01], - [1.36568821e-14, 4.24621905e+01, 5.20011857e-15]], - [[-3.20988324e-35, 0.00000000e+00, 4.24621905e+01], - [0.00000000e+00, 0.00000000e+00, 0.00000000e+00], - [4.24621905e+01, 0.00000000e+00, 5.20011857e-15]], - [[1.36568821e-14, 4.24621905e+01, 5.20011857e-15], - [4.24621905e+01, -2.40741243e-35, 5.20011857e-15], - [5.20011857e-15, 5.20011857e-15, 9.55246283e-31]]]) - - raman = np.array([[[[7.82438427e-38, -1.38120586e-37, -1.13220290e-17], - [-1.37630797e-37, -5.64237288e-37, -3.52026291e-02], - [-1.13220290e-17, -3.52026291e-02, -4.31107870e-18]], - [[-4.23177966e-37, -7.99336159e-37, -3.52026291e-02], - [-5.48564030e-37, -4.99585099e-38, 1.47328625e-36], - [-3.52026291e-02, 1.77891478e-36, -4.31107870e-18]], - [[-1.13220290e-17, -3.52026291e-02, -4.31107870e-18], - [-3.52026291e-02, -2.66445386e-37, -4.31107870e-18], - [-4.31107870e-18, -4.31107870e-18, -7.91497448e-34]]], - [[[-1.01998624e-37, -1.60357021e-36, 1.13220290e-17], - [-1.45026616e-36, 2.31964219e-36, 3.52026291e-02], - [1.13220290e-17, 3.52026291e-02, 4.31107870e-18]], - [[7.67989643e-37, -7.36643127e-37, 3.52026291e-02], - [-4.23177966e-37, -1.59671316e-37, -7.20969869e-37], - [3.52026291e-02, -5.09380885e-37, 4.31107870e-18]], - [[1.13220290e-17, 3.52026291e-02, 4.31107870e-18], - [3.52026291e-02, -2.66445386e-37, 4.31107870e-18], - [4.31107870e-18, 4.31107870e-18, 7.91868953e-34]]]]) + c = 42.4621905 # pm/V + chi2 = np.array([[ + [0, 0, 0], + [0, 0, c], + [0, 0, 0]], + [ + [0, 0, c], + [0, 0, 0], + [0, 0, 0] + ], + [ + [0, c, 0], + [c, 0, 0], + [0, 0, 0] + ] + ]) + + a = 3.52026291e-02 # 1/Ang + raman = np.array([ + [ + [[0, 0, 0], [0, 0, -a], [0, -a, 0]], + [[0, 0, -a],[0, 0, 0],[-a, 0, 0]], + [[0, -a, 0], [-a, 0, 0],[0, 0, 0]], + ], + [ + [[0, 0, 0], [0, 0, a], [0, a, 0]], + [[0, 0, a], [0, 0, 0], [a, 0, 0]], + [[0, a, 0], [a, 0, 0], [0, 0, 0]], + ] + ]) + return raman, chi2 return _generate_third_rank_tensors -@pytest.mark.skip(reason='This may fail for unknown reasons during online testing.') def test_compute_raman_susceptibility_tensors(generate_phonopy_instance, generate_third_rank_tensors): - """Test the `compute_raman_susceptibility_tensors` function.""" + """Test the `compute_raman_susceptibility_tensors` function. + + For cubic semiconductors AB, the Raman susceptibility for phonons polarized along the `l` + direction can be written as sqrt(mu*Omega)*alpha_{12} = Omega dChi_{12}/dtau_{A,3}, + where A is the atom located at the origin, and the atom B in (1/4,1/4,1/4). + + As a consequence, we can test the implementation when specifying a q-direction (Cartesian). + """ from aiida_vibroscopy.calculations.spectra_utils import compute_raman_susceptibility_tensors from aiida_vibroscopy.common.constants import DEFAULT ph = generate_phonopy_instance() + ph.symmetrize_force_constants() vol = ph.unitcell.volume raman, chi2 = generate_third_rank_tensors() @@ -101,36 +110,138 @@ def test_compute_raman_susceptibility_tensors(generate_phonopy_instance, generat phonopy_instance=ph, raman_tensors=raman, nlo_susceptibility=chi2, - nac_direction=(0, 0, 0), + nac_direction=[1, 0, 0], ) + alpha_comp = prefactor * alpha[2, 1, 2] + alpha_theo = vol * raman[0, 0, 1, 2] if DEBUG: print('\n', '================================', '\n') + print((prefactor * alpha).round(3)) print('\t', 'DEBUG') - print(prefactor * alpha[1, 1, 2], vol * raman[1, 0, 1, 2]) + print(alpha_comp, alpha_theo) print('\n', '================================', '\n') - assert np.abs(prefactor * alpha[1, 1, 2] + vol * raman[1, 0, 1, 2]) < 0.01 + assert np.abs(abs(alpha_comp) - abs(alpha_theo)) < 1e-5 alpha, _, _ = compute_raman_susceptibility_tensors( phonopy_instance=ph, raman_tensors=raman, nlo_susceptibility=chi2, - nac_direction=np.dot(ph.primitive.cell, [0, 0, 1]), + nac_direction=[0, 0, 1], ) diel = ph.nac_params['dielectric'] borns = ph.nac_params['born'] - dchivol = vol * raman[1, 0, 1, 2] - DEFAULT.nlo_conversion * borns[1, 0, 0] * chi2[0, 1, 2] / diel[0, 0] + nlocorr = DEFAULT.nlo_conversion * borns[1, 2, 2] * chi2[0, 1, 2] / diel[2, 2] + alpha_theo = vol * raman[1, 0, 1, 2] - nlocorr + # we take the last, cause it is associated to the LO mode + alpha_comp = prefactor * alpha[2, 0, 1] if DEBUG: print('\n', '================================', '\n') print('\t', 'DEBUG') - print('NLO corr. expected: ', -DEFAULT.nlo_conversion * borns[1, 0, 0] * chi2[0, 1, 2] / diel[0, 0]) + print((prefactor * alpha).round(3)) + print('NLO corr. expected: ', nlocorr) print('Born corr. expected: ', -borns[1, 0, 0] / np.sqrt(reduced_mass)) print('Conversion factor nlo: ', DEFAULT.nlo_conversion) - # print(prefactor * alpha) - print(dchivol, prefactor * np.abs(alpha).max()) + print(alpha_comp, alpha_theo) + print('\n', '================================', '\n') + + assert np.abs(abs(alpha_comp) - abs(alpha_theo)) < 1e-3 + + +def test_compute_clamped_pockels_tensor(generate_phonopy_instance, generate_third_rank_tensors, ndarrays_regression): + """Test the `compute_clamped_pockels_tensor` function.""" + import os + + from aiida_vibroscopy.calculations.spectra_utils import compute_clamped_pockels_tensor + + ph = generate_phonopy_instance('BTO') + basepath = os.path.dirname(os.path.abspath(__file__)) + chi2 = np.load(os.path.join(basepath, 'chi2_BTO.npy')) + raman = np.load(os.path.join(basepath, 'raman_BTO.npy')) + + pockels, pockels_el, pockels_ion = compute_clamped_pockels_tensor( + phonopy_instance=ph, + raman_tensors=raman, + nlo_susceptibility=chi2, + ) + + if DEBUG: print('\n', '================================', '\n') + print('\t', 'DEBUG') + print(pockels) + print('\t', 'DEBUG EL') + print(pockels_el) + print('\t', 'DEBUG ION') + print(pockels_ion) + print('\n', '================================', '\n') + + results = { + 'pockels': pockels, + 'pockels_el': pockels_el, + 'pockels_ion': pockels_ion, + } + ndarrays_regression.check(results, default_tolerance=dict(atol=1e-4, rtol=1e-4)) + + +def test_compute_methods(generate_phonopy_instance, generate_third_rank_tensors, ndarrays_regression): + """Test the post-processing methods with data regression techniques.""" + from aiida_vibroscopy.calculations.spectra_utils import ( + compute_active_modes, + compute_complex_dielectric, + compute_raman_space_average, + compute_raman_susceptibility_tensors, + ) + + results = {} + ph = generate_phonopy_instance() + ph.symmetrize_force_constants() + raman, chi2 = generate_third_rank_tensors() + + freqs, _, _ = compute_active_modes(phonopy_instance=ph) + results['active_modes_freqs'] = freqs + # results['active_modes_eigvecs'] = eigenvectors + + freqs, _ , _ = compute_active_modes(phonopy_instance=ph, nac_direction=[0,0,1]) + results['active_modes_nac_freqs'] = freqs + # results['active_modes_nac_eigvecs'] = eigenvectors + + alpha, _, _ = compute_raman_susceptibility_tensors(ph, raman, chi2) + ints_hh, ints_hv = compute_raman_space_average(alpha) + # results['raman_susceptibility_tensors'] = alpha + results['intensities_hh'] = ints_hh + results['intensities_hv'] = ints_hv + + # alpha, _, _ = compute_raman_susceptibility_tensors(ph, raman, chi2, nac_direction=[0,0,1]) + # results['raman_susceptibility_tensors_nac'] = alpha + + # pols, _, _ = compute_polarization_vectors(ph) + # results['polarization_vectors'] = pols + + # pols, _, _ = compute_polarization_vectors(ph, nac_direction=[0,0,1]) + # results['polarization_vectors_nac'] = pols + + freq_range = np.linspace(10,1000,900) + eps = compute_complex_dielectric(ph, freq_range=freq_range) + results['complex_dielectric'] = eps + + eps = compute_complex_dielectric(ph, nac_direction=[0,0,1], freq_range=freq_range) + results['complex_dielectric_nac'] = eps + + ndarrays_regression.check(results, default_tolerance=dict(atol=1e-4, rtol=1e-4)) + + +def test_generate_vibrational_data_from_forces(generate_vibrational_data_from_forces, ndarrays_regression): + """Test `generate_vibrational_data_from_phonopy`.""" + vibro = generate_vibrational_data_from_forces() - assert np.abs(prefactor * alpha[2, 0, 1] - dchivol) < 0.01 + results = { + 'dielectric': vibro.dielectric, + 'raman': vibro.raman_tensors, + 'nlo': vibro.nlo_susceptibility, + 'becs': vibro.born_charges, + 'forces': vibro.forces, + } + ndarrays_regression.check(results, default_tolerance=dict(atol=1e-8, rtol=1e-8)) diff --git a/tests/calculations/test_spectra/test_compute_clamped_pockels_tensor.npz b/tests/calculations/test_spectra/test_compute_clamped_pockels_tensor.npz new file mode 100644 index 0000000000000000000000000000000000000000..52a98f4e0fb343ef85c8cdd5730c02412e35a27b GIT binary patch literal 1133 zcmWIWW@Zs#U|`??VnqheOYEk+j0_ABK+Me`!cdT(oSm9ete00%$;co876z(g04V{1 z*>A*ONei4j5pXPE?Yv1*b7Gd{EnSc|Ic?#(#S7+5iw_B!GDCcR{FF&tLgn6PH2qk+ z#M4#8mN5O4I%dVCuw>=pfYlCi4{mL{+W9+a*}A*Q)_cDgc#5|B|IbrjQEnj8bxz&RQ^6`*V8lu*?qxDPGteh?VdBD zg^Gb&nh^^$eeolON@ji@da!_$Q7u?tX)N~Ht-iZuueA8WpXdH7`!@M6WAE$Ggnx&O z_W#J9x$Ae{nVFjt_CLv$ez};DbfUb?|R$RyI}H(SNFVc z{bmuJ&+fZNvH+B{7@0(vab;6TIDyCpaF`)wT6B%5sS;VE7f>ryBQ%Yo>qU(_WW5PY Y2))pl4De=U11Vt!!c9PWDKm%%0E@=ATL1t6 literal 0 HcmV?d00001 diff --git a/tests/calculations/test_spectra/test_compute_methods.npz b/tests/calculations/test_spectra/test_compute_methods.npz new file mode 100644 index 0000000000000000000000000000000000000000..4e1cec86a244d4d0953cae08a694e4bded54b842 GIT binary patch literal 144610 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

sc07w$tno-2X>Zl| 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 literal 0 HcmV?d00001 diff --git a/tests/calculations/test_spectra/test_generate_vibrational_data_from_forces.npz b/tests/calculations/test_spectra/test_generate_vibrational_data_from_forces.npz new file mode 100644 index 0000000000000000000000000000000000000000..46e23c646493e08b7c227de4b70162f762a58204 GIT binary patch literal 1016 zcmWIWW@Zs#U|`??Vnv2+AEku?fUFZh%*P5Reo7s);yO0Vifb!Z3bP)Y!HS zcb&a}mPIf!Ft7t%l$Vnaa}P)e1VGXtK(>2;L8Ab1j@*MJiDl?cnCd3U?Jq6a)ywR3^bh19X)`${KJ?0fQ#x0N+1n#$6V|d|&?;tN^7QMkWzv zT&Wr21Q6K(4kXmHjjk6p0jZkDwOM#kT(TlDN eHOP^5SpqW#A_4-uS=m6ctUxFRH17;J+W-LWCkx&H literal 0 HcmV?d00001 diff --git a/tests/calculations/test_symmetry.py b/tests/calculations/test_symmetry.py new file mode 100644 index 0000000..178a585 --- /dev/null +++ b/tests/calculations/test_symmetry.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +################################################################################# +# Copyright (c), All rights reserved. # +# This file is part of the AiiDA-Vibroscopy code. # +# # +# The code is hosted on GitHub at https://github.com/bastonero/aiida-vibroscopy # +# For further information on the license, see the LICENSE.txt file # +################################################################################# +"""Tests for :mod:`calculations.spectra_utils`.""" +import numpy as np +import pytest + +DEBUG = True + + +@pytest.fixture +def generate_phonopy_instance(): + """Return AlAs Phonopy instance. + + It contains: + - force constants in 2x2x2 supercell + - born charges and dielectric tensors + - symmetry info + """ + + def _generate_phonopy_instance(): + """Return AlAs Phonopy instance.""" + import os + + import phonopy + + filename = 'phonopy_AlAs.yaml' + basepath = os.path.dirname(os.path.abspath(__file__)) + phyaml = os.path.join(basepath, filename) + + return phonopy.load(phyaml) + + return _generate_phonopy_instance + + +@pytest.fixture +def generate_trajectory(): + """Return a `TrajectoryData` node.""" + + def _generate_trajectory(scale=1): + """Return a `TrajectoryData` with AlAs data.""" + from aiida.orm import TrajectoryData + import numpy as np + + node = TrajectoryData() + polarization = scale * np.array([[-4.88263729e-09, 6.84208048e-09, 1.67517339e-01]]) + node.set_array('electronic_dipole_cartesian_axes', polarization) + forces = scale * np.array( + [[[-0.00000000e+00, -0.00000000e+00, 1.95259855e-02], [-0.00000000e+00, 0.00000000e+00, 1.95247000e-02], + [-0.00000000e+00, -0.00000000e+00, 1.95247000e-02], [-0.00000000e+00, 0.00000000e+00, 1.95262427e-02], + [-1.25984053e-05, -1.25984053e-05, -1.95383268e-02], [-1.31126259e-05, 1.31126259e-05, -1.95126158e-02], + [1.28555156e-05, 1.25984053e-05, -1.95383268e-02], [1.31126259e-05, -1.31126259e-05, -1.95126158e-02]]] + ) + node.set_array('forces', forces) + + stepids = np.array([1]) + times = stepids * 0.0 + cells = np.array([5.62475444 * np.eye(3)]) + positions = np.array([[ + [ + 0., + 0., + 0., + ], + [ + 0., + 2.81237722, + 2.81237722, + ], + [ + 2.81237722, + 0., + 2.81237722, + ], + [ + 2.81237722, + 2.81237722, + 0., + ], + [ + 1.40621634, + 1.40621634, + 1.40621634, + ], + [ + 1.40621634, + 4.21853809, + 4.21853809, + ], + [ + 4.21853809, + 4.21853809, + 1.40621634, + ], + [ + 4.21853809, + 1.40621634, + 4.21853809, + ], + ]]) + symbols = ['Al', 'Al', 'Al', 'Al', 'As', 'As', 'As', 'As'] + node.set_trajectory(stepids=stepids, cells=cells, symbols=symbols, positions=positions, times=times) + + return node.store() + + return _generate_trajectory + + +def test_transform_trajectory(generate_phonopy_instance, generate_trajectory): + """Test the `compute_raman_susceptibility_tensors` function.""" + from aiida_vibroscopy.calculations.symmetry import transform_trajectory + + ph = generate_phonopy_instance() # here z + rot = np.array([[0, 1, 0], [-1, 0, 0], [0, 0, -1]]) # to -z + trans = np.array([0, 0, 0]) + traj = generate_trajectory() + forces = traj.get_array('forces') + + new_traj = transform_trajectory(traj, np.array([0, 0, 0]), rot, trans, ph.unitcell, 1e-5) + new_forces = new_traj.get_array('forces') + assert np.abs(forces + new_forces).max() < 5.0e-5 # eV/Ang + + rot = np.array([[-1, 0, 0], [0, 0, -1], [0, 1, 0]]) # to -y + new_traj = transform_trajectory(traj, np.array([0, 0, 0]), rot, trans, ph.unitcell, 1e-5) + new_forces = new_traj.get_array('forces') + assert np.abs(forces[-1, :, 2] + new_forces[-1, :, 1]).max() < 5.0e-5 # eV/Ang + + +def test_get_connected_field_with_operations(generate_phonopy_instance): + """Test the `get_connected_fields_with_operations` method.""" + from aiida_vibroscopy.calculations.symmetry import get_connected_fields_with_operations + + ph = generate_phonopy_instance() + direction = [0, 0, 1] + eq_directions, _, _ = get_connected_fields_with_operations(ph, direction) + + for array in [[0, 0, -1], [0, 0, 1], [1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0]]: + assert array in eq_directions + + +def test_get_irreducible_numbers_and_signs(generate_phonopy_instance): + """Test the `get_irreducible_numbers_and_signs` method.""" + from aiida_phonopy.data.preprocess import PreProcessData + + from aiida_vibroscopy.calculations.symmetry import get_irreducible_numbers_and_signs + + ph = generate_phonopy_instance() + preprocess_data = PreProcessData(phonopy_atoms=ph.unitcell) + + irr_numbers, irr_signs = get_irreducible_numbers_and_signs(preprocess_data, 6) + + assert irr_numbers == [2, 3] + assert irr_signs == [[True, False], [True, False]] + + +def test_get_trajectories_from_symmetries(generate_phonopy_instance, generate_trajectory): + """Test the `get_trajectories_from_symmetries` method.""" + from aiida_phonopy.data.preprocess import PreProcessData + + from aiida_vibroscopy.calculations.symmetry import get_trajectories_from_symmetries + + ph = generate_phonopy_instance() + preprocess_data = PreProcessData(phonopy_atoms=ph.unitcell) + + accuracy = 2 + data = {'field_index_2': {'0': generate_trajectory()}} + data_0 = generate_trajectory(scale=0) + + trajs = get_trajectories_from_symmetries(preprocess_data, data, data_0, accuracy) + + for i in range(3): + assert f'field_index_{i}' in trajs + for j in range(2): + assert str(j) in trajs[f'field_index_{i}'] + + for i in range(3): + forces_pz = trajs[f'field_index_{i}']['0'].get_array('forces') + forces_mz = trajs[f'field_index_{i}']['1'].get_array('forces') + assert np.abs(forces_pz + forces_mz).max() < 5.0e-5 # eV/Ang + + accuracy = 4 + data = {'fields_data_2': {'0': generate_trajectory(), '1': generate_trajectory(scale=2)}} + data_0 = generate_trajectory(scale=0) + + trajs = get_trajectories_from_symmetries(preprocess_data, data, data_0, accuracy) + + for i in range(3): + assert f'field_index_{i}' in trajs + for j in range(2): + assert str(j) in trajs[f'field_index_{i}'] + + for i in range(3): + forces_pz = trajs[f'field_index_{i}']['2'].get_array('forces')[-1, :, 2] + forces_mz = trajs[f'field_index_{i}']['3'].get_array('forces')[-1, :, 2] + assert np.abs(forces_pz + forces_mz).max() < 1.0e-4 # eV/Ang + # Note: the higher thr for the diff is due to little distortion diff --git a/tests/cli/__init__.py b/tests/cli/__init__.py new file mode 100644 index 0000000..598c7b7 --- /dev/null +++ b/tests/cli/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Test module for the command line interface.""" diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py new file mode 100644 index 0000000..82caf7c --- /dev/null +++ b/tests/cli/conftest.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# pylint: disable=redefined-outer-name +"""Fixtures for the command line interface.""" +import pytest + + +def mock_launch_process(*_, **__): + """Mock the :meth:`~aiida_vibroscopy.cli.utilslaunch.launch_process` to be a no-op.""" + return + + +@pytest.fixture +def filepath_cli_fixture(filepath_tests): + """Return the filepath for CLI fixtures.""" + from pathlib import Path + return Path(filepath_tests, 'cli', 'fixtures') + + +@pytest.fixture +def run_cli_command(): + """Run a `click` command with the given options. + + The call will raise if the command triggered an exception or the exit code returned is non-zero. + """ + + def _run_cli_command(command, options=None, raises=None): + """Run the command and check the result. + + :param command: the command to invoke + :param options: the list of command line options to pass to the command invocation + :param raises: optionally an exception class that is expected to be raised + """ + import traceback + + from click.testing import CliRunner + + runner = CliRunner() + result = runner.invoke(command, options or []) + + if raises is not None: + assert result.exception is not None, result.output + assert result.exit_code != 0 + else: + assert result.exception is None, ''.join(traceback.format_exception(*result.exc_info)) + assert result.exit_code == 0, result.output + + result.output_lines = [line.strip() for line in result.output.split('\n') if line.strip()] + + return result + + return _run_cli_command + + +@pytest.fixture +def run_cli_process_launch_command(run_cli_command, monkeypatch): + """Run a process launch command with the given options. + + The call will raise if the command triggered an exception or the exit code returned is non-zero. + + :param command: the command to invoke + :param options: the list of command line options to pass to the command invocation + :param raises: optionally an exception class that is expected to be raised + """ + + def _inner(command, options=None, raises=None): + """Run the command and check the result.""" + from aiida_vibroscopy.cli.utils import launch + monkeypatch.setattr(launch, 'launch_process', mock_launch_process) + return run_cli_command(command, options, raises) + + return _inner diff --git a/tests/cli/fixtures/overrides/dielectric.yaml b/tests/cli/fixtures/overrides/dielectric.yaml new file mode 100644 index 0000000..9e5a4e1 --- /dev/null +++ b/tests/cli/fixtures/overrides/dielectric.yaml @@ -0,0 +1,4 @@ +scf: + parameters: + SYSTEM: + ecutwfc: 10 diff --git a/tests/cli/fixtures/overrides/harmonic.yaml b/tests/cli/fixtures/overrides/harmonic.yaml new file mode 100644 index 0000000..8da91f1 --- /dev/null +++ b/tests/cli/fixtures/overrides/harmonic.yaml @@ -0,0 +1,3 @@ +phonon: + scf: + kpoints_distance: 0.2 diff --git a/tests/cli/fixtures/overrides/iraman-spectra.yaml b/tests/cli/fixtures/overrides/iraman-spectra.yaml new file mode 100644 index 0000000..8da91f1 --- /dev/null +++ b/tests/cli/fixtures/overrides/iraman-spectra.yaml @@ -0,0 +1,3 @@ +phonon: + scf: + kpoints_distance: 0.2 diff --git a/tests/cli/fixtures/overrides/phonon.yaml b/tests/cli/fixtures/overrides/phonon.yaml new file mode 100644 index 0000000..577eb05 --- /dev/null +++ b/tests/cli/fixtures/overrides/phonon.yaml @@ -0,0 +1,2 @@ +scf: + kpoints_distance: 0.2 diff --git a/tests/cli/test_commands.py b/tests/cli/test_commands.py new file mode 100644 index 0000000..d064d86 --- /dev/null +++ b/tests/cli/test_commands.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +"""Tests for CLI commands.""" +from __future__ import annotations + +import subprocess + +from aiida_pseudo.cli import cmd_root +import click +import pytest + + +def recurse_commands(command: click.Command, parents: list[str] = None): + """Recursively return all subcommands that are part of ``command``. + + :param command: The click command to start with. + :param parents: A list of strings that represent the parent commands leading up to the current command. + :returns: A list of strings denoting the full path to the current command. + """ + if isinstance(command, click.Group): + for command_name in command.commands: + subcommand = command.get_command(None, command_name) + if parents is not None: + subparents = parents + [command.name] + else: + subparents = [command.name] + yield from recurse_commands(subcommand, subparents) + + if parents is not None: + yield parents + [command.name] + else: + yield [command.name] + + +@pytest.mark.parametrize('command', recurse_commands(cmd_root)) +@pytest.mark.parametrize('help_option', ('--help', '-h')) +def test_commands_help_option(command, help_option): + """Test the help options for all subcommands of the CLI. + + The usage of ``subprocess.run`` is on purpose because using :meth:`click.Context.invoke`, which is used by the + ``run_cli_command`` fixture that should usually be used in testing CLI commands, does not behave exactly the same + compared to a direct invocation on the command line. The invocation through ``invoke`` does not go through all the + parent commands and so might not get all the necessary initializations. + """ + result = subprocess.run(command + [help_option], check=False, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + assert 'Usage:' in result.stdout diff --git a/tests/cli/workflows/__init__.py b/tests/cli/workflows/__init__.py new file mode 100644 index 0000000..b960e61 --- /dev/null +++ b/tests/cli/workflows/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Test module for the workflows CLI.""" diff --git a/tests/cli/workflows/dielectric/__init__.py b/tests/cli/workflows/dielectric/__init__.py new file mode 100644 index 0000000..1a42c6d --- /dev/null +++ b/tests/cli/workflows/dielectric/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Test module for the dielectric workflows CLI.""" diff --git a/tests/cli/workflows/dielectric/test_base.py b/tests/cli/workflows/dielectric/test_base.py new file mode 100644 index 0000000..6ef8a8a --- /dev/null +++ b/tests/cli/workflows/dielectric/test_base.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +"""Tests for the ``launch dielectric`` command.""" +from pathlib import Path + +from aiida_vibroscopy.cli.workflows.dielectric.base import launch_workflow + + +# yapf: disable +def test_command_dielectric(run_cli_process_launch_command, fixture_code, filepath_cli_fixture): + """Test invoking the launch command with only required inputs.""" + code = fixture_code('quantumespresso.pw').store() + options = [ + '--pw', code.full_label, + '-o', str(Path(filepath_cli_fixture, 'overrides', 'dielectric.yaml')), + '-p', 'fast', + '-F', 'SSSP/1.3/PBEsol/efficiency', + ] + run_cli_process_launch_command(launch_workflow, options=options) + + options = [ + '--pw', code.full_label, + '-p', 'fast', + '-F', 'SSSP/1.3/PBEsol/efficiency', + ] + run_cli_process_launch_command(launch_workflow, options=options) + + options = [ + '--pw', code.full_label, + '-p', 'fast', + '-o', str(Path(filepath_cli_fixture, 'overrides', 'dielectric.yaml')), + '-k', '2', '2', '2', '0.5', '0.5', '0.5', + ] + run_cli_process_launch_command(launch_workflow, options=options) + + options = [ + '--pw', code.full_label, + '-p', 'fast', + '-k', '2', '2', '2', '0.5', '0.5', '0.5', + ] + run_cli_process_launch_command(launch_workflow, options=options) diff --git a/tests/cli/workflows/phonons/__init__.py b/tests/cli/workflows/phonons/__init__.py new file mode 100644 index 0000000..4878b54 --- /dev/null +++ b/tests/cli/workflows/phonons/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Test module for the phonon workflows CLI.""" diff --git a/tests/cli/workflows/phonons/test_base.py b/tests/cli/workflows/phonons/test_base.py new file mode 100644 index 0000000..1e4c62f --- /dev/null +++ b/tests/cli/workflows/phonons/test_base.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +"""Tests for the ``launch phonon`` command.""" +from pathlib import Path + +from aiida_vibroscopy.cli.workflows.phonons.base import launch_workflow + + +# yapf: disable +def test_command_phonon(run_cli_process_launch_command, fixture_code, filepath_cli_fixture): + """Test invoking the launch command with only required inputs.""" + code = fixture_code('quantumespresso.pw').store() + options = [ + '--pw', code.full_label, + '-o', str(Path(filepath_cli_fixture, 'overrides', 'phonon.yaml')), + '-p', 'fast', + '-F', 'SSSP/1.3/PBEsol/efficiency', + ] + run_cli_process_launch_command(launch_workflow, options=options) + + options = [ + '--pw', code.full_label, + '-p', 'fast', + '-F', 'SSSP/1.3/PBEsol/efficiency', + ] + run_cli_process_launch_command(launch_workflow, options=options) + + options = [ + '--pw', code.full_label, + '-p', 'fast', + '-o', str(Path(filepath_cli_fixture, 'overrides', 'phonon.yaml')), + '-k', '2', '2', '2', '0.5', '0.5', '0.5', + ] + run_cli_process_launch_command(launch_workflow, options=options) + + options = [ + '--pw', code.full_label, + '-p', 'fast', + '-k', '2', '2', '2', '0.5', '0.5', '0.5', + ] + run_cli_process_launch_command(launch_workflow, options=options) diff --git a/tests/cli/workflows/phonons/test_harmonic.py b/tests/cli/workflows/phonons/test_harmonic.py new file mode 100644 index 0000000..14a8da9 --- /dev/null +++ b/tests/cli/workflows/phonons/test_harmonic.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +"""Tests for the ``launch harmonic`` command.""" +from pathlib import Path + +from aiida_vibroscopy.cli.workflows.phonons.harmonic import launch_workflow + + +# yapf: disable +def test_command_harmonic(run_cli_process_launch_command, fixture_code, filepath_cli_fixture): + """Test invoking the launch command with only required inputs.""" + code = fixture_code('quantumespresso.pw').store() + options = [ + '--pw', code.full_label, + '-o', str(Path(filepath_cli_fixture, 'overrides', 'harmonic.yaml')), + '-p', 'fast', + '-F', 'SSSP/1.3/PBEsol/efficiency', + ] + run_cli_process_launch_command(launch_workflow, options=options) + + options = [ + '--pw', code.full_label, + '-p', 'fast', + '-F', 'SSSP/1.3/PBEsol/efficiency', + ] + run_cli_process_launch_command(launch_workflow, options=options) + + options = [ + '--pw', code.full_label, + '-p', 'fast', + '-o', str(Path(filepath_cli_fixture, 'overrides', 'harmonic.yaml')), + '-k', '2', '2', '2', '0.5', '0.5', '0.5', + ] + run_cli_process_launch_command(launch_workflow, options=options) + + options = [ + '--pw', code.full_label, + '-p', 'fast', + '-k', '2', '2', '2', '0.5', '0.5', '0.5', + ] + run_cli_process_launch_command(launch_workflow, options=options) diff --git a/tests/cli/workflows/spectra/__init__.py b/tests/cli/workflows/spectra/__init__.py new file mode 100644 index 0000000..6dc7d41 --- /dev/null +++ b/tests/cli/workflows/spectra/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Test module for the spectra workflows CLI.""" diff --git a/tests/cli/workflows/spectra/test_iraman_spectra.py b/tests/cli/workflows/spectra/test_iraman_spectra.py new file mode 100644 index 0000000..0c2d9da --- /dev/null +++ b/tests/cli/workflows/spectra/test_iraman_spectra.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +"""Tests for the ``launch iraman-spectra`` command.""" +from pathlib import Path + +from aiida_vibroscopy.cli.workflows.spectra.iraman import launch_workflow + + +# yapf: disable +def test_command_iraman_spectra(run_cli_process_launch_command, fixture_code, filepath_cli_fixture): + """Test invoking the launch command with only required inputs.""" + code = fixture_code('quantumespresso.pw').store() + options = [ + '--pw', code.full_label, + '-o', str(Path(filepath_cli_fixture, 'overrides', 'iraman-spectra.yaml')), + '-p', 'fast', + '-F', 'SSSP/1.3/PBEsol/efficiency', + ] + run_cli_process_launch_command(launch_workflow, options=options) + + options = [ + '--pw', code.full_label, + '-p', 'fast', + '-F', 'SSSP/1.3/PBEsol/efficiency', + ] + run_cli_process_launch_command(launch_workflow, options=options) + + options = [ + '--pw', code.full_label, + '-p', 'fast', + '-o', str(Path(filepath_cli_fixture, 'overrides', 'iraman-spectra.yaml')), + '-k', '2', '2', '2', '0.5', '0.5', '0.5', + ] + run_cli_process_launch_command(launch_workflow, options=options) + + options = [ + '--pw', code.full_label, + '-p', 'fast', + '-k', '2', '2', '2', '0.5', '0.5', '0.5', + ] + run_cli_process_launch_command(launch_workflow, options=options) diff --git a/tests/conftest.py b/tests/conftest.py index faed5b6..8bafc73 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -179,7 +179,7 @@ def sssp(aiida_profile, generate_upf_data): 'cutoff_rho': 240.0, } - label = 'SSSP/1.2/PBEsol/efficiency' + label = 'SSSP/1.3/PBEsol/efficiency' family = SsspFamily.create_from_folder(dirpath, label) family.set_cutoffs(cutoffs, stringency, unit='Ry') diff --git a/tests/data/test_vibro.py b/tests/data/test_vibro.py index 05f05b5..bcf828e 100644 --- a/tests/data/test_vibro.py +++ b/tests/data/test_vibro.py @@ -11,16 +11,29 @@ @pytest.mark.usefixtures('aiida_profile') -def test_methods(generate_vibrational_data_from_forces): +def test_methods(generate_vibrational_data_from_forces, ndarrays_regression): """Test `VibrationalMixin` methods.""" + import numpy as np + vibrational_data = generate_vibrational_data_from_forces() - vibrational_data.run_raman_susceptibility_tensors() - vibrational_data.run_polarization_vectors() - vibrational_data.run_single_crystal_raman_intensities([1, 0, 0], [-1, 0, 0]) - vibrational_data.run_powder_raman_intensities() - vibrational_data.run_single_crystal_ir_intensities([1, 0, 0]) - vibrational_data.run_powder_ir_intensities() + results = {} + vibrational_data.run_raman_susceptibility_tensors()[0] + # results['raman_susceptibility_tensors'] = vibrational_data.run_raman_susceptibility_tensors()[0] + vibrational_data.run_polarization_vectors()[0] + # results['polarization_vectors'] = vibrational_data.run_polarization_vectors()[0] + results['single_crystal_raman_intensities'] = vibrational_data.run_single_crystal_raman_intensities([1, 0, 0], + [-1, 0, 0])[0] + results['powder_raman_intensities'] = vibrational_data.run_powder_raman_intensities()[0] + results['single_crystal_ir_intensities'] = vibrational_data.run_single_crystal_ir_intensities([1, 0, 0])[0] + results['powder_ir_intensities'] = vibrational_data.run_powder_ir_intensities()[0] + freq_range = np.arange(10, 1000, 10) + results['complex_dielectric_function'] = vibrational_data.run_complex_dielectric_function(freq_range=freq_range) + results['normal_reflectivity_spectrum'] = vibrational_data.run_normal_reflectivity_spectrum([ + 0, 0, 1 + ], **dict(freq_range=freq_range)) + + ndarrays_regression.check(results, default_tolerance=dict(atol=1e-4, rtol=1e-4)) @pytest.mark.usefixtures('aiida_profile') diff --git a/tests/data/test_vibro/test_methods.npz b/tests/data/test_vibro/test_methods.npz new file mode 100644 index 0000000000000000000000000000000000000000..c4498f2f49bbe291afc354746dad01eec1110e33 GIT binary patch literal 6864 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 H{?mT}OKjiT literal 0 HcmV?d00001 diff --git a/tests/utils/test_validation.py b/tests/utils/test_validation.py new file mode 100644 index 0000000..b3ddb1b --- /dev/null +++ b/tests/utils/test_validation.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +################################################################################# +# Copyright (c), All rights reserved. # +# This file is part of the AiiDA-Vibroscopy code. # +# # +# The code is hosted on GitHub at https://github.com/bastonero/aiida-vibroscopy # +# For further information on the license, see the LICENSE.txt file # +################################################################################# +"""Test the :mod:`utils.validation`.""" +import numpy as np +import pytest + + +def test_validate_tot_magnetization(): + """Test `validate_tot_magnetization`.""" + from aiida_vibroscopy.utils.validation import validate_tot_magnetization + + magnetization = 1.1 + assert not validate_tot_magnetization(magnetization) + + magnetization = 1.3 + assert validate_tot_magnetization(magnetization) + + +@pytest.mark.parametrize(('value', 'message'), ( + (5.0, 'value is not of the right type; only `list`, `aiida.orm.List` and `numpy.ndarray`'), + ([0, 0], 'need exactly 3 diagonal elements or 3x3 arrays.'), + ([[0, 0, 0], [0], [0, 0, 0]], 'matrix need to have 3x1 or 3x3 shape.'), + (np.ones((3)), None), + (np.ones((3, 3)), None), +)) +def test_validate_matrix( + value, + message, +): + """Test `validate_tot_magnetization`.""" + from aiida_vibroscopy.utils.validation import validate_matrix + assert validate_matrix(value, None) == message + + +def test_validate_positive(): + """Test `validate_positive`.""" + from aiida.orm import Float + + from aiida_vibroscopy.utils.validation import validate_positive + + assert validate_positive(Float(1.0), None) is None + assert validate_positive(Float(-1.0), None) == 'specified value is negative.' diff --git a/tests/workflows/phonons/test_harmonic.py b/tests/workflows/phonons/test_harmonic.py index 75611d6..537227b 100644 --- a/tests/workflows/phonons/test_harmonic.py +++ b/tests/workflows/phonons/test_harmonic.py @@ -141,20 +141,36 @@ def test_setup(generate_workchain_harmonic): @pytest.mark.usefixtures('aiida_profile') def test_run_phonon(generate_workchain_harmonic): - """Test `HarmonicWorkChain.run_phonon`.""" - process = generate_workchain_harmonic() + """Test `HarmonicWorkChain.run_phonon`. + + Test that the symmetry options are passed as well. + """ + symmetry = {'symprec': orm.Float(1.0), 'distinguish_kinds': orm.Bool(True), 'is_symmetry': orm.Bool(False)} + process = generate_workchain_harmonic(append_inputs={'symmetry': symmetry}) process.setup() process.run_phonon() assert 'phonon' in process.ctx + workchain = orm.load_node(process.ctx.phonon.pk) + assert workchain.inputs.symmetry.symprec.value == 1.0 + assert workchain.inputs.symmetry.distinguish_kinds.value == True + assert workchain.inputs.symmetry.is_symmetry.value == False @pytest.mark.usefixtures('aiida_profile') def test_run_dielectric(generate_workchain_harmonic): - """Test `HarmonicWorkChain.run_dielectric`.""" - process = generate_workchain_harmonic() + """Test `HarmonicWorkChain.run_dielectric`. + + Test that the symmetry options are passed as well. + """ + symmetry = {'symprec': orm.Float(1.0), 'distinguish_kinds': orm.Bool(True), 'is_symmetry': orm.Bool(False)} + process = generate_workchain_harmonic(append_inputs={'symmetry': symmetry}) process.setup() process.run_dielectric() assert 'dielectric' in process.ctx + workchain = orm.load_node(process.ctx.dielectric.pk) + assert workchain.inputs.symmetry.symprec.value == 1.0 + assert workchain.inputs.symmetry.distinguish_kinds.value == True + assert workchain.inputs.symmetry.is_symmetry.value == False @pytest.mark.usefixtures('aiida_profile') diff --git a/tests/workflows/phonons/test_phonon.py b/tests/workflows/phonons/test_phonon.py index d2137cd..8f4ca9f 100644 --- a/tests/workflows/phonons/test_phonon.py +++ b/tests/workflows/phonons/test_phonon.py @@ -8,6 +8,7 @@ ################################################################################# """Tests for the :mod:`workflows.phonons.phonon` module.""" from aiida import orm +from aiida.common import AttributeDict from aiida_quantumespresso.data.hubbard_structure import HubbardStructureData import pytest @@ -18,7 +19,7 @@ def generate_workchain_phonon(generate_workchain, generate_inputs_pw_base): """Generate an instance of a `PhononWorkChain`.""" - def _generate_workchain_phonon(structure=None, append_inputs=None, return_inputs=False): + def _generate_workchain_phonon(structure=None, append_inputs=None, return_inputs=False, the_inputs=None): entry_point = 'vibroscopy.phonons.phonon' scf_inputs = generate_inputs_pw_base() @@ -29,17 +30,21 @@ def _generate_workchain_phonon(structure=None, append_inputs=None, return_inputs inputs = { 'scf': scf_inputs, 'settings': { - 'sleep_submission_time': 0. + 'sleep_submission_time': 0., + 'max_concurrent_base_workchains': -1, }, 'symmetry': {}, } if return_inputs: - return inputs + return AttributeDict(inputs) if append_inputs is not None: inputs.update(append_inputs) + if the_inputs is not None: + inputs = the_inputs + process = generate_workchain(entry_point, inputs) return process @@ -192,6 +197,15 @@ def test_set_reference_kpoints(generate_workchain_phonon): assert 'kpoints' in process.ctx + inputs = generate_workchain_phonon(return_inputs=True) + inputs['scf'].pop('kpoints') + inputs['scf']['kpoints_distance'] = 0.1 + process = generate_workchain_phonon(the_inputs=inputs) + process.setup() + process.set_reference_kpoints() + + assert 'kpoints' in process.ctx + @pytest.mark.usefixtures('aiida_profile') def test_run_base_supercell(generate_workchain_phonon): @@ -223,23 +237,53 @@ def test_inspect_base_supercell( assert process.ctx.is_insulator +@pytest.mark.usefixtures('aiida_profile') +def test_run_supercells(generate_workchain_phonon): + """Test `PhononWorkChain.run_supercells` method.""" + process = generate_workchain_phonon() + process.setup() + process.run_supercells() + + assert 'supercells' in process.outputs + assert 'supercells' in process.ctx + assert 'supercell_1' in process.outputs['supercells'] + + +@pytest.mark.usefixtures('aiida_profile') +def test_should_run_forces(generate_workchain_phonon): + """Test `PhononWorkChain.should_run_forces` method.""" + process = generate_workchain_phonon() + process.setup() + process.run_supercells() + assert process.should_run_forces() + + @pytest.mark.usefixtures('aiida_profile') def test_run_forces(generate_workchain_phonon, generate_base_scf_workchain_node): """Test `PhononWorkChain.run_forces` method.""" - process = generate_workchain_phonon() + append_inputs = { + 'settings': { + 'sleep_submission_time': 0., + 'max_concurrent_base_workchains': 1, + } + } + process = generate_workchain_phonon(append_inputs=append_inputs) process.setup() process.set_reference_kpoints() process.run_base_supercell() + process.run_supercells() assert 'scf_supercell_0' + num_supercells = len(process.ctx.supercells) process.ctx.scf_supercell_0 = generate_base_scf_workchain_node() process.run_forces() assert 'supercells' in process.outputs assert 'supercell_1' in process.outputs['supercells'] assert 'scf_supercell_1' in process.ctx + assert num_supercells == len(process.ctx.supercells) + 1 @pytest.mark.usefixtures('aiida_profile') @@ -251,6 +295,7 @@ def test_run_forces_with_hubbard(generate_workchain_phonon, generate_base_scf_wo process.setup() process.set_reference_kpoints() process.run_base_supercell() + process.run_supercells() assert 'scf_supercell_0' @@ -261,6 +306,7 @@ def test_run_forces_with_hubbard(generate_workchain_phonon, generate_base_scf_wo assert 'supercell_1' in process.outputs['supercells'] assert isinstance(process.outputs['supercells']['supercell_1'], HubbardStructureData) assert 'scf_supercell_1' in process.ctx + assert len(process.ctx.supercells) == 0 @pytest.mark.parametrize(('expected_result', 'exit_status'), diff --git a/tests/workflows/protocols/test_dielectric/test_default.yml b/tests/workflows/protocols/test_dielectric/test_default.yml index 6a0b3a4..8d267b0 100644 --- a/tests/workflows/protocols/test_dielectric/test_default.yml +++ b/tests/workflows/protocols/test_dielectric/test_default.yml @@ -4,6 +4,7 @@ property: raman scf: kpoints_distance: 0.4 kpoints_force_parity: false + max_iterations: 5 pw: code: test.quantumespresso.pw@localhost metadata: @@ -11,6 +12,7 @@ scf: max_wallclock_seconds: 43200 resources: num_machines: 1 + num_mpiprocs_per_machine: 1 withmpi: true parameters: CONTROL: diff --git a/tests/workflows/protocols/test_harmonic/test_default.yml b/tests/workflows/protocols/test_harmonic/test_default.yml index ca26ff2..b427097 100644 --- a/tests/workflows/protocols/test_harmonic/test_default.yml +++ b/tests/workflows/protocols/test_harmonic/test_default.yml @@ -6,6 +6,7 @@ dielectric: scf: kpoints_distance: 0.4 kpoints_force_parity: false + max_iterations: 5 pw: code: test.quantumespresso.pw@localhost metadata: @@ -13,6 +14,7 @@ dielectric: max_wallclock_seconds: 43200 resources: num_machines: 1 + num_mpiprocs_per_machine: 1 withmpi: true parameters: CONTROL: @@ -43,6 +45,7 @@ phonon: scf: kpoints_distance: 0.15 kpoints_force_parity: false + max_iterations: 5 pw: code: test.quantumespresso.pw@localhost metadata: @@ -50,6 +53,7 @@ phonon: max_wallclock_seconds: 43200 resources: num_machines: 1 + num_mpiprocs_per_machine: 1 withmpi: true parameters: CONTROL: diff --git a/tests/workflows/protocols/test_iraman/test_default.yml b/tests/workflows/protocols/test_iraman/test_default.yml index 1774b77..2a85cfc 100644 --- a/tests/workflows/protocols/test_iraman/test_default.yml +++ b/tests/workflows/protocols/test_iraman/test_default.yml @@ -6,6 +6,7 @@ dielectric: scf: kpoints_distance: 0.4 kpoints_force_parity: false + max_iterations: 5 pw: code: test.quantumespresso.pw@localhost metadata: @@ -13,6 +14,7 @@ dielectric: max_wallclock_seconds: 43200 resources: num_machines: 1 + num_mpiprocs_per_machine: 1 withmpi: true parameters: CONTROL: @@ -43,6 +45,7 @@ phonon: scf: kpoints_distance: 0.15 kpoints_force_parity: false + max_iterations: 5 pw: code: test.quantumespresso.pw@localhost metadata: @@ -50,6 +53,7 @@ phonon: max_wallclock_seconds: 43200 resources: num_machines: 1 + num_mpiprocs_per_machine: 1 withmpi: true parameters: CONTROL: diff --git a/tests/workflows/protocols/test_phonon.py b/tests/workflows/protocols/test_phonon.py index 4b99587..f106ed3 100644 --- a/tests/workflows/protocols/test_phonon.py +++ b/tests/workflows/protocols/test_phonon.py @@ -82,11 +82,15 @@ def test_overrides(fixture_code, generate_structure): 'displacement_generator': { 'distance': 0.005 }, + 'settings': { + 'max_concurrent_base_workchains': 1, + } } builder = PhononWorkChain.get_builder_from_protocol(code, structure, overrides=overrides) assert builder.primitive_matrix.get_list() == [[1, 0, 0], [0, 1, 0], [0, 0, 1]] assert builder.displacement_generator.get_dict() == {'distance': 0.005} + assert builder.settings.max_concurrent_base_workchains == 1 def test_phonon_properties(fixture_code, generate_structure): diff --git a/tests/workflows/protocols/test_phonon/test_default.yml b/tests/workflows/protocols/test_phonon/test_default.yml index f24ec35..0b252e5 100644 --- a/tests/workflows/protocols/test_phonon/test_default.yml +++ b/tests/workflows/protocols/test_phonon/test_default.yml @@ -4,6 +4,7 @@ displacement_generator: scf: kpoints_distance: 0.15 kpoints_force_parity: false + max_iterations: 5 pw: code: test.quantumespresso.pw@localhost metadata: @@ -11,6 +12,7 @@ scf: max_wallclock_seconds: 43200 resources: num_machines: 1 + num_mpiprocs_per_machine: 1 withmpi: true parameters: CONTROL: