├── .codecov.yml ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── CI.yaml │ └── draft-pdf.yml ├── .gitignore ├── .lgtm.yml ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── devtools ├── README.md ├── conda-envs │ └── test_env.yaml ├── legacy-miniconda-setup │ └── before_install.sh └── scripts │ └── create_conda_env.py ├── docs ├── Makefile ├── README.md ├── _static │ └── README.md ├── _templates │ └── README.md ├── api.rst ├── api │ ├── coordination.rst │ ├── networking.rst │ ├── pairing.rst │ ├── plotting.rst │ ├── residence.rst │ ├── solute.rst │ └── speciation.rst ├── conf.py ├── getting_started.ipynb ├── index.rst ├── make.bat ├── requirements.yaml ├── tutorials.rst └── tutorials │ ├── basics_tutorial.ipynb │ ├── clustering_and_residence_tutorial.ipynb │ ├── images │ ├── all_atoms.png │ ├── coordination_plot.png │ ├── network.png │ ├── rdf_plot.png │ ├── shell.png │ ├── shell_5.png │ ├── speciation_plot.png │ └── summary_figure.png │ ├── multi_atom_solutes.ipynb │ ├── plotting_tutorial.ipynb │ ├── rdf_fitting_demo.ipynb │ ├── setup_eax_solutes.py │ └── visualization_tutorial.ipynb ├── joss_paper ├── paper.bib ├── paper.md └── summary_figure.jpg ├── meta.yaml ├── pyproject.toml ├── readthedocs.yml ├── requirements.txt ├── setup.cfg ├── setup.py ├── solvation_analysis ├── __init__.py ├── _column_names.py ├── _utils.py ├── _version.py ├── coordination.py ├── networking.py ├── pairing.py ├── plotting.py ├── rdf_parser.py ├── residence.py ├── solute.py ├── speciation.py └── tests │ ├── __init__.py │ ├── conftest.py │ ├── data │ ├── README.md │ ├── bn_fec_data │ │ ├── bn_fec.data │ │ ├── bn_fec_elements.csv │ │ ├── bn_fec_short_unwrap.dcd │ │ ├── bn_fec_short_wrap.dcd │ │ └── bn_solv_df_large.csv │ ├── ea_fec_data │ │ ├── ea_fec.dcd │ │ └── ea_fec.pdb │ ├── eax_data │ │ ├── ea │ │ │ ├── topology.pdb │ │ │ └── trajectory_equil.dcd │ │ ├── eaf │ │ │ ├── topology.pdb │ │ │ └── trajectory_equil.dcd │ │ ├── fea │ │ │ ├── topology.pdb │ │ │ └── trajectory_equil.dcd │ │ └── feaf │ │ │ ├── topology.pdb │ │ │ └── trajectory_equil.dcd │ ├── iba_data │ │ ├── isobutyric_acid.dcd │ │ └── isobutyric_acid.pdb │ ├── rdf_non_solvated │ │ ├── rdf_bn_N_vs_fec_F_bins.npz │ │ ├── rdf_bn_N_vs_fec_F_data.npz │ │ ├── rdf_bn_N_vs_fec_O_bins.npz │ │ ├── rdf_bn_N_vs_fec_O_data.npz │ │ ├── rdf_bn_N_vs_fec_all_bins.npz │ │ ├── rdf_bn_N_vs_fec_all_data.npz │ │ ├── rdf_bn_N_vs_pf6_F_bins.npz │ │ ├── rdf_bn_N_vs_pf6_F_data.npz │ │ ├── rdf_bn_N_vs_pf6_all_bins.npz │ │ ├── rdf_bn_N_vs_pf6_all_data.npz │ │ ├── rdf_fec_F_vs_bn_N_bins.npz │ │ ├── rdf_fec_F_vs_bn_N_data.npz │ │ ├── rdf_fec_F_vs_bn_all_bins.npz │ │ ├── rdf_fec_F_vs_bn_all_data.npz │ │ ├── rdf_fec_F_vs_pf6_F_bins.npz │ │ ├── rdf_fec_F_vs_pf6_F_data.npz │ │ ├── rdf_fec_F_vs_pf6_all_bins.npz │ │ ├── rdf_fec_F_vs_pf6_all_data.npz │ │ ├── rdf_fec_O_vs_bn_N_bins.npz │ │ ├── rdf_fec_O_vs_bn_N_data.npz │ │ ├── rdf_fec_O_vs_bn_all_bins.npz │ │ ├── rdf_fec_O_vs_bn_all_data.npz │ │ ├── rdf_fec_O_vs_pf6_F_bins.npz │ │ ├── rdf_fec_O_vs_pf6_F_data.npz │ │ ├── rdf_fec_O_vs_pf6_all_bins.npz │ │ ├── rdf_fec_O_vs_pf6_all_data.npz │ │ ├── rdf_pf6_F_vs_bn_N_bins.npz │ │ ├── rdf_pf6_F_vs_bn_N_data.npz │ │ ├── rdf_pf6_F_vs_bn_all_bins.npz │ │ ├── rdf_pf6_F_vs_bn_all_data.npz │ │ ├── rdf_pf6_F_vs_fec_F_bins.npz │ │ ├── rdf_pf6_F_vs_fec_F_data.npz │ │ ├── rdf_pf6_F_vs_fec_O_bins.npz │ │ ├── rdf_pf6_F_vs_fec_O_data.npz │ │ ├── rdf_pf6_F_vs_fec_all_bins.npz │ │ └── rdf_pf6_F_vs_fec_all_data.npz │ ├── rdf_vs_li_easy │ │ ├── rdf_bn_N_bins.npz │ │ ├── rdf_bn_N_data.npz │ │ ├── rdf_bn_all_bins.npz │ │ ├── rdf_bn_all_data.npz │ │ ├── rdf_fec_F_bins.npz │ │ ├── rdf_fec_F_data.npz │ │ ├── rdf_fec_O_bins.npz │ │ ├── rdf_fec_O_data.npz │ │ ├── rdf_fec_all_bins.npz │ │ ├── rdf_fec_all_data.npz │ │ ├── rdf_pf6_F_bins.npz │ │ ├── rdf_pf6_F_data.npz │ │ ├── rdf_pf6_all_bins.npz │ │ ├── rdf_pf6_all_data.npz │ │ ├── rdf_universe_all_bins.npz │ │ └── rdf_universe_all_data.npz │ └── rdf_vs_li_hard │ │ ├── rdf_bn_N_bins.npz │ │ ├── rdf_bn_N_data.npz │ │ ├── rdf_bn_all_bins.npz │ │ ├── rdf_bn_all_data.npz │ │ ├── rdf_fec_F_bins.npz │ │ ├── rdf_fec_F_data.npz │ │ ├── rdf_fec_O_bins.npz │ │ ├── rdf_fec_O_data.npz │ │ ├── rdf_fec_all_bins.npz │ │ ├── rdf_fec_all_data.npz │ │ ├── rdf_pf6_F_bins.npz │ │ ├── rdf_pf6_F_data.npz │ │ ├── rdf_pf6_all_bins.npz │ │ ├── rdf_pf6_all_data.npz │ │ ├── rdf_universe_all_bins.npz │ │ └── rdf_universe_all_data.npz │ ├── datafiles.py │ ├── test_coordination.py │ ├── test_networking.py │ ├── test_pairing.py │ ├── test_plotting.py │ ├── test_rdf_parser.py │ ├── test_residence.py │ ├── test_selection.py │ ├── test_solute.py │ └── test_speciation.py └── versioneer.py /.codecov.yml: -------------------------------------------------------------------------------- 1 | # Codecov configuration to make it a bit less noisy 2 | coverage: 3 | status: 4 | patch: false 5 | project: 6 | default: 7 | threshold: 50% 8 | comment: 9 | layout: "header" 10 | require_changes: false 11 | branches: null 12 | behavior: default 13 | flags: null 14 | paths: null -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | solvation_analysis/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | We welcome contributions from external contributors, and this document 4 | describes how to merge code changes into this solvation_analysis. 5 | 6 | ## Getting Started 7 | 8 | * Make sure you have a [GitHub account](https://github.com/signup/free). 9 | * [Fork](https://help.github.com/articles/fork-a-repo/) this repository on GitHub. 10 | * On your local machine, 11 | [clone](https://help.github.com/articles/cloning-a-repository/) your fork of 12 | the repository. 13 | 14 | ## Making Changes 15 | 16 | * Add some really awesome code to your local fork. It's usually a [good 17 | idea](http://blog.jasonmeridth.com/posts/do-not-issue-pull-requests-from-your-master-branch/) 18 | to make changes on a 19 | [branch](https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/) 20 | with the branch name relating to the feature you are going to add. 21 | * When you are ready for others to examine and comment on your new feature, 22 | navigate to your fork of solvation_analysis on GitHub and open a [pull 23 | request](https://help.github.com/articles/using-pull-requests/) (PR). Note that 24 | after you launch a PR from one of your fork's branches, all 25 | subsequent commits to that branch will be added to the open pull request 26 | automatically. Each commit added to the PR will be validated for 27 | mergability, compilation and test suite compliance; the results of these tests 28 | will be visible on the PR page. 29 | * If you're providing a new feature, you must add test cases and documentation. 30 | * When the code is ready to go, make sure you run the test suite using pytest. 31 | * When you're ready to be considered for merging, check the "Ready to go" 32 | box on the PR page to let the solvation_analysis devs know that the changes are complete. 33 | The code will not be merged until this box is checked, the continuous 34 | integration returns checkmarks, 35 | and multiple core developers give "Approved" reviews. 36 | 37 | # Additional Resources 38 | 39 | * [General GitHub documentation](https://help.github.com/) 40 | * [PR best practices](http://codeinthehole.com/writing/pull-requests-and-other-good-practices-for-teams-using-github/) 41 | * [A guide to contributing to software packages](http://www.contribution-guide.org) 42 | * [Thinkful PR example](http://www.thinkful.com/learn/github-pull-request-tutorial/#Time-to-Submit-Your-First-PR) 43 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | Provide a brief description of the PR's purpose here. 3 | 4 | ## Todos 5 | Notable points that this PR has either accomplished or will accomplish. 6 | - [ ] TODO 1 7 | 8 | ## Questions 9 | - [ ] Question1 10 | 11 | ## Status 12 | - [ ] Ready to go -------------------------------------------------------------------------------- /.github/workflows/CI.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | # GitHub has started calling new repo's first branch "main" https://github.com/github/renaming 5 | # Existing codes likely still have "master" as the primary branch 6 | # Both are tracked here to keep legacy and new codes working 7 | push: 8 | branches: 9 | - "main" 10 | pull_request: 11 | branches: 12 | - "main" 13 | schedule: 14 | # Midnight Tuesdays and Fridays 15 | - cron: "0 0 * * 2,5" 16 | 17 | concurrency: 18 | # Probably overly cautious group naming. 19 | # Commits to develop/master will cancel each other, but PRs will only cancel 20 | # commits within the same PR 21 | group: "${{ github.ref }}-${{ github.head_ref }}" 22 | cancel-in-progress: true 23 | 24 | jobs: 25 | lint: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - uses: actions/setup-python@v5 31 | with: 32 | python-version: "3.9" 33 | cache: pip 34 | cache-dependency-path: pyproject.toml 35 | 36 | - uses: pre-commit/action@v3.0.0 37 | with: 38 | extra_args: --files solvation_analysis/* 39 | 40 | test: 41 | name: pip install on ${{ matrix.os }}, Python ${{ matrix.python-version }} 42 | runs-on: ${{ matrix.os }} 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | os: [macOS-latest, ubuntu-latest, windows-latest] 47 | python-version: [3.9, "3.10", 3.11, 3.12] 48 | 49 | steps: 50 | - uses: actions/checkout@v3 51 | 52 | - uses: actions/setup-python@v4 53 | with: 54 | python-version: ${{ matrix.python-version }} 55 | 56 | - name: Install package 57 | run: | 58 | python -m pip install . 59 | 60 | - name: Run tests 61 | run: | 62 | pytest -v --color=yes solvation_analysis/tests/ 63 | 64 | - name: CodeCov 65 | uses: codecov/codecov-action@v1 66 | with: 67 | file: ./coverage.xml 68 | flags: unittests 69 | name: codecov-${{ matrix.os }}-py${{ matrix.python-version }} 70 | -------------------------------------------------------------------------------- /.github/workflows/draft-pdf.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | 3 | jobs: 4 | paper: 5 | runs-on: ubuntu-latest 6 | name: Paper Draft 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v2 10 | - name: Build draft PDF 11 | uses: openjournals/openjournals-draft-action@master 12 | with: 13 | journal: joss 14 | # This should be the path to the paper within your repo. 15 | paper-path: joss_paper/paper.md 16 | - name: Upload 17 | uses: actions/upload-artifact@v1 18 | with: 19 | name: paper 20 | # This is the output path where Pandoc will write the compiled 21 | # PDF. Note, this should be the same directory as the input 22 | # paper.md 23 | path: joss_paper/paper.pdf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.DS_Store 6 | *.vscode 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | .pytest_cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # dotenv 86 | .env 87 | 88 | # virtualenv 89 | .venv 90 | venv/ 91 | ENV/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # profraw files from LLVM? Unclear exactly what triggers this 107 | # There are reports this comes from LLVM profiling, but also Xcode 9. 108 | *profraw 109 | 110 | # Pycharm 111 | .idea -------------------------------------------------------------------------------- /.lgtm.yml: -------------------------------------------------------------------------------- 1 | # Configure LGTM for this package 2 | 3 | extraction: 4 | python: # Configure Python 5 | python_setup: # Configure the setup 6 | version: 3 # Specify Version 3 7 | path_classifiers: 8 | library: 9 | - versioneer.py # Set Versioneer.py to an external "library" (3rd party code) 10 | - devtools/* 11 | generated: 12 | - solvation_analysis/_version.py 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3 3 | repos: 4 | - repo: https://github.com/charliermarsh/ruff-pre-commit 5 | rev: v0.4.2 6 | hooks: 7 | - id: ruff 8 | args: [--fix] 9 | - id: ruff-format 10 | - repo: https://github.com/pre-commit/pre-commit-hooks 11 | rev: v4.6.0 12 | hooks: 13 | - id: check-yaml 14 | - id: fix-encoding-pragma 15 | args: [--remove] 16 | - id: end-of-file-fixer 17 | - id: trailing-whitespace 18 | - repo: https://github.com/pre-commit/pygrep-hooks 19 | rev: v1.10.0 20 | hooks: 21 | - id: python-use-type-annotations 22 | - id: rst-backticks 23 | - id: rst-directive-colons 24 | - id: rst-inline-touching-normal 25 | - repo: https://github.com/pre-commit/mirrors-mypy 26 | rev: v1.10.0 27 | hooks: 28 | - id: mypy 29 | files: ^src/ 30 | additional_dependencies: 31 | - tokenize-rt==4.1.0 32 | - types-paramiko 33 | - repo: https://github.com/codespell-project/codespell 34 | rev: v2.2.6 35 | hooks: 36 | - id: codespell 37 | stages: [commit, commit-msg] 38 | args: [--ignore-words-list, 'titel,statics,ba,nd,te,atomate'] 39 | types_or: [python, rst, markdown] 40 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | **Table of Contents** 3 | 4 | - [MDAnalysis Code of Conduct and Community Guidelines](#mdanalysis-code-of-conduct-and-community-guidelines) 5 | - [Reporting](#reporting) 6 | - [Enforcement](#enforcement) 7 | - [Acknowledgment](#acknowledgment) 8 | 9 | 10 | # MDAnalysis Code of Conduct and Community Guidelines 11 | 12 | MDAnalysis is an engaged and respectful community made up of people from all 13 | over the world. Your involvement helps us to further our mission and to create 14 | an open platform that serves a broad range of communities, from research and 15 | education to industry and beyond. This diversity is one of our biggest 16 | strengths, but it can also lead to communication issues and conflicts. 17 | Therefore, we have a few ground rules we ask that our community members adhere 18 | to. 19 | 20 | Fundamentally, we are committed to providing a productive, 21 | harassment-free environment for everyone. Rather than considering this 22 | code an exhaustive list of things that you can’t do, take it in the 23 | spirit it is intended - a guide to make it easier to enrich all of us 24 | and the communities in which we participate. 25 | 26 | Importantly: as a member of our community, you are also a steward of these 27 | values. Not all problems need to be resolved via formal processes, and often a 28 | quick, friendly but clear word on an online forum or in person can help resolve 29 | a misunderstanding and de-escalate things. 30 | 31 | However, sometimes these informal processes may be inadequate: they fail to 32 | work, there is urgency or risk to someone, nobody is intervening publicly and 33 | you don't feel comfortable speaking in public, etc. For these or other reasons, 34 | structured follow-up may be necessary and here we provide the means for that: we 35 | welcome reports by 36 | emailing [*Conduct-email*][conduct-mail] or 37 | in anonymous by filling out [*this form*][conduct-form]. 38 | 39 | This code applies equally to founders, developers, mentors and new 40 | community members, in all spaces managed by MDAnalysis. This 41 | includes the mailing lists, our GitHub organizations, our chat rooms, 42 | in-person events, and any other forums created by the project team. In 43 | addition, violations of this code outside these spaces may affect a 44 | person's ability to participate within them. 45 | 46 | By embracing the following principles, guidelines and actions to follow or 47 | avoid, you will help us make MDAnalysis a welcoming and productive community. If 48 | that doesn't answer your questions, feel free to contact us 49 | at our [*user-mailing-list*](mailto:mdnalysis-discussions@googlegroups.com). 50 | 51 | 52 | 1. **Be friendly and patient**. 53 | 54 | 2. **Be welcoming**. We strive to be a community that welcomes and supports 55 | people of all backgrounds and identities. This includes, but is not limited 56 | to, members of any race, ethnicity, culture, national origin, color, 57 | immigration status, social and economic class, educational level, sex, sexual 58 | orientation, gender identity and expression, age, physical appearance, family 59 | status, political belief, technological or professional choices, academic 60 | discipline, religion, mental ability, and physical ability. 61 | 62 | 3. **Be considerate**. Your work will be used by other people, and you in turn 63 | will depend on the work of others. Any decision you take will affect users 64 | and colleagues, and you should take those consequences into account when 65 | making decisions. Remember that we're a world-wide community. You may be 66 | communicating with someone with a different primary language or cultural 67 | background. 68 | 69 | 4. **Be respectful**. Not all of us will agree all the time, but disagreement is 70 | no excuse for poor behavior or poor manners. We might all experience some 71 | frustration now and then, but we cannot allow that frustration to turn into a 72 | personal attack. It’s important to remember that a community where people 73 | feel uncomfortable or threatened is not a productive one. 74 | 75 | 5. **Be careful in the words that you choose**. Be kind to others. Do not insult 76 | or put down other community members. Harassment and other exclusionary 77 | behavior are not acceptable. This includes, but is not limited to: 78 | * threats or violent language directed against another person 79 | * discriminatory jokes and language 80 | * posting sexually explicit or violent material 81 | * posting (or threatening to post) other people's personally identifying 82 | information ("doxing") 83 | * personal insults, especially those using racist or sexist terms 84 | * unwelcome sexual attention 85 | * advocating for, or encouraging, any of the above behavior 86 | * repeated harassment of others. In general, if someone asks you to stop, 87 | then stop 88 | 89 | 6. **Moderate your expectations**. Many in our community volunteer their time. 90 | They are probably not purposefully ignoring issues, refusing to engage in 91 | discussion, avoiding features, etc. but often just unavailable. 92 | 93 | 7. **When we disagree, try to understand why**. Disagreements, both social and 94 | technical, happen all the time and MDAnalysis is no exception. It is important 95 | that we resolve disagreements and differing views constructively. Remember 96 | that we’re different. The strength of MDAnalysis comes from its varied community 97 | that includes people from a wide range of backgrounds. Different people have 98 | different perspectives on issues. Being unable to understand why someone 99 | holds a viewpoint doesn’t mean they’re wrong. Don’t forget that it is human 100 | to err and blaming each other doesn’t get us anywhere. Instead, focus on 101 | helping to resolve issues and learning from mistakes. 102 | 103 | 8. **A simple apology can go a long way**. It can often de-escalate a situation, 104 | and telling someone that you are sorry is act of empathy that doesn’t 105 | automatically imply an admission of guilt. 106 | 107 | # Reporting 108 | 109 | If someone makes you or any other contributor feel unsafe or unwelcome, please 110 | report this in a timely manner. Code of conduct violations reduce the value of 111 | the community for everyone and we take them seriously. All complaints will be 112 | reviewed and investigated and will result in a response that is deemed necessary 113 | and appropriate to the circumstances. 114 | 115 | You can file a report by emailing 116 | the [*Conduct-mail*][conduct-mail] or by 117 | filing out [this form][conduct-form]. The project team is obligated to maintain 118 | confidentiality with regard to the reporter of an incident. 119 | 120 | The online form gives you the option to keep your report anonymous or request 121 | that we follow up with you directly. While we cannot follow up on an anonymous 122 | report, we will take appropriate action. 123 | 124 | # Enforcement 125 | 126 | When a report is sent to us we will reply as soon as possible to confirm receipt; 127 | we strive to answer in less than 24 hours. We will review the incident and 128 | determine, to the best of our ability 129 | 130 | - what happened 131 | - whether this event constitutes a code of conduct violation 132 | - who, if anyone, was at fault 133 | - whether this is an ongoing situation 134 | 135 | This information will be collected in writing. We strive to reach a resolution 136 | within a week of confirmation. Once a resolution has been agreed upon, but before it is 137 | enacted, we will contact the original reporter and any other affected parties 138 | and explain the proposed resolution. We will ask if this resolution is 139 | acceptable and note feedback for the record. We are, however, not required to act 140 | on this feedback. 141 | 142 | 143 | # Acknowledgment 144 | 145 | Original text courtesy of 146 | the 147 | [*Speak Up!*](http://web.archive.org/web/20141109123859/http://speakup.io/coc.html), 148 | [*Django*](https://www.djangoproject.com/conduct), 149 | [*Contributor Covenant*](http://contributor-covenant.org/), 150 | and 151 | [*Jupyter*](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md) projects, 152 | modified by MDAnalysis. We are grateful to those projects for contributing these 153 | materials under open licensing terms for us to easily reuse. 154 | 155 | All content on this page is licensed under a [*Creative Commons 156 | Attribution*](http://creativecommons.org/licenses/by/3.0/) license. 157 | 158 | [conduct-mail]: mailto:mdnalysis-conduct@googlegroups.com 159 | [conduct-form]: https://goo.gl/forms/w2IwBKkY3oT0aVEB3 -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include requirements.txt 4 | include MANIFEST.in 5 | include CODE_OF_CONDUCT.md 6 | include versioneer.py 7 | 8 | graft solvation_analysis 9 | global-exclude *.py[cod] __pycache__ *.soinclude solvation_analysis/_version.py 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SolvationAnalysis 2 | ============================== 3 | [//]: # (Badges) 4 | 5 | [![Powered by NumFOCUS](https://img.shields.io/badge/powered%20by-NumFOCUS-orange.svg?style=flat&colorA=E1523D&colorB=007D8A)](https://www.numfocus.org/) 6 | [![Powered by MDAnalysis](https://img.shields.io/badge/powered%20by-MDAnalysis-orange.svg?logoWidth=16&logo=data:image/x-icon;base64,AAABAAEAEBAAAAEAIAAoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJD+XwCY/fEAkf3uAJf97wGT/a+HfHaoiIWE7n9/f+6Hh4fvgICAjwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACT/yYAlP//AJ///wCg//8JjvOchXly1oaGhv+Ghob/j4+P/39/f3IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJH8aQCY/8wAkv2kfY+elJ6al/yVlZX7iIiI8H9/f7h/f38UAAAAAAAAAAAAAAAAAAAAAAAAAAB/f38egYF/noqAebF8gYaagnx3oFpUUtZpaWr/WFhY8zo6OmT///8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgICAn46Ojv+Hh4b/jouJ/4iGhfcAAADnAAAA/wAAAP8AAADIAAAAAwCj/zIAnf2VAJD/PAAAAAAAAAAAAAAAAICAgNGHh4f/gICA/4SEhP+Xl5f/AwMD/wAAAP8AAAD/AAAA/wAAAB8Aov9/ALr//wCS/Z0AAAAAAAAAAAAAAACBgYGOjo6O/4mJif+Pj4//iYmJ/wAAAOAAAAD+AAAA/wAAAP8AAABhAP7+FgCi/38Axf4fAAAAAAAAAAAAAAAAiIiID4GBgYKCgoKogoB+fYSEgZhgYGDZXl5e/m9vb/9ISEjpEBAQxw8AAFQAAAAAAAAANQAAADcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjo6Mb5iYmP+cnJz/jY2N95CQkO4pKSn/AAAA7gAAAP0AAAD7AAAAhgAAAAEAAAAAAAAAAACL/gsAkv2uAJX/QQAAAAB9fX3egoKC/4CAgP+NjY3/c3Nz+wAAAP8AAAD/AAAA/wAAAPUAAAAcAAAAAAAAAAAAnP4NAJL9rgCR/0YAAAAAfX19w4ODg/98fHz/i4uL/4qKivwAAAD/AAAA/wAAAP8AAAD1AAAAGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALGxsVyqqqr/mpqa/6mpqf9KSUn/AAAA5QAAAPkAAAD5AAAAhQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADkUFBSuZ2dn/3V1df8uLi7bAAAATgBGfyQAAAA2AAAAMwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB0AAADoAAAA/wAAAP8AAAD/AAAAWgC3/2AAnv3eAJ/+dgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9AAAA/wAAAP8AAAD/AAAA/wAKDzEAnP3WAKn//wCS/OgAf/8MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQAAANwAAADtAAAA7QAAAMAAABUMAJn9gwCe/e0Aj/2LAP//AQAAAAAAAAAA)](https://www.mdanalysis.org) 7 | [![GitHub Actions Status](https://github.com/MDAnalysis/solvation-analysis/workflows/CI/badge.svg)](https://github.com/MDAnalysis/solvation-analysis/actions?query=workflow%3ACI) 8 | [![codecov](https://codecov.io/gh/MDAnalysis/solvation-analysis//branch/main/graph/badge.svg)](https://codecov.io/gh/MDAnalysis/solvation-analysis//branch/main) 9 | [![docs](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://solvation-analysis.readthedocs.io/en/latest/) 10 | [![pub](https://joss.theoj.org/papers/10.21105/joss.05183/status.svg)](https://doi.org/10.21105/joss.05183) 11 | 12 | [//]: # ([![DOI](https://zenodo.org/badge/371804402.svg)](https://zenodo.org/badge/latestdoi/371804402)) 13 | 14 | 15 | --- 16 | 17 | Solvation analysis implements a robust, cohesive, and fast set of methods 18 | for analyzing the solvation structure of a liquid. It integrates with 19 | [MDAnalysis](https://www.mdanalysis.org/) to seamlessly calculate, query, 20 | and visualize solvation information. 21 | 22 | Find documentation and tutorials on [readthedocs](https://solvation-analysis.readthedocs.io/en/latest/). 23 | 24 | ### Installing SolvationAnalysis 25 | 26 | SolvationAnalysis is available on PyPI and conda-forge can be installed with pip or conda: 27 | 28 | ```bash 29 | pip install solvation-analysis 30 | 31 | # or 32 | 33 | conda install -c conda-forge solvation_analysis 34 | ``` 35 | 36 | ### Solvation Analysis Summarized 37 | 38 | ![summary](docs/tutorials/images/summary_figure.png) 39 | 40 | 41 | ### Visualization 42 | 43 | With just a few lines of code, solvation analysis can calculate detailed 44 | properties within and between solvent systems. A few examples are shown below. 45 | 46 | ![solvation_analysis.plotting.compare_coordination_numbers](docs/tutorials/images/coordination_plot.png) 47 | 48 | ![solvation_analysis.plotting.plot_speciation](docs/tutorials/images/speciation_plot.png) 49 | 50 | ![solvation_analysis.plotting.plot_rdfs](docs/tutorials/images/rdf_plot.png) 51 | 52 | 53 | ### Contributing 54 | 55 | Contributions, both issues and PRs, are welcome. If you'd like to contribute, we ask that you 56 | follow the community guidelines outlined in the [MDAnalysis Code of Conduct](https://www.mdanalysis.org/pages/conduct/). 57 | 58 | Solvation Analysis uses [pre-commit](https://pre-commit.com/) for linting. Make sure to install 59 | the pre-commit hooks if you are working on a contribution. 60 | 61 | ### Citation 62 | 63 | This work is described in [JOSS](https://doi.org/10.21105/joss.05183), please cite it if you make 64 | use of this package in published work. 65 | 66 | --- 67 | 68 | Project based on the 69 | [Computational Molecular Science Python Cookiecutter](https://github.com/molssi/cookiecutter-cms) version 1.5. 70 | 71 | 72 | 73 | [readthedocs]: (https://solvation-analysis.readthedocs.io/en/latest/) 74 | [robust, cohesive, and fast set of methods]:(https://summerofcode.withgoogle.com/projects/#6227159028334592) 75 | [Google Summer of Code]: https://summerofcode.withgoogle.com/ 76 | [MDAnalysis]: https://www.mdanalysis.org/ 77 | -------------------------------------------------------------------------------- /devtools/README.md: -------------------------------------------------------------------------------- 1 | # Development, testing, and deployment tools 2 | 3 | This directory contains a collection of tools for running Continuous Integration (CI) tests, 4 | conda installation, and other development tools not directly related to the coding process. 5 | 6 | 7 | ## Manifest 8 | 9 | ### Continuous Integration 10 | 11 | You should test your code, but do not feel compelled to use these specific programs. You also may not need Unix and 12 | Windows testing if you only plan to deploy on specific platforms. These are just to help you get started. 13 | 14 | The items in this directory have been left for legacy purposes since the change to GitHub Actions, 15 | They will likely be removed in a future version. 16 | 17 | * `legacy-miniconda-setup`: A preserved copy of a helper directory which made Linux and OSX based testing through [Travis-CI](https://about.travis-ci.com/) simpler 18 | * `before_install.sh`: Pip/Miniconda pre-package installation script for Travis. No longer needed thanks to 19 | [GitHub Actions](https://docs.github.com/en/free-pro-team@latest/actions) and the [conda-incubator/setup-miniconda Action](https://github.com/conda-incubator/setup-miniconda) 20 | 21 | ### Conda Environment: 22 | 23 | This directory contains the files to setup the Conda environment for testing purposes 24 | 25 | * `conda-envs`: directory containing the YAML file(s) which fully describe Conda Environments, their dependencies, and those dependency provenance's 26 | * `test_env.yaml`: Simple test environment file with base dependencies. Channels are not specified here and therefore respect global Conda configuration 27 | 28 | ### Additional Scripts: 29 | 30 | This directory contains OS agnostic helper scripts which don't fall in any of the previous categories 31 | * `scripts` 32 | * `create_conda_env.py`: Helper program for spinning up new conda environments based on a starter file with Python Version and Env. Name command-line options 33 | 34 | 35 | ## How to contribute changes 36 | - Clone the repository if you have write access to the main repo, fork the repository if you are a collaborator. 37 | - Make a new branch with `git checkout -b {your branch name}` 38 | - Make changes and test your code 39 | - Ensure that the test environment dependencies (`conda-envs`) line up with the build and deploy dependencies (`conda-recipe/meta.yaml`) 40 | - Push the branch to the repo (either the main or your fork) with `git push -u origin {your branch name}` 41 | * Note that `origin` is the default name assigned to the remote, yours may be different 42 | - Make a PR on GitHub with your changes 43 | - We'll review the changes and get your code into the repo after lively discussion! 44 | 45 | 46 | ## Checklist for updates 47 | - [ ] Make sure there is an/are issue(s) opened for your specific update 48 | - [ ] Create the PR, referencing the issue 49 | - [ ] Debug the PR as needed until tests pass 50 | - [ ] Tag the final, debugged version 51 | * `git tag -a X.Y.Z [latest pushed commit] && git push --follow-tags` 52 | - [ ] Get the PR merged in 53 | 54 | ## Versioneer Auto-version 55 | [Versioneer](https://github.com/warner/python-versioneer) will automatically infer what version 56 | is installed by looking at the `git` tags and how many commits ahead this version is. The format follows 57 | [PEP 440](https://www.python.org/dev/peps/pep-0440/) and has the regular expression of: 58 | ```regexp 59 | \d+.\d+.\d+(?\+\d+-[a-z0-9]+) 60 | ``` 61 | If the version of this commit is the same as a `git` tag, the installed version is the same as the tag, 62 | e.g. `solvation_analysis-0.1.2`, otherwise it will be appended with `+X` where `X` is the number of commits 63 | ahead from the last tag, and then `-YYYYYY` where the `Y`'s are replaced with the `git` commit hash. 64 | -------------------------------------------------------------------------------- /devtools/conda-envs/test_env.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | - python>=3.8 7 | - pip 8 | - MDAnalysis>=2.0.0 9 | - scipy 10 | - setuptools 11 | - numpy 12 | - pandas>=2.2 13 | - matplotlib 14 | - statsmodels 15 | - pytest 16 | - pytest-cov 17 | - codecov 18 | - rdkit 19 | - plotly 20 | -------------------------------------------------------------------------------- /devtools/legacy-miniconda-setup/before_install.sh: -------------------------------------------------------------------------------- 1 | # Temporarily change directory to $HOME to install software 2 | pushd . 3 | cd $HOME 4 | # Make sure some level of pip is installed 5 | python -m ensurepip 6 | 7 | # Install Miniconda 8 | if [ "$TRAVIS_OS_NAME" == "osx" ]; then 9 | # Make OSX md5 mimic md5sum from linux, alias does not work 10 | md5sum () { 11 | command md5 -r "$@" 12 | } 13 | MINICONDA=Miniconda3-latest-MacOSX-x86_64.sh 14 | else 15 | MINICONDA=Miniconda3-latest-Linux-x86_64.sh 16 | fi 17 | MINICONDA_HOME=$HOME/miniconda 18 | MINICONDA_MD5=$(wget -qO- https://repo.anaconda.com/miniconda/ | grep -A3 $MINICONDA | sed -n '4p' | sed -n 's/ *\(.*\)<\/td> */\1/p') 19 | wget -q https://repo.anaconda.com/miniconda/$MINICONDA 20 | if [[ $MINICONDA_MD5 != $(md5sum $MINICONDA | cut -d ' ' -f 1) ]]; then 21 | echo "Miniconda MD5 mismatch" 22 | exit 1 23 | fi 24 | bash $MINICONDA -b -p $MINICONDA_HOME 25 | 26 | # Configure miniconda 27 | export PIP_ARGS="-U" 28 | # New to conda >=4.4 29 | echo ". $MINICONDA_HOME/etc/profile.d/conda.sh" >> ~/.bashrc # Source the profile.d file 30 | echo "conda activate" >> ~/.bashrc # Activate conda 31 | source ~/.bashrc # source file to get new commands 32 | #export PATH=$MINICONDA_HOME/bin:$PATH # Old way, should not be needed anymore 33 | 34 | conda config --add channels conda-forge 35 | 36 | conda config --set always_yes yes 37 | conda install conda conda-build jinja2 anaconda-client 38 | conda update --quiet --all 39 | 40 | # Restore original directory 41 | popd 42 | -------------------------------------------------------------------------------- /devtools/scripts/create_conda_env.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import re 4 | import glob 5 | import shutil 6 | import subprocess as sp 7 | from tempfile import TemporaryDirectory 8 | from contextlib import contextmanager 9 | # YAML imports 10 | try: 11 | import yaml # PyYAML 12 | loader = yaml.load 13 | except ImportError: 14 | try: 15 | import ruamel_yaml as yaml # Ruamel YAML 16 | except ImportError: 17 | try: 18 | # Load Ruamel YAML from the base conda environment 19 | from importlib import util as import_util 20 | CONDA_BIN = os.path.dirname(os.environ['CONDA_EXE']) 21 | ruamel_yaml_path = glob.glob(os.path.join(CONDA_BIN, '..', 22 | 'lib', 'python*.*', 'site-packages', 23 | 'ruamel_yaml', '__init__.py'))[0] 24 | # Based on importlib example, but only needs to load_module since its the whole package, not just 25 | # a module 26 | spec = import_util.spec_from_file_location('ruamel_yaml', ruamel_yaml_path) 27 | yaml = spec.loader.load_module() 28 | except (KeyError, ImportError, IndexError): 29 | raise ImportError("No YAML parser could be found in this or the conda environment. " 30 | "Could not find PyYAML or Ruamel YAML in the current environment, " 31 | "AND could not find Ruamel YAML in the base conda environment through CONDA_EXE path. " 32 | "Environment not created!") 33 | loader = yaml.YAML(typ="safe").load # typ="safe" avoids odd typing on output 34 | 35 | 36 | @contextmanager 37 | def temp_cd(): 38 | """Temporary CD Helper""" 39 | cwd = os.getcwd() 40 | with TemporaryDirectory() as td: 41 | try: 42 | os.chdir(td) 43 | yield 44 | finally: 45 | os.chdir(cwd) 46 | 47 | 48 | # Args 49 | parser = argparse.ArgumentParser(description='Creates a conda environment from file for a given Python version.') 50 | parser.add_argument('-n', '--name', type=str, 51 | help='The name of the created Python environment') 52 | parser.add_argument('-p', '--python', type=str, 53 | help='The version of the created Python environment') 54 | parser.add_argument('conda_file', 55 | help='The file for the created Python environment') 56 | 57 | args = parser.parse_args() 58 | 59 | # Open the base file 60 | with open(args.conda_file, "r") as handle: 61 | yaml_script = loader(handle.read()) 62 | 63 | python_replacement_string = "python {}*".format(args.python) 64 | 65 | try: 66 | for dep_index, dep_value in enumerate(yaml_script['dependencies']): 67 | if re.match('python([ ><=*]+[0-9.*]*)?$', dep_value): # Match explicitly 'python' and its formats 68 | yaml_script['dependencies'].pop(dep_index) 69 | break # Making the assumption there is only one Python entry, also avoids need to enumerate in reverse 70 | except (KeyError, TypeError): 71 | # Case of no dependencies key, or dependencies: None 72 | yaml_script['dependencies'] = [] 73 | finally: 74 | # Ensure the python version is added in. Even if the code does not need it, we assume the env does 75 | yaml_script['dependencies'].insert(0, python_replacement_string) 76 | 77 | # Figure out conda path 78 | if "CONDA_EXE" in os.environ: 79 | conda_path = os.environ["CONDA_EXE"] 80 | else: 81 | conda_path = shutil.which("conda") 82 | if conda_path is None: 83 | raise RuntimeError("Could not find a conda binary in CONDA_EXE variable or in executable search path") 84 | 85 | print("CONDA ENV NAME {}".format(args.name)) 86 | print("PYTHON VERSION {}".format(args.python)) 87 | print("CONDA FILE NAME {}".format(args.conda_file)) 88 | print("CONDA PATH {}".format(conda_path)) 89 | 90 | # Write to a temp directory which will always be cleaned up 91 | with temp_cd(): 92 | temp_file_name = "temp_script.yaml" 93 | with open(temp_file_name, 'w') as f: 94 | f.write(yaml.dump(yaml_script)) 95 | sp.call("{} env create -n {} -f {}".format(conda_path, args.name, temp_file_name), shell=True) 96 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = solvation_analysis 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Compiling SolvationAnalysis's Documentation 2 | 3 | The docs for this project are built with [Sphinx](http://www.sphinx-doc.org/en/master/). 4 | To compile the docs, first ensure that Sphinx and the ReadTheDocs theme are installed. 5 | 6 | 7 | ```bash 8 | conda install sphinx sphinx_rtd_theme 9 | ``` 10 | 11 | 12 | Once installed, you can use the `Makefile` in this directory to compile static HTML pages by 13 | ```bash 14 | make html 15 | ``` 16 | 17 | The compiled docs will be in the `_build` directory and can be viewed by opening `index.html` (which may itself 18 | be inside a directory called `html/` depending on what version of Sphinx is installed). 19 | 20 | 21 | A configuration file for [Read The Docs](https://readthedocs.org/) (readthedocs.yaml) is included in the top level of the repository. To use Read the Docs to host your documentation, go to https://readthedocs.org/ and connect this repository. You may need to change your default branch to `main` under Advanced Settings for the project. 22 | 23 | If you would like to use Read The Docs with `autodoc` (included automatically) and your package has dependencies, you will need to include those dependencies in your documentation yaml file (`docs/requirements.yaml`). 24 | 25 | -------------------------------------------------------------------------------- /docs/_static/README.md: -------------------------------------------------------------------------------- 1 | # Static Doc Directory 2 | 3 | Add any paths that contain custom static files (such as style sheets) here, 4 | relative to the `conf.py` file's directory. 5 | They are copied after the builtin static files, 6 | so a file named "default.css" will overwrite the builtin "default.css". 7 | 8 | The path to this folder is set in the Sphinx `conf.py` file in the line: 9 | ```python 10 | templates_path = ['_static'] 11 | ``` 12 | 13 | ## Examples of file to add to this directory 14 | * Custom Cascading Style Sheets 15 | * Custom JavaScript code 16 | * Static logo images 17 | -------------------------------------------------------------------------------- /docs/_templates/README.md: -------------------------------------------------------------------------------- 1 | # Templates Doc Directory 2 | 3 | Add any paths that contain templates here, relative to 4 | the `conf.py` file's directory. 5 | They are copied after the builtin template files, 6 | so a file named "page.html" will overwrite the builtin "page.html". 7 | 8 | The path to this folder is set in the Sphinx `conf.py` file in the line: 9 | ```python 10 | html_static_path = ['_templates'] 11 | ``` 12 | 13 | ## Examples of file to add to this directory 14 | * HTML extensions of stock pages like `page.html` or `layout.html` 15 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | .. autosummary:: 5 | :toctree: stubs 6 | 7 | .. toctree:: 8 | :maxdepth: 3 9 | :caption: API Reference 10 | 11 | api/solute 12 | api/coordination 13 | api/networking 14 | api/pairing 15 | api/residence 16 | api/speciation 17 | api/plotting 18 | -------------------------------------------------------------------------------- /docs/api/coordination.rst: -------------------------------------------------------------------------------- 1 | Coordination 2 | ============ 3 | 4 | .. automodule:: solvation_analysis.coordination 5 | :members: -------------------------------------------------------------------------------- /docs/api/networking.rst: -------------------------------------------------------------------------------- 1 | Networking 2 | ========== 3 | 4 | .. automodule:: solvation_analysis.networking 5 | :members: -------------------------------------------------------------------------------- /docs/api/pairing.rst: -------------------------------------------------------------------------------- 1 | Pairing 2 | ======= 3 | 4 | .. automodule:: solvation_analysis.pairing 5 | :members: -------------------------------------------------------------------------------- /docs/api/plotting.rst: -------------------------------------------------------------------------------- 1 | Plotting 2 | ======== 3 | 4 | .. automodule:: solvation_analysis.plotting 5 | :members: -------------------------------------------------------------------------------- /docs/api/residence.rst: -------------------------------------------------------------------------------- 1 | Residence 2 | ========= 3 | 4 | .. automodule:: solvation_analysis.residence 5 | :members: -------------------------------------------------------------------------------- /docs/api/solute.rst: -------------------------------------------------------------------------------- 1 | Solute 2 | ====== 3 | 4 | .. automodule:: solvation_analysis.solute 5 | :members: -------------------------------------------------------------------------------- /docs/api/speciation.rst: -------------------------------------------------------------------------------- 1 | Speciation 2 | ========== 3 | 4 | .. automodule:: solvation_analysis.speciation 5 | :members: -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/stable/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | 15 | # Incase the project was not installed 16 | import os 17 | import sys 18 | sys.path.insert(0, os.path.abspath('..')) 19 | 20 | import solvation_analysis 21 | 22 | 23 | # -- Project information ----------------------------------------------------- 24 | 25 | project = 'SolvationAnalysis' 26 | copyright = ("2021, Orion Cohen. Project structure based on the " 27 | "Computational Molecular Science Python Cookiecutter version 1.5") 28 | author = 'Orion Cohen' 29 | 30 | # The short X.Y version 31 | version = '' 32 | # The full version, including alpha/beta/rc tags 33 | release = '' 34 | 35 | 36 | # -- General configuration --------------------------------------------------- 37 | 38 | # If your documentation needs a minimal Sphinx version, state it here. 39 | # 40 | # needs_sphinx = '1.0' 41 | 42 | # Add any Sphinx extension module names here, as strings. They can be 43 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 44 | # ones. 45 | extensions = [ 46 | 'sphinx.ext.autosummary', 47 | 'sphinx.ext.autodoc', 48 | 'sphinx.ext.mathjax', 49 | 'sphinx.ext.viewcode', 50 | 'sphinx.ext.napoleon', 51 | 'sphinx.ext.intersphinx', 52 | 'sphinx.ext.extlinks', 53 | 'nbsphinx', 54 | 'IPython.sphinxext.ipython_directive', 55 | ] 56 | 57 | autosummary_generate = True 58 | napoleon_google_docstring = False 59 | napoleon_use_param = False 60 | napoleon_use_ivar = True 61 | 62 | # Add any paths that contain templates here, relative to this directory. 63 | templates_path = ['_templates'] 64 | 65 | # The suffix(es) of source filenames. 66 | # You can specify multiple suffix as a list of string: 67 | # 68 | # source_suffix = ['.rst', '.md'] 69 | source_suffix = '.rst' 70 | 71 | # The master toctree document. 72 | master_doc = 'index' 73 | 74 | # The language for content autogenerated by Sphinx. Refer to documentation 75 | # for a list of supported languages. 76 | # 77 | # This is also used if you do content translation via gettext catalogs. 78 | # Usually you set "language" from the command line for these cases. 79 | language = None 80 | 81 | # List of patterns, relative to source directory, that match files and 82 | # directories to ignore when looking for source files. 83 | # This pattern also affects html_static_path and html_extra_path . 84 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 85 | 86 | # The name of the Pygments (syntax highlighting) style to use. 87 | pygments_style = 'default' 88 | 89 | 90 | # -- Options for HTML output ------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | # 95 | html_theme = 'sphinx_rtd_theme' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | # 101 | # html_theme_options = {} 102 | 103 | # Add any paths that contain custom static files (such as style sheets) here, 104 | # relative to this directory. They are copied after the builtin static files, 105 | # so a file named "default.css" will overwrite the builtin "default.css". 106 | html_static_path = ['_static'] 107 | 108 | # Custom sidebar templates, must be a dictionary that maps document names 109 | # to template names. 110 | # 111 | # The default sidebars (for documents that don't match any pattern) are 112 | # defined by theme itself. Builtin themes are using these templates by 113 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 114 | # 'searchbox.html']``. 115 | # 116 | # html_sidebars = {} 117 | 118 | 119 | # -- Options for HTMLHelp output --------------------------------------------- 120 | 121 | # Output file base name for HTML help builder. 122 | htmlhelp_basename = 'solvation_analysisdoc' 123 | 124 | 125 | # -- Options for LaTeX output ------------------------------------------------ 126 | 127 | latex_elements = { 128 | # The paper size ('letterpaper' or 'a4paper'). 129 | # 130 | # 'papersize': 'letterpaper', 131 | 132 | # The font size ('10pt', '11pt' or '12pt'). 133 | # 134 | # 'pointsize': '10pt', 135 | 136 | # Additional stuff for the LaTeX preamble. 137 | # 138 | # 'preamble': '', 139 | 140 | # Latex figure (float) alignment 141 | # 142 | # 'figure_align': 'htbp', 143 | } 144 | 145 | # Grouping the document tree into LaTeX files. List of tuples 146 | # (source start file, target name, title, 147 | # author, documentclass [howto, manual, or own class]). 148 | latex_documents = [ 149 | (master_doc, 'solvation_analysis.tex', 'SolvationAnalysis Documentation', 150 | 'solvation_analysis', 'manual'), 151 | ] 152 | 153 | 154 | # -- Options for manual page output ------------------------------------------ 155 | 156 | # One entry per manual page. List of tuples 157 | # (source start file, name, description, authors, manual section). 158 | man_pages = [ 159 | (master_doc, 'solvation_analysis', 'SolvationAnalysis Documentation', 160 | [author], 1) 161 | ] 162 | 163 | 164 | # -- Options for Texinfo output ---------------------------------------------- 165 | 166 | # Grouping the document tree into Texinfo files. List of tuples 167 | # (source start file, target name, title, author, 168 | # dir menu entry, description, category) 169 | texinfo_documents = [ 170 | (master_doc, 'solvation_analysis', 'SolvationAnalysis Documentation', 171 | author, 'solvation_analysis', 'An MDAnalysis rmodule for solvation analysis.', 172 | 'Miscellaneous'), 173 | ] 174 | 175 | # -- Extension configuration ------------------------------------------------- 176 | 177 | autodoc_class_signature = "separated" 178 | autodoc_member_order = 'bysource' 179 | -------------------------------------------------------------------------------- /docs/getting_started.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "collapsed": true 7 | }, 8 | "source": [ 9 | "## Getting Started\n", 10 | "\n", 11 | "Solvation analysis makes it easy to rapidly understand the solvation structure of a solution. Solvation analysis is powered by [MDAnalysis](https://www.mdanalysis.org/), it's recommended that you understand MDAnalysis [universes](https://userguide.mdanalysis.org/stable/universe.html), [atom groups](https://userguide.mdanalysis.org/stable/atomgroup.html), and [atom selection language](https://userguide.mdanalysis.org/stable/selections.html) before getting started. After that, move on to the tutorials, these explain the basic usage of solvation-analysis and its core features. If you prefer learning-by-doing, all tutorials are available as Jupyter Notebooks on [GitHub](https://github.com/MDAnalysis/solvation-analysis/tree/main/docs/tutorials).\n", 12 | "\n", 13 | "Tutorials:\n", 14 | "\n", 15 | "- The Basics: create and analyze a `Solute`\n", 16 | "- Multi Atom Solutes: generalize to solutes with many atoms\n", 17 | "- Visualization: use `nglview` to visualize structures\n", 18 | "- Residence and Networking: calculate residence times and solute-solvent networks\n", 19 | "- Plotting and Comparing: generate illustrative plots of solvation properties\n", 20 | "- RDF Fitting: See how solvation-analysis finds solvation cutoffs\n", 21 | "\n", 22 | "For a full catalog of the properties calculated, read through the API documentation. Solvation-analysis is a powerful tool that calculates a wide range of properties, but it will take some time to master. If you ever have any questions, or encounter any bugs, please raise an issue on [GitHub](https://github.com/MDAnalysis/solvation-analysis).\n" 23 | ] 24 | } 25 | ], 26 | "metadata": { 27 | "kernelspec": { 28 | "name": "solvation_analysis", 29 | "language": "python", 30 | "display_name": "solvation_analysis" 31 | }, 32 | "language_info": { 33 | "codemirror_mode": { 34 | "name": "ipython", 35 | "version": 2 36 | }, 37 | "file_extension": ".py", 38 | "mimetype": "text/x-python", 39 | "name": "python", 40 | "nbconvert_exporter": "python", 41 | "pygments_lexer": "ipython2", 42 | "version": "2.7.6" 43 | } 44 | }, 45 | "nbformat": 4, 46 | "nbformat_minor": 0 47 | } 48 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. solvation_analysis documentation master file, created by 2 | sphinx-quickstart on Thu Mar 15 13:55:56 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to SolvationAnalysis's documentation! 7 | ========================================================= 8 | 9 | Solvation analysis implements a robust, cohesive, and fast set of methods for 10 | analyzing the solvation structure of a liquid. It seamlessly integrates with 11 | `MDAnalysis `_, making use of the core AtomGroup 12 | and Universe data structures to parse solvation information. If you are interested 13 | in understanding the solvation structure of a liquid, this package is for you! 14 | 15 | To get started, check out the Getting Started section of the documentation. 16 | 17 | 18 | .. toctree:: 19 | :maxdepth: 2 20 | :caption: Contents: 21 | 22 | getting_started.ipynb 23 | tutorials 24 | api 25 | 26 | 27 | 28 | Indices and tables 29 | ================== 30 | 31 | * :ref:`genindex` 32 | * :ref:`modindex` 33 | * :ref:`search` 34 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=solvation_analysis 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/requirements.yaml: -------------------------------------------------------------------------------- 1 | name: docs 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | # Base depends 7 | - python 8 | - pip 9 | - numpy 10 | - pytest 11 | - mdanalysis 12 | - nglview 13 | - nbsphinx 14 | - ipython 15 | - plotly 16 | - sphinx_rtd_theme 17 | - scipy==1.12.0 18 | 19 | 20 | # Pip-only installs 21 | #- pip: 22 | -------------------------------------------------------------------------------- /docs/tutorials.rst: -------------------------------------------------------------------------------- 1 | Tutorials 2 | ========= 3 | 4 | .. autosummary:: 5 | :toctree: stubs 6 | 7 | .. toctree:: 8 | 9 | tutorials/basics_tutorial.ipynb 10 | tutorials/multi_atom_solutes.ipynb 11 | tutorials/visualization_tutorial.ipynb 12 | tutorials/clustering_and_residence_tutorial.ipynb 13 | tutorials/plotting_tutorial.ipynb 14 | tutorials/rdf_fitting_demo.ipynb 15 | -------------------------------------------------------------------------------- /docs/tutorials/images/all_atoms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/docs/tutorials/images/all_atoms.png -------------------------------------------------------------------------------- /docs/tutorials/images/coordination_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/docs/tutorials/images/coordination_plot.png -------------------------------------------------------------------------------- /docs/tutorials/images/network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/docs/tutorials/images/network.png -------------------------------------------------------------------------------- /docs/tutorials/images/rdf_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/docs/tutorials/images/rdf_plot.png -------------------------------------------------------------------------------- /docs/tutorials/images/shell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/docs/tutorials/images/shell.png -------------------------------------------------------------------------------- /docs/tutorials/images/shell_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/docs/tutorials/images/shell_5.png -------------------------------------------------------------------------------- /docs/tutorials/images/speciation_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/docs/tutorials/images/speciation_plot.png -------------------------------------------------------------------------------- /docs/tutorials/images/summary_figure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/docs/tutorials/images/summary_figure.png -------------------------------------------------------------------------------- /docs/tutorials/setup_eax_solutes.py: -------------------------------------------------------------------------------- 1 | import MDAnalysis as mda 2 | from MDAnalysis import transformations 3 | import pathlib 4 | import numpy as np 5 | from solvation_analysis.tests.datafiles import eax_data 6 | 7 | boxes = { 8 | 'ea': [45.760393, 45.760393, 45.760393, 90, 90, 90], 9 | 'eaf': [47.844380, 47.844380, 47.844380, 90, 90, 90], 10 | 'fea': [48.358954, 48.358954, 48.358954, 90, 90, 90], 11 | 'feaf': [50.023129, 50.023129, 50.023129, 90, 90, 90], 12 | } 13 | us = {} 14 | # iterate through all the paths 15 | for solvent_dir in pathlib.Path(eax_data).iterdir(): 16 | u_solv = mda.Universe( 17 | str(solvent_dir / 'topology.pdb'), 18 | str(solvent_dir / 'trajectory_equil.dcd') 19 | ) 20 | # our dcd lacks dimensions so we must manually set them 21 | box = boxes[solvent_dir.stem] 22 | set_dim = transformations.boxdimensions.set_dimensions(box) 23 | u_solv.trajectory.add_transformations(set_dim) 24 | us[solvent_dir.stem] = u_solv 25 | 26 | reordered_us = {name: us[name] for name in ['ea', 'eaf', 'fea', 'feaf']} 27 | 28 | u_eax_atom_groups = {} 29 | for name, u in reordered_us.items(): 30 | atom_groups = {} 31 | atom_groups['li'] = u.atoms.select_atoms("element Li") 32 | atom_groups['pf6'] = u.atoms.select_atoms("byres element P") 33 | # this finds the boundary between fec and eax and selects both groups 34 | residue_lengths = np.array([len(elements) for elements in u.residues.elements]) 35 | eax_fec_cutoff = np.unique(residue_lengths, return_index=True)[1][2] 36 | atom_groups[name] = u.atoms.select_atoms(f"resid 1:{eax_fec_cutoff}") 37 | atom_groups['fec'] = u.atoms.select_atoms(f"resid {eax_fec_cutoff + 1}:600") 38 | u_eax_atom_groups[name] = atom_groups 39 | -------------------------------------------------------------------------------- /joss_paper/paper.bib: -------------------------------------------------------------------------------- 1 | @article{Michaud-Agrawal:2014, 2 | title = {MDAnalysis: A toolkit for the analysis of molecular dynamics simulations}, 3 | author = {Michaud‐Agrawal, Naveen and Denning, Elizabeth J. and Woolf, Thomas B. and Beckstein, Oliver}, 4 | year = 2011, 5 | month = 04, 6 | journal = {Journal of Computational Chemistry}, 7 | issn = {0192-8651}, 8 | pmid = {21500218}, 9 | pmcid = {PMC3144279}, 10 | pages = {2319 - 2327}, 11 | number = {10}, 12 | volume = {32}, 13 | doi = {10.1002/jcc.21787}, 14 | } 15 | 16 | @InProceedings{Gowers:2016, 17 | author = { Gowers, Richard and Linke, Max and Barnoud, Jonathan and Reddy, 18 | Tyler and Melo, Manuel and Seyler, Sean and Domański, Jan and Dotson, 19 | David and Buchoux, Sébastien and Kenney, Ian and Beckstein, Oliver }, 20 | title = { MDAnalysis: A Python Package for the Rapid Analysis of Molecular Dynamics Simulations }, 21 | url = {https://conference.scipy.org/proceedings/scipy2016/oliver\_beckstein.html}, 22 | pages = {98 - 105}, 23 | year = 2016, 24 | doi = {10.25080/majora-629e541a-00e}, 25 | } 26 | 27 | @online{plotly:2015, 28 | author = {Plotly}, 29 | title = {Collaborative data science}, 30 | publisher = {Plotly Technologies Inc.}, 31 | address = {Montreal, QC}, 32 | year = 2015, 33 | url = {https://plot.ly}, 34 | } 35 | 36 | @article{scipy:2020, 37 | author = {Virtanen, Pauli and Gommers, Ralf and Oliphant, Travis E. and 38 | Haberland, Matt and Reddy, Tyler and Cournapeau, David and 39 | Burovski, Evgeni and Peterson, Pearu and Weckesser, Warren and 40 | Bright, Jonathan and {van der Walt}, St{\'e}fan J. and 41 | Brett, Matthew and Wilson, Joshua and Millman, K. Jarrod and 42 | Mayorov, Nikolay and Nelson, Andrew R. J. and Jones, Eric and 43 | Kern, Robert and Larson, Eric and Carey, C J and 44 | Polat, {\.I}lhan and Feng, Yu and Moore, Eric W. and 45 | {VanderPlas}, Jake and Laxalde, Denis and Perktold, Josef and 46 | Cimrman, Robert and Henriksen, Ian and Quintero, E. A. and 47 | Harris, Charles R. and Archibald, Anne M. and 48 | Ribeiro, Ant{\^o}nio H. and Pedregosa, Fabian and 49 | {van Mulbregt}, Paul and {SciPy 1.0 Contributors}}, 50 | title = {{SciPy} 1.0: Fundamental Algorithms for Scientific 51 | Computing in Python}, 52 | journal = {Nature Methods}, 53 | year = {2020}, 54 | volume = {17}, 55 | pages = {261--272}, 56 | adsurl = {https://rdcu.be/b08Wh}, 57 | doi = {10.1038/s41592-019-0686-2}, 58 | } 59 | 60 | @article{matplotlib:2007, 61 | Author = {Hunter, J. D.}, 62 | Title = {Matplotlib: A 2D graphics environment}, 63 | Journal = {Computing in Science \& Engineering}, 64 | Volume = {9}, 65 | Number = {3}, 66 | Pages = {90--95}, 67 | abstract = {Matplotlib is a 2D graphics package used for Python for 68 | application development, interactive scripting, and publication-quality 69 | image generation across user interfaces and operating systems.}, 70 | publisher = {IEEE COMPUTER SOC}, 71 | doi = {10.1109/MCSE.2007.55}, 72 | year = 2007, 73 | } 74 | 75 | @article{numpy:2020, 76 | author = {Harris, Charles R. and Millman, K. Jarrod and van der Walt, 77 | Stéfan J and Gommers, Ralf and Virtanen, Pauli and Cournapeau, David 78 | and Wieser, Eric and Taylor, Julian and Berg, Sebastian and Smith, 79 | Nathaniel J. and Kern, Robert and Picus, Matti and Hoyer, Stephan and van 80 | Kerkwijk, Marten H. and Brett, Matthew and Haldane, Allan and Fernández 81 | del Río, Jaime and Wiebe, Mark and Peterson, Pearu and Gérard-Marchant, 82 | Pierre and Sheppard, Kevin and Reddy, Tyler and Weckesser, Warren 83 | and Abbasi, Hameer and Gohlke, Christoph and Oliphant, Travis E.}, 84 | title = {Array programming with {NumPy}}, 85 | journal = {Nature}, 86 | year = {2020}, 87 | volume = {585}, 88 | number = {7825}, 89 | pages = {357--362}, 90 | pages = {357–362}, 91 | doi = {10.1038/s41586-020-2649-2}, 92 | } 93 | 94 | @software{pandas:2020, 95 | author = {DevTeam}, 96 | title = {pandas-dev/pandas: Pandas}, 97 | month = 02, 98 | year = 2020, 99 | publisher = {Zenodo}, 100 | version = {latest}, 101 | doi = {10.5281/zenodo.3509134}, 102 | url = {https://doi.org/10.5281/zenodo.3509134}, 103 | } 104 | 105 | @article{nglview:2018, 106 | author = {Nguyen, Hai and Case, David A and Rose, Alexander S}, 107 | title = "{NGLview–interactive molecular graphics for Jupyter notebooks}", 108 | journal = {Bioinformatics}, 109 | volume = {34}, 110 | number = {7}, 111 | pages = {1241-1242}, 112 | year = {2017}, 113 | month = {12}, 114 | issn = {1367-4803}, 115 | doi = {10.1093/bioinformatics/btx789}, 116 | url = {https://doi.org/10.1093/bioinformatics/btx789}, 117 | eprint = {https://academic.oup.com/bioinformatics/article-pdf/34/7/1241/48914829/bioinformatics\_34\_7\_1241.pdf}, 118 | } 119 | 120 | 121 | 122 | @article{Hou:2019, 123 | title = {The influence of FEC on the solvation structure and reduction reaction of LiPF6/EC electrolytes and its implication for solid electrolyte interphase formation}, 124 | author = {Hou, Tingzheng and Yang, Guang and Rajput, Nav Nidhi and Self, Julian and Park, Sang-Won and Nanda, Jagjit and Persson, Kristin A.}, 125 | journal = {Nano Energy}, 126 | issn = {22112855}, 127 | url = {https://linkinghub.elsevier.com/retrieve/pii/S2211285519305877}, 128 | year = {2019}, 129 | pages = {103881}, 130 | volume = {64}, 131 | month = {10}, 132 | doi = {10.1016/j.nanoen.2019.103881}, 133 | } 134 | 135 | 136 | @article{Dong-Joo:2022, 137 | author = {Yoo, Dong-Joo and Liu, Qian and Cohen, Orion and Kim, Minkyu and Persson, Kristin A. and Zhang, Zhengcheng}, 138 | title = {Understanding the Role of SEI Layer in Low-Temperature Performance of Lithium-Ion Batteries}, 139 | journal = {ACS Applied Materials \& Interfaces}, 140 | volume = {14}, 141 | number = {9}, 142 | pages = {11910-11918}, 143 | year = {2022}, 144 | doi = {10.1021/acsami.1c23934}, 145 | } 146 | 147 | @article{Self:2019, 148 | year = {2019}, 149 | title = {Transport in Superconcentrated LiPF6 and LiBF4/Propylene Carbonate Electrolytes}, 150 | author = {Self, Julian and Fong, Kara D. and Persson, Kristin A.}, 151 | journal = {ACS Energy Letters}, 152 | issn = {2380-8195}, 153 | doi = {10.1021/acsenergylett.9b02118}, 154 | pages = {2843--2849}, 155 | number = {12}, 156 | volume = {4}, 157 | } 158 | 159 | 160 | @article{Xie:2023, 161 | year = {2023}, 162 | title = {{Spatially resolved structural order in low-temperature liquid electrolyte}}, 163 | author = {Xie, Yujun and Wang, Jingyang and Savitzky, Benjamin H. and Chen, Zheng and Wang, Yu and Betzler, Sophia and Bustillo, Karen and Persson, Kristin and Cui, Yi and Wang, Lin-Wang and Ophus, Colin and Ercius, Peter and Zheng, Haimei}, 164 | journal = {Science Advances}, 165 | doi = {10.1126/sciadv.adc9721}, 166 | pmid = {36638171}, 167 | pages = {eadc9721}, 168 | number = {2}, 169 | volume = {9} 170 | } -------------------------------------------------------------------------------- /joss_paper/paper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'SolvationAnalysis: A Python toolkit for understanding liquid solvation structure in classical molecular dynamics simulations' 3 | tags: 4 | - python 5 | - chemistry 6 | - electrolytes 7 | - molecular dynamics 8 | - solvation structure 9 | authors: 10 | - name: Orion Archer Cohen 11 | orcid: 0000-0003-3940-2456 12 | affiliation: 1 13 | - name: Hugo Macdermott-Opeskin 14 | orcid: 0000-0002-7393-7457 15 | affiliation: 2 16 | - name: Lauren Lee 17 | affiliation: 1 18 | - name: Tingzheng Hou 19 | orcid: 0000-0002-7163-2561 20 | affiliation: 3 21 | - name: Kara D. Fong 22 | orcid: 0000-0002-0711-097X 23 | affiliation: 1 24 | - name: Ryan Kingsbury 25 | orcid: 0000-0002-7168-3967 26 | affiliation: 4 27 | - name: Jingyang Wang 28 | orcid: 0000-0003-3307-5132 29 | affiliation: 1 30 | - name: Kristin A. Persson 31 | orcid: 0000-0003-2495-5509 32 | affiliation: "5,6" 33 | affiliations: 34 | - name: Materials Science Division, Lawrence Berkeley National Laboratory, United States of America 35 | index: 1 36 | - name: Australian National University, Australia 37 | index: 2 38 | - name: Institute of Materials Research, Shenzhen International Graduate School, Tsinghua University, China 39 | index: 3 40 | - name: Energy Storage & Distributed Resources Division, Lawrence Berkeley National Laboratory, United States of America 41 | index: 4 42 | - name: Department of Materials Science, University of California, United States of America 43 | index: 5 44 | - name: Molecular Foundry, Lawrence Berkeley National Laboratory, United States of America 45 | index: 6 46 | date: 12 January 2023 47 | bibliography: paper.bib 48 | --- 49 | 50 | # Summary 51 | 52 | The macroscopic behavior of matter is determined by the microscopic 53 | arrangement of atoms, but this arrangement is often 54 | difficult or impossible to observe experimentally. Instead, researchers use 55 | simulation techniques like molecular dynamics to probe the microscopic 56 | structure and dynamics of everything from proteins to battery electrolytes. 57 | SolvationAnalysis extracts solvation information from completed 58 | molecular dynamics simulations, letting researchers access key solvation 59 | structure statistics with minimal effort and accelerating scientific research. 60 | 61 | # Statement of need 62 | 63 | Molecular dynamics studies of liquid solvation structures often replicate 64 | established analyses on novel systems. In electrolyte systems, it is common 65 | to calculate coordination numbers, radial distribution functions, solute 66 | dissociation, and cluster speciation [@Hou:2019]. In principle, these analyses are highly 67 | similar across a diversity of systems. In practice, many specialized bespoke 68 | tools have sprung up to address the same underlying problem. Enter `SolvationAnalysis`, 69 | an easy-to-use Python package with an interactive interface for 70 | computing a wide variety of solvation properties. Building on `MDAnalysis` and 71 | `pandas` [@Michaud-Agrawal:2014] [@Gowers:2016] [@pandas:2020], it efficiently 72 | processes output from a wide variety of Molecular Dynamics simulation packages. 73 | 74 | `SolvationAnalysis` was designed to free researchers from laboriously 75 | implementing and validating common analyses. In addition to routine properties like 76 | coordination numbers, solute-solvent pairing, and solute speciation, 77 | SolvationAnalysis uses tools from the SciPy ecosystem [@numpy:2020] [@scipy:2020] 78 | to implement analyses of network formation [@Xie:2023] and residence 79 | times [@Self:2019], summarized in \autoref{fig:summary}. To make visualization fast, 80 | the package includes a robust set of plotting tools built on top of `Matplotlib` and 81 | `Plotly` [@matplotlib:2007] [@plotly:2015]. Paired with nglview [@nglview:2018], both 82 | exploration and 3d visualization can be done in a Jupyter notebook. 83 | A full set of tutorials based on state-of-the-art battery electrolytes 84 | [@Hou:2019] [@Dong-Joo:2022] are also included to familiarize new researchers 85 | with solvation structure analysis. Together, these features allow for 86 | rapid interactive or programmatic calculation of solvation properties. 87 | 88 | # Figures 89 | 90 | ![A visual summary of SolvationAnalysis capabilities. \label{fig:summary}](summary_figure.jpg) 91 | 92 | # Acknowledgements 93 | 94 | Thank you to Oliver Beckstein, Richard Gowers, Irfan Alibay, and Lily Wang for 95 | technical advice about MDAnalysis and Python development. Thank you to Google 96 | Summer of Code, the NSF GRFP Fellowship, and the US Department of Energy for 97 | funding. 98 | 99 | # References -------------------------------------------------------------------------------- /joss_paper/summary_figure.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/joss_paper/summary_figure.jpg -------------------------------------------------------------------------------- /meta.yaml: -------------------------------------------------------------------------------- 1 | {% set name = "solvation_analysis" %} 2 | {% set version = "0.3.1" %} 3 | 4 | package: 5 | name: {{ name|lower }} 6 | version: {{ version }} 7 | 8 | source: 9 | url: https://pypi.io/packages/source/{{ name[0] }}/{{ name }}/solvation-analysis-{{ version }}.tar.gz 10 | sha256: 18503c20d12d745da33d5da5f06f4a2f0482dbbe55364ad34ca3eec416cb2e71 11 | 12 | build: 13 | noarch: python 14 | script: {{ PYTHON }} -m pip install . -vv 15 | number: 0 16 | 17 | requirements: 18 | build: 19 | - versioneer-518 20 | host: 21 | - python >=3.7 22 | - pip 23 | - pathlib 24 | - pytest 25 | - rdkit 26 | - versioneer-518 27 | run: 28 | - python >=3.7 29 | - numpy >=1.16.0 30 | - mdanalysis >=2.0.0 31 | - pandas 32 | - matplotlib-base 33 | - scipy 34 | - plotly 35 | - statsmodels 36 | - rdkit 37 | 38 | test: 39 | imports: 40 | - solvation_analysis 41 | commands: 42 | - pip check 43 | requires: 44 | - pip 45 | 46 | about: 47 | home: https://pypi.org/project/solvation-analysis/ 48 | summary: 'Rapidly understand solvation with MDAnalysis.' 49 | description: | 50 | Solvation analysis implements a robust, cohesive, and fast set of 51 | methods for analyzing the solvation structure of a liquid. It seamlessly 52 | integrates with MDAnalysis, making use of the core AtomGroup and Universe 53 | data structures to parse solvation information. If you are interested in 54 | understanding the solvation structure of a liquid, this package is for you! 55 | license: GPL-3.0-only 56 | license_file: LICENSE 57 | 58 | extra: 59 | recipe-maintainers: 60 | - orionarcher 61 | - hmacdope -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools", 4 | "versioneer[toml]", 5 | "wheel" 6 | ] 7 | build-backend = "setuptools.build_meta" 8 | 9 | [project] 10 | name = "solvation-analysis" 11 | dynamic = ['version', 'readme'] 12 | license = {file = "LICENSE"} 13 | description = "A toolkit to analyze solvation structure in molecular dynamics trajectories." 14 | authors = [ 15 | {name = 'Orion Cohen', email = 'orioncohen@berkeley.edu'} 16 | ] 17 | maintainers = [ 18 | {name = 'Orion Cohen', email = 'orioncohen@berkeley.edu'}, 19 | {name = 'Hugo MacDermott-Opeskin', email = 'hugomacdermott@gmail.com'} 20 | 21 | ] 22 | requires-python = ">=3.8" 23 | 24 | keywords = [ 25 | "python", "science", "chemistry", "biophysics", "molecular-dynamics", 26 | "computational-chemistry", "molecular-simulation", "analysis", 27 | "trajectory-analysis", "solvation" 28 | ] 29 | 30 | dependencies = [ 31 | 'numpy>=1.20.0', 32 | 'pandas>=2.2', 33 | 'mdanalysis>=2.0.0', 34 | 'pytest', 35 | 'matplotlib', 36 | 'setuptools', 37 | 'scipy', 38 | 'statsmodels', 39 | 'plotly', 40 | 'rdkit' 41 | ] 42 | 43 | classifiers = [ 44 | 'Development Status :: 3 - Alpha', 45 | 'Environment :: Console', 46 | 'Intended Audience :: Science/Research', 47 | 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)', 48 | 'Operating System :: POSIX', 49 | 'Operating System :: MacOS :: MacOS X', 50 | 'Operating System :: Microsoft :: Windows', 51 | 'Programming Language :: Python', 52 | 'Programming Language :: Python :: 3.8', 53 | 'Programming Language :: Python :: 3.9', 54 | 'Programming Language :: Python :: 3.10', 55 | 'Programming Language :: Python :: 3.11', 56 | 'Topic :: Scientific/Engineering', 57 | 'Topic :: Scientific/Engineering :: Bio-Informatics', 58 | 'Topic :: Scientific/Engineering :: Chemistry', 59 | 'Topic :: Software Development :: Libraries :: Python Modules', 60 | ] 61 | 62 | 63 | [project.urls] 64 | Documentation = 'https://solvation-analysis.readthedocs.io' 65 | "Issue Tracker" = 'https://github.com/MDAnalysis/solvation-analysis/issues' 66 | Source = 'https://github.com/MDAnalysis/solvation-analysis' 67 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | # readthedocs.yml 2 | 3 | version: 2 4 | 5 | build: 6 | os: ubuntu-22.04 7 | tools: 8 | python: "mambaforge-22.9" 9 | 10 | conda: 11 | environment: docs/requirements.yaml 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools 2 | numpy>=1.20.0 3 | pandas>=2.2 4 | mdanalysis>=2.7.0 5 | pytest 6 | pathlib 7 | matplotlib 8 | scipy 9 | statsmodels 10 | plotly 11 | rdkit 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # Helper file to handle all configs 2 | 3 | [coverage:run] 4 | # .coveragerc to control coverage.py and pytest-cov 5 | omit = 6 | # Omit the tests 7 | */tests/* 8 | # Omit generated versioneer 9 | solvation_analysis/_version.py 10 | 11 | [yapf] 12 | # YAPF, in .style.yapf files this shows up as "[style]" header 13 | COLUMN_LIMIT = 119 14 | INDENT_WIDTH = 4 15 | USE_TABS = False 16 | 17 | [flake8] 18 | # Flake8, PyFlakes, etc 19 | max-line-length = 119 20 | 21 | [versioneer] 22 | # Automatic version numbering scheme 23 | VCS = git 24 | style = pep440 25 | versionfile_source = solvation_analysis/_version.py 26 | versionfile_build = solvation_analysis/_version.py 27 | tag_prefix = '' 28 | 29 | [aliases] 30 | test = pytest 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | SolvationAnalysis 3 | An MDAnalysis rmodule for solvation analysis. 4 | """ 5 | 6 | import sys 7 | from setuptools import setup, find_packages 8 | import versioneer 9 | 10 | short_description = __doc__.split("\n") 11 | 12 | # from https://github.com/pytest-dev/pytest-runner#conditional-requirement 13 | needs_pytest = {"pytest", "test", "ptr"}.intersection(sys.argv) 14 | pytest_runner = ["pytest-runner"] if needs_pytest else [] 15 | 16 | try: 17 | with open("README.md", "r") as handle: 18 | long_description = handle.read() 19 | except: # noqa 20 | long_description = "\n".join(short_description[2:]) 21 | 22 | 23 | setup( 24 | # Self-descriptive entries which should always be present 25 | name="solvation_analysis", 26 | author="Orion Cohen", 27 | author_email="orioncohen@gmail.com", 28 | description=short_description[0], 29 | long_description=long_description, 30 | long_description_content_type="text/markdown", 31 | version=versioneer.get_version(), 32 | cmdclass=versioneer.get_cmdclass(), 33 | license="GNU Public License v3", 34 | # Which Python importable modules should be included when your package is installed 35 | # Handled automatically by setuptools. Use 'exclude' to prevent some specific 36 | # subpackage(s) from being added, if needed 37 | packages=find_packages(), 38 | # Optional include package data to ship with your package 39 | # Customize MANIFEST.in if the general case does not suit your needs 40 | # Comment out this line to prevent the files from being packaged with your software 41 | include_package_data=True, 42 | # Allows `setup.py test` to work correctly with pytest 43 | setup_requires=[] + pytest_runner, 44 | install_requires=[ 45 | "numpy>=1.20.0", 46 | "mdanalysis>=2.7.0", 47 | "pandas", 48 | "matplotlib", 49 | "scipy==1.12.0", 50 | "statsmodels", 51 | "plotly", 52 | "rdkit", 53 | ], 54 | # Additional entries you may want simply uncomment the lines you want and fill in the data 55 | # url='http://www.my_package.com', # Website 56 | # install_requires=[], # Required packages, pulls from pip if needed; do not use for Conda deployment 57 | # platforms=['Linux', 58 | # 'Mac OS-X', 59 | # 'Unix', 60 | # 'Windows'], # Valid platforms your code works on, adjust to your flavor 61 | # python_requires=">=3.5", # Python version restrictions 62 | # Manual control if final package is compressible or not, set False to prevent the .egg from being made 63 | # zip_safe=False, 64 | ) 65 | -------------------------------------------------------------------------------- /solvation_analysis/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | SolvationAnalysis 3 | An MDAnalysis rmodule for solvation analysis. 4 | """ 5 | 6 | from . import _version 7 | from solvation_analysis.solute import Solute 8 | 9 | # Handle versioneer 10 | from ._version import get_versions 11 | 12 | versions = get_versions() 13 | __version__ = versions["version"] 14 | __git_revision__ = versions["full-revisionid"] 15 | del get_versions, versions 16 | 17 | 18 | __version__ = _version.get_versions()["version"] 19 | __all__ = ["Solute"] 20 | -------------------------------------------------------------------------------- /solvation_analysis/_column_names.py: -------------------------------------------------------------------------------- 1 | """ 2 | To change the string name a variable: 3 | 1. change the string name in this file 4 | 2. search through the documentation and change the string name wherever it appears 5 | 3. change the name in the bn_solve_df_large dataframe 6 | 7 | To change the variable name of a variable: 8 | 1. change the variable name in all files 9 | """ 10 | 11 | # for solvation_data 12 | FRAME = "frame" 13 | SOLUTE_IX = "solute_ix" 14 | SOLUTE_ATOM_IX = "solute_atom_ix" 15 | SOLVENT_ATOM_IX = "solvent_atom_ix" 16 | DISTANCE = "distance" 17 | SOLUTE = "solute" 18 | SOLVENT = "solvent" 19 | SOLVENT_IX = "solvent_ix" 20 | 21 | # for coordination 22 | ATOM_TYPE = "atom_type" 23 | FRACTION = "fraction" 24 | 25 | # for networking 26 | NETWORK = "network" 27 | ISOLATED = "isolated" 28 | PAIRED = "paired" 29 | NETWORKED = "networked" 30 | 31 | # for speciation 32 | COUNT = "fraction" 33 | -------------------------------------------------------------------------------- /solvation_analysis/_utils.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from functools import reduce 3 | from typing import Union 4 | 5 | import numpy as np 6 | import pandas as pd 7 | import MDAnalysis as mda 8 | from MDAnalysis.analysis import distances 9 | 10 | from solvation_analysis._column_names import FRAME, SOLUTE_IX, SOLVENT_IX, DISTANCE 11 | 12 | 13 | def verify_solute_atoms(solute_atom_group: mda.AtomGroup) -> dict[int, mda.AtomGroup]: 14 | # we presume that the solute_atoms has the same number of atoms on each residue 15 | # and that they all have the same indices on those residues 16 | # and that the residues are all the same length 17 | # then this should work 18 | all_res_len = np.array([res.atoms.n_atoms for res in solute_atom_group.residues]) 19 | assert np.all( 20 | all_res_len[0] == all_res_len 21 | ), "All residues must be the same length." 22 | res_atom_local_ix = defaultdict(list) 23 | res_atom_ix = defaultdict(list) 24 | 25 | for atom in solute_atom_group.atoms: 26 | res_atom_local_ix[atom.resindex].append(atom.ix - atom.residue.atoms[0].ix) 27 | res_atom_ix[atom.resindex].append(atom.index) 28 | res_occupancy = np.array([len(ix) for ix in res_atom_local_ix.values()]) 29 | assert np.all( 30 | res_occupancy[0] == res_occupancy 31 | ), "All residues must have the same number of solute_atoms atoms on them." 32 | 33 | res_atom_array = np.array(list(res_atom_local_ix.values())) 34 | assert np.all( 35 | res_atom_array[0] == res_atom_array 36 | ), "All residues must have the same solute_atoms atoms on them." 37 | 38 | res_atom_ix_array = np.array(list(res_atom_ix.values())) 39 | solute_atom_group_dict = {} 40 | for i in range(0, res_atom_ix_array.shape[1]): 41 | solute_atom_group_dict[i] = solute_atom_group.universe.atoms[ 42 | res_atom_ix_array[:, i] 43 | ] 44 | return solute_atom_group_dict 45 | 46 | 47 | def verify_solute_atoms_dict( 48 | solute_atoms_dict: dict[str, mda.AtomGroup], 49 | ) -> mda.AtomGroup: 50 | # first we verify the input format 51 | atom_group_lengths = [] 52 | for solute_name, solute_atom_group in solute_atoms_dict.items(): 53 | assert isinstance(solute_name, str), "The keys of solutes_dict must be strings." 54 | assert isinstance(solute_atom_group, mda.AtomGroup), ( 55 | f"The values of solutes_dict must be MDAnalysis.AtomGroups. But the value" 56 | f"for {solute_name} is a {type(solute_atom_group)}." 57 | ) 58 | assert len(solute_atom_group) == len( 59 | solute_atom_group.residues 60 | ), "The solute_atom_group must have a single atom on each residue." 61 | atom_group_lengths.append(len(solute_atom_group)) 62 | assert np.all(np.array(atom_group_lengths) == atom_group_lengths[0]), ( 63 | "AtomGroups in solutes_dict must have the same length because there should be" 64 | "one atom per solute residue." 65 | ) 66 | 67 | # verify that the solute_atom_groups have no overlap 68 | solute_atom_group = reduce( 69 | lambda x, y: x | y, [atoms for atoms in solute_atoms_dict.values()] 70 | ) 71 | assert solute_atom_group.n_atoms == sum( 72 | [atoms.n_atoms for atoms in solute_atoms_dict.values()] 73 | ), "The solute_atom_groups must not overlap." 74 | verify_solute_atoms(solute_atom_group) 75 | 76 | return solute_atom_group 77 | 78 | 79 | def get_atom_group( 80 | selection: Union[ 81 | mda.core.groups.Residue, 82 | mda.core.groups.ResidueGroup, 83 | mda.core.groups.Atom, 84 | mda.core.groups.AtomGroup, 85 | ], 86 | ) -> mda.AtomGroup: 87 | """ 88 | Cast an MDAnalysis.Atom, MDAnalysis.Residue, or MDAnalysis.ResidueGroup to AtomGroup. 89 | 90 | Parameters 91 | ---------- 92 | selection: MDAnalysis.Atom, MDAnalysis.Residue or MDAnalysis.ResidueGroup 93 | atoms to cast 94 | 95 | Returns 96 | ------- 97 | MDAnalysis.AtomGroup 98 | 99 | """ 100 | assert isinstance( 101 | selection, 102 | ( 103 | mda.core.groups.Residue, 104 | mda.core.groups.ResidueGroup, 105 | mda.core.groups.Atom, 106 | mda.core.groups.AtomGroup, 107 | ), 108 | ), "central_species must be one of the preceding types" 109 | u = selection.universe 110 | if isinstance(selection, (mda.core.groups.Residue, mda.core.groups.ResidueGroup)): 111 | selection = selection.atoms 112 | if isinstance(selection, mda.core.groups.Atom): 113 | selection = u.select_atoms(f"index {selection.index}") 114 | return selection 115 | 116 | 117 | def get_closest_n_mol( 118 | central_species: Union[ 119 | mda.core.groups.Residue, 120 | mda.core.groups.ResidueGroup, 121 | mda.core.groups.Atom, 122 | mda.core.groups.AtomGroup, 123 | ], 124 | n_mol: int, 125 | guess_radius: Union[float, int] = 3, 126 | return_ordered_resix: bool = False, 127 | return_radii: bool = False, 128 | ) -> Union[ 129 | mda.AtomGroup, 130 | tuple[mda.AtomGroup, np.ndarray], 131 | tuple[mda.AtomGroup, np.ndarray, np.ndarray], 132 | ]: 133 | """ 134 | Returns the closest n molecules to the central species, an array of their resix, 135 | and an array of the distance of the closest atom in each molecule. 136 | 137 | Parameters 138 | ---------- 139 | central_species : MDAnalysis.Atom, MDAnalysis.AtomGroup, MDAnalysis.Residue or MDAnalysis.ResidueGroup 140 | n_mol : int 141 | The number of molecules to return 142 | guess_radius : float or int 143 | an initial search radius to look for closest n mol 144 | return_ordered_resix : bool, default False 145 | whether to return the resix of the closest n 146 | molecules, ordered by radius 147 | return_radii : bool, default False 148 | whether to return the distance of the closest atom of each 149 | of the n molecules 150 | 151 | Returns 152 | ------- 153 | full shell : MDAnalysis.AtomGroup 154 | the atoms in the shell 155 | ordered_resix : numpy.array of int 156 | the residue index of the n_mol closest atoms 157 | radii : numpy.array of float 158 | the distance of each atom from the center 159 | """ 160 | u = central_species.universe 161 | central_species = get_atom_group(central_species) 162 | coords = central_species.center_of_mass() 163 | pairs, radii = mda.lib.distances.capped_distance( 164 | coords, u.atoms.positions, guess_radius, return_distances=True, box=u.dimensions 165 | ) 166 | partial_shell = u.atoms[pairs[:, 1]] 167 | shell_resix = partial_shell.resindices 168 | if len(np.unique(shell_resix)) < n_mol + 1: 169 | return get_closest_n_mol( 170 | central_species, 171 | n_mol, 172 | guess_radius + 1, 173 | return_ordered_resix=return_ordered_resix, 174 | return_radii=return_radii, 175 | ) 176 | radii = distances.distance_array(coords, partial_shell.positions, box=u.dimensions)[ 177 | 0 178 | ] 179 | ordering = np.argsort(radii) 180 | ordered_resix = shell_resix[ordering] 181 | closest_n_resix = np.sort(np.unique(ordered_resix, return_index=True)[1])[ 182 | 0 : n_mol + 1 183 | ] 184 | str_resix = " ".join(str(resix) for resix in ordered_resix[closest_n_resix]) 185 | full_shell = u.select_atoms(f"resindex {str_resix}") 186 | if return_ordered_resix and return_radii: 187 | return ( 188 | full_shell, 189 | ordered_resix[closest_n_resix], 190 | radii[ordering][closest_n_resix], 191 | ) 192 | elif return_ordered_resix: 193 | return full_shell, ordered_resix[closest_n_resix] 194 | elif return_radii: 195 | return full_shell, radii[ordering][closest_n_resix] 196 | else: 197 | return full_shell 198 | 199 | 200 | def get_radial_shell( 201 | central_species: Union[ 202 | mda.core.groups.Residue, 203 | mda.core.groups.ResidueGroup, 204 | mda.core.groups.Atom, 205 | mda.core.groups.AtomGroup, 206 | ], 207 | radius: Union[float, int], 208 | ) -> mda.AtomGroup: 209 | """ 210 | Returns all molecules with atoms within the radius of the central species. 211 | (specifically, within the radius of the COM of central species). 212 | 213 | Parameters 214 | ---------- 215 | central_species : MDAnalysis.Atom, MDAnalysis.AtomGroup, MDAnalysis.Residue, or MDAnalysis.ResidueGroup 216 | radius : float or int 217 | radius used for atom selection 218 | 219 | Returns 220 | ------- 221 | MDAnalysis.AtomGroup 222 | 223 | """ 224 | u = central_species.universe 225 | central_species = get_atom_group(central_species) 226 | coords = central_species.center_of_mass() 227 | str_coords = " ".join(str(coord) for coord in coords) 228 | partial_shell = u.select_atoms(f"point {str_coords} {radius}") 229 | full_shell = partial_shell.residues.atoms 230 | return full_shell 231 | 232 | 233 | def calculate_adjacency_dataframe(solvation_data: pd.DataFrame) -> pd.DataFrame: 234 | """ 235 | Calculate a frame-by-frame adjacency matrix from the solvation data. 236 | 237 | This will calculate the adjacency matrix of the solute and all possible 238 | solvents. It will maintain an index of ["frame", "solute_atom", "solvent"] 239 | where each "frame" is a sparse adjacency matrix between solvated atom ix 240 | and residue ix. 241 | 242 | Parameters 243 | ---------- 244 | solvation_data : pd.DataFrame 245 | the solvation_data from a Solute. 246 | 247 | Returns 248 | ------- 249 | adjacency_df : pandas.DataFrame 250 | """ 251 | # generate an adjacency matrix from the solvation data 252 | adjacency_group = solvation_data.groupby([FRAME, SOLUTE_IX, SOLVENT_IX]) 253 | adjacency_df = adjacency_group[DISTANCE].count().unstack(fill_value=0) 254 | return adjacency_df 255 | -------------------------------------------------------------------------------- /solvation_analysis/coordination.py: -------------------------------------------------------------------------------- 1 | """ 2 | ================ 3 | Coordination 4 | ================ 5 | :Author: Orion Cohen, Tingzheng Hou, Kara Fong 6 | :Year: 2021 7 | :Copyright: GNU Public License v3 8 | 9 | Elucidate the coordination of each solvating species. 10 | 11 | Coordination numbers for each solvent are automatically calculated, along with 12 | the types of every coordinating atom. 13 | 14 | While ``coordination`` can be used in isolation, it is meant to be used 15 | as an attribute of the Solute class. This makes instantiating it and calculating the 16 | solvation data a non-issue. 17 | """ 18 | 19 | import pandas as pd 20 | import MDAnalysis as mda 21 | import solvation_analysis 22 | 23 | from solvation_analysis._column_names import ( 24 | FRAME, 25 | SOLUTE_IX, 26 | SOLVENT, 27 | SOLVENT_IX, 28 | SOLVENT_ATOM_IX, 29 | ATOM_TYPE, 30 | FRACTION, 31 | ) 32 | 33 | 34 | class Coordination: 35 | """ 36 | Calculate the coordination number for each solvent. 37 | 38 | Coordination calculates the coordination number by averaging the number of 39 | coordinated solvents in all of the solvation shells. This is equivalent to 40 | the typical method of integrating the solute-solvent RDF up to the solvation 41 | radius cutoff. As a result, Coordination calculates **species-species** coordination 42 | numbers, not the total coordination number of the solute. So if the coordination 43 | number of mol1 is 3.2, there are on average 3.2 mol1 residues within the solvation 44 | distance of each solute. 45 | 46 | The coordination numbers are made available as an mean over the whole 47 | simulation and by frame. 48 | 49 | Parameters 50 | ---------- 51 | solvation_data : pandas.DataFrame 52 | The solvation dataframe output by Solute. 53 | n_frames : int 54 | The number of frames in solvation_data. 55 | n_solutes : int 56 | The number of solutes in solvation_data. 57 | solvent_counts: Dict[str, int] 58 | A dictionary of the number of residues for each solvent. 59 | atom_group : MDAnalysis.core.groups.AtomGroup 60 | The atom group of all atoms in the universe. 61 | 62 | Examples 63 | -------- 64 | 65 | .. code-block:: python 66 | 67 | # first define Li, BN, and FEC AtomGroups 68 | >>> solute = Solute(Li, {'BN': BN, 'FEC': FEC, 'PF6': PF6}) 69 | >>> solute.run() 70 | >>> solute.coordination.coordination_numbers 71 | {'BN': 4.328, 'FEC': 0.253, 'PF6': 0.128} 72 | 73 | """ 74 | 75 | def __init__( 76 | self, 77 | solvation_data: pd.DataFrame, 78 | n_frames: int, 79 | n_solutes: int, 80 | solvent_counts: dict[str, int], 81 | atom_group: mda.core.groups.AtomGroup, 82 | ) -> None: 83 | self.solvation_data = solvation_data 84 | self.n_frames = n_frames 85 | self.n_solutes = n_solutes 86 | self._cn_dict, self._cn_dict_by_frame = self._mean_cn() 87 | self.atom_group = atom_group 88 | self._coordinating_atoms = self._calculate_coordinating_atoms() 89 | self.solvent_counts = solvent_counts 90 | self._coordination_vs_random = self._calculate_coordination_vs_random() 91 | 92 | @staticmethod 93 | def from_solute(solute: "solvation_analysis.Solute") -> "Coordination": 94 | """ 95 | Generate a Coordination object from a solute. 96 | 97 | Parameters 98 | ---------- 99 | solute : Solute 100 | 101 | Returns 102 | ------- 103 | Pairing 104 | """ 105 | assert solute.has_run, "The solute must be run before calling from_solute" 106 | return Coordination( 107 | solute.solvation_data, 108 | solute.n_frames, 109 | solute.n_solutes, 110 | solute.solvent_counts, 111 | solute.u.atoms, 112 | ) 113 | 114 | def _mean_cn(self) -> tuple[dict[str, float], pd.DataFrame]: 115 | counts = self.solvation_data.groupby([FRAME, SOLUTE_IX, SOLVENT]).count()[ 116 | SOLVENT_IX 117 | ] 118 | cn_series = counts.groupby([SOLVENT, FRAME]).sum() / ( 119 | self.n_solutes * self.n_frames 120 | ) 121 | cn_by_frame = cn_series.unstack() 122 | cn_dict = cn_series.groupby([SOLVENT]).sum().to_dict() 123 | return cn_dict, cn_by_frame 124 | 125 | def _calculate_coordinating_atoms(self, tol: float = 0.005) -> pd.DataFrame: 126 | """ 127 | Determine which atom types are actually coordinating 128 | return the types of those atoms 129 | """ 130 | # lookup atom types 131 | atom_types = self.solvation_data.reset_index([SOLVENT_ATOM_IX]) 132 | atom_types[ATOM_TYPE] = self.atom_group[ 133 | atom_types[SOLVENT_ATOM_IX].values 134 | ].types 135 | # count atom types 136 | atoms_by_type = atom_types[[ATOM_TYPE, SOLVENT, SOLVENT_ATOM_IX]] 137 | type_counts = atoms_by_type.groupby([SOLVENT, ATOM_TYPE]).count() 138 | solvent_counts = type_counts.groupby([SOLVENT]).sum()[SOLVENT_ATOM_IX] 139 | # calculate fraction of each 140 | solvent_counts_list = [ 141 | solvent_counts[solvent] 142 | for solvent in type_counts.index.get_level_values(SOLVENT) 143 | ] 144 | type_fractions = type_counts[SOLVENT_ATOM_IX] / solvent_counts_list 145 | type_fractions.name = FRACTION 146 | # change index type 147 | type_fractions = ( 148 | type_fractions.reset_index(ATOM_TYPE) 149 | .astype({ATOM_TYPE: str}) 150 | .set_index(ATOM_TYPE, append=True) 151 | ) 152 | return type_fractions[type_fractions[FRACTION] > tol] 153 | 154 | def _calculate_coordination_vs_random(self) -> dict[str, float]: 155 | """ 156 | Calculate the coordination number relative to random coordination. 157 | 158 | Values higher than 1 imply a higher coordination than expected from 159 | random distribution of solvents. Values lower than 1 imply a lower 160 | coordination than expected from random distribution of solvents. 161 | """ 162 | average_shell_size = sum(self.coordination_numbers.values()) 163 | total_solvents = sum(self.solvent_counts.values()) 164 | coordination_vs_random = {} 165 | for solvent, cn in self.coordination_numbers.items(): 166 | count = self.solvent_counts[solvent] 167 | random = count * average_shell_size / total_solvents 168 | vs_random = cn / random 169 | coordination_vs_random[solvent] = vs_random 170 | return coordination_vs_random 171 | 172 | @property 173 | def coordination_numbers(self) -> dict[str, float]: 174 | """ 175 | A dictionary where keys are residue names (str) and values are the 176 | mean coordination number of that residue (float). 177 | """ 178 | return self._cn_dict 179 | 180 | @property 181 | def coordination_numbers_by_frame(self) -> pd.DataFrame: 182 | """ 183 | A DataFrame of the mean coordination number of in each frame of the trajectory. 184 | """ 185 | return self._cn_dict_by_frame 186 | 187 | @property 188 | def coordinating_atoms(self) -> pd.DataFrame: 189 | """ 190 | Fraction of each atom_type participating in solvation, calculated for each solvent. 191 | """ 192 | return self._coordinating_atoms 193 | 194 | @property 195 | def coordination_vs_random(self) -> dict[str, float]: 196 | """ 197 | Coordination number relative to random coordination. 198 | 199 | Values higher than 1 imply a higher coordination than expected from 200 | random distribution of solvents. Values lower than 1 imply a lower 201 | coordination than expected from random distribution of solvents. 202 | """ 203 | return self._coordination_vs_random 204 | -------------------------------------------------------------------------------- /solvation_analysis/networking.py: -------------------------------------------------------------------------------- 1 | """ 2 | ================ 3 | Networking 4 | ================ 5 | :Author: Orion Cohen, Tingzheng Hou, Kara Fong 6 | :Year: 2021 7 | :Copyright: GNU Public License v3 8 | 9 | Study the topology and structure of solute-solvent networks. 10 | 11 | Networking yields a complete description of coordinated solute-solvent networks 12 | in the solute, regardless of identify. This could include cation-anion networks 13 | or hydrogen bond networks. 14 | 15 | While ``networking`` can be used in isolation, it is meant to be used 16 | as an attribute of the Solute class. This makes instantiating it and calculating the 17 | solvation data a non-issue. 18 | """ 19 | 20 | from typing import Union 21 | 22 | import pandas as pd 23 | import numpy as np 24 | from scipy.sparse import csr_matrix 25 | from scipy.sparse.csgraph import connected_components 26 | 27 | import solvation_analysis 28 | from solvation_analysis._utils import calculate_adjacency_dataframe 29 | from solvation_analysis._column_names import ( 30 | FRAME, 31 | SOLVENT, 32 | SOLVENT_IX, 33 | SOLUTE_IX, 34 | NETWORK, 35 | PAIRED, 36 | NETWORKED, 37 | ISOLATED, 38 | ) 39 | 40 | 41 | class Networking: 42 | """ 43 | Calculate the number and size of solute-solvent networks. 44 | 45 | A network is defined as a bipartite graph of solutes and solvents, where edges 46 | are defined by coordination in the solvation_data DataFrame. A single solvent 47 | or multiple solvents can be selected, but coordination between solvents will 48 | not be included, only coordination between solutes and solvents. 49 | 50 | Networking uses the solvation_data to construct an adjacency matrix and then 51 | extracts the connected subgraphs within it. These connected subgraphs are stored 52 | in a DataFrame in Networking.network_df. 53 | 54 | Several other representations of the networking data are included in the attributes. 55 | 56 | Parameters 57 | ---------- 58 | solvents : str or list[str] 59 | the solvents to include in the solute-solvent network. 60 | solvation_data : pandas.DataFrame 61 | a dataframe of solvation data with columns "frame", "solute_atom", "solvent_atom", 62 | "distance", "solvent_name", and "solvent". 63 | solute_res_ix : np.ndarray 64 | the residue indices of the solutes in solvation_data 65 | res_name_map : pd.Series 66 | a mapping between residue indices and the solute & solvent names in a Solute. 67 | 68 | Examples 69 | -------- 70 | .. code-block:: python 71 | 72 | # first define Li, BN, and FEC AtomGroups 73 | >>> solute = Solute.from_atoms(Li, {'BN': BN, 'FEC': FEC, 'PF6': PF6}) 74 | >>> networking = Networking.from_solute(solute, 'PF6') 75 | """ 76 | 77 | def __init__( 78 | self, 79 | solvents: Union[str, list[str]], 80 | solvation_data: pd.DataFrame, 81 | solute_res_ix: np.ndarray, 82 | res_name_map: pd.Series, 83 | ) -> None: 84 | self.solvents = solvents 85 | self.solvation_data = solvation_data 86 | # TODO: we need all analysis classes to run when there is no solvation_data 87 | # solvent_present = np.isin(self.solvents, self.solvation_data[SOLVENT].unique()) 88 | # if not solvent_present.all(): 89 | # raise Exception(f"Solvent(s) {np.array(self.solvents)[~solvent_present]} not found in solvation data.") 90 | self.solute_res_ix = solute_res_ix 91 | self.res_name_map = res_name_map 92 | self.n_solute = len(solute_res_ix) 93 | self._network_df = self._generate_networks() 94 | self._network_sizes = self._calculate_network_sizes() 95 | self._solute_status, self._solute_status_by_frame = ( 96 | self._calculate_solute_status() 97 | ) 98 | self._solute_status = self._solute_status.to_dict() 99 | 100 | @staticmethod 101 | def from_solute( 102 | solute: "solvation_analysis.Solute", solvents: Union[str, list[str]] 103 | ) -> "Networking": 104 | """ 105 | Generate a Networking object from a solute and solvent names. 106 | 107 | Parameters 108 | ---------- 109 | solute : Solute 110 | solvents : str or list of str 111 | the strings should be the name of solvents in the Solute. The 112 | strings must match exactly for Networking to work properly. The 113 | selected solvents will be used to construct the networking graph 114 | that is described in documentation for the Networking class. 115 | 116 | Returns 117 | ------- 118 | Networking 119 | """ 120 | return Networking( 121 | solvents, 122 | solute.solvation_data, 123 | solute.solute_res_ix, 124 | solute.res_name_map, 125 | ) 126 | 127 | @staticmethod 128 | def _unwrap_adjacency_dataframe(df: pd.DataFrame) -> csr_matrix: 129 | # this class will transform the biadjacency matrix into a proper adjacency matrix 130 | connections = df.reset_index(FRAME).drop(columns=FRAME) 131 | idx = connections.columns.append(connections.index) 132 | directed = connections.reindex(index=idx, columns=idx, fill_value=0) 133 | undirected = directed.values + directed.values.T 134 | adjacency_matrix = csr_matrix(undirected) 135 | return adjacency_matrix 136 | 137 | def _generate_networks(self) -> pd.DataFrame: 138 | """ 139 | This function generates a dataframe containing all the solute-solvent networks 140 | in every frame of the simulation. The rough approach is as follows: 141 | 142 | 1. transform the solvation_data DataFrame into an adjacency matrix 143 | 2. determine the connected subgraphs in the adjacency matrix 144 | 3. tabulate the solvent involved in each network and store in a DataFrame 145 | """ 146 | solvents = [self.solvents] if isinstance(self.solvents, str) else self.solvents 147 | solvation_subset = self.solvation_data[ 148 | np.isin(self.solvation_data[SOLVENT], solvents) 149 | ] 150 | # create adjacency matrix from solvation_subset 151 | graph = calculate_adjacency_dataframe(solvation_subset) 152 | network_arrays = [] 153 | # loop through each time step / frame 154 | for frame, df in graph.groupby(FRAME): 155 | # drop empty columns 156 | df = df.loc[:, (df != 0).any(axis=0)] 157 | # save map from local index to residue index 158 | solute_map = df.index.get_level_values(SOLUTE_IX).values 159 | solvent_map = df.columns.values 160 | ix_to_res_ix = np.concatenate([solvent_map, solute_map]) 161 | adjacency_df = Networking._unwrap_adjacency_dataframe(df) 162 | _, network = connected_components( 163 | csgraph=adjacency_df, directed=False, return_labels=True 164 | ) 165 | network_array = np.vstack( 166 | [ 167 | np.full(len(network), frame), # frame 168 | network, # network 169 | self.res_name_map[ix_to_res_ix], # res_names 170 | ix_to_res_ix, # res index 171 | ] 172 | ).T 173 | network_arrays.append(network_array) 174 | # create and return network dataframe 175 | if len(network_arrays) == 0: 176 | all_clusters = [] 177 | else: 178 | all_clusters = np.concatenate(network_arrays) 179 | cluster_df = ( 180 | pd.DataFrame(all_clusters, columns=[FRAME, NETWORK, SOLVENT, SOLVENT_IX]) 181 | .set_index([FRAME, NETWORK]) 182 | .sort_values([FRAME, NETWORK]) 183 | ) 184 | return cluster_df 185 | 186 | def _calculate_network_sizes(self) -> pd.DataFrame: 187 | # This utility calculates the network sizes and returns a convenient dataframe. 188 | cluster_df = self.network_df 189 | cluster_sizes = cluster_df.groupby([FRAME, NETWORK]).count() 190 | size_counts = ( 191 | cluster_sizes.groupby([FRAME, SOLVENT]).count().unstack(fill_value=0) 192 | ) 193 | size_counts.columns = size_counts.columns.droplevel( 194 | None 195 | ) # the column value is None 196 | return size_counts 197 | 198 | def _calculate_solute_status(self) -> tuple[pd.Series, pd.DataFrame]: 199 | """ 200 | This utility calculates the fraction of each solute with a given "status". 201 | Namely, whether the solvent is "isolated", "paired" (with a single solvent), or 202 | "networked" of > 2 species. 203 | """ 204 | # an empty df with the right index 205 | status = self.network_sizes.iloc[:, 0:0] 206 | status[PAIRED] = self.network_sizes.iloc[:, 0:1].sum(axis=1).astype(int) 207 | status[NETWORKED] = self.network_sizes.iloc[:, 1:].sum(axis=1).astype(int) 208 | status[ISOLATED] = self.n_solute - status.loc[:, [PAIRED, NETWORKED]].sum( 209 | axis=1 210 | ) 211 | status = status.loc[:, [ISOLATED, PAIRED, NETWORKED]] 212 | solute_status_by_frame = status / self.n_solute 213 | solute_status = solute_status_by_frame.mean() 214 | return solute_status, solute_status_by_frame 215 | 216 | def get_network_res_ix(self, network_index: int, frame: int) -> np.ndarray: 217 | """ 218 | Return the indexes of all residues in a selected network. 219 | 220 | The network_index and frame must be provided to fully specify the network. 221 | Once the indexes are returned, they can be used to select an AtomGroup with 222 | the species of interest, see Examples. 223 | 224 | Parameters 225 | ---------- 226 | network_index : int 227 | The index of the network of interest 228 | frame : int 229 | the frame in the trajectory to perform selection at. Defaults to the 230 | current trajectory frame. 231 | Returns 232 | ------- 233 | res_ix : np.ndarray 234 | 235 | Examples 236 | -------- 237 | .. code-block:: python 238 | 239 | # first define Li, BN, and FEC AtomGroups 240 | >>> solute = Solute(Li, {'BN': BN, 'FEC': FEC, 'PF6': PF6}) 241 | >>> networking = Networking.from_solute(solute, 'PF6') 242 | >>> res_ix = networking.get_network_res_ix(1, 5) 243 | >>> solute.u.residues[res_ix].atoms 244 | 245 | 246 | """ 247 | res_ix = self.network_df.loc[ 248 | pd.IndexSlice[frame, network_index], SOLVENT_IX 249 | ].values 250 | return res_ix.astype(int) 251 | 252 | @property 253 | def network_df(self) -> pd.DataFrame: 254 | """ 255 | The dataframe containing all networking data. the indices are the frame and 256 | network index, respectively. the columns are the solvent_name and res_ix. 257 | """ 258 | return self._network_df 259 | 260 | @property 261 | def network_sizes(self) -> pd.DataFrame: 262 | """ 263 | A dataframe of network sizes. the index is the frame. the column headers 264 | are network sizes, or the number of solutes + solvents in the network, so 265 | the columns might be [2, 3, 4, ...]. the values in each column are the 266 | number of networks with that size in each frame. 267 | """ 268 | return self._network_sizes 269 | 270 | @property 271 | def solute_status(self) -> dict[str, float]: 272 | """ 273 | A dictionary where the keys are the "status" of the solute and the values 274 | are the fraction of solute with that status, averaged over all frames. 275 | "isolated" means that the solute not coordinated with any of the networking 276 | solvents, network size is 1. 277 | "paired" means the solute and is coordinated with a single networking 278 | solvent and that solvent is not coordinated to any other solutes, network 279 | size is 2. 280 | "networked" means that the solute is coordinated to more than one solvent 281 | or its solvent is coordinated to more than one solute, network size >= 3. 282 | """ 283 | return self._solute_status 284 | 285 | @property 286 | def solute_status_by_frame(self) -> pd.DataFrame: 287 | """ 288 | As described above, except organized into a dataframe where each 289 | row is a unique frame and the columns are "isolated", "paired", and "networked". 290 | """ 291 | return self._solute_status_by_frame 292 | -------------------------------------------------------------------------------- /solvation_analysis/pairing.py: -------------------------------------------------------------------------------- 1 | """ 2 | ================ 3 | Pairing 4 | ================ 5 | :Author: Orion Cohen, Tingzheng Hou, Kara Fong 6 | :Year: 2021 7 | :Copyright: GNU Public License v3 8 | 9 | Elucidate the composition of the the uncoordinated solvent molecules. 10 | 11 | Pairing tracks the fraction of all solvent molecules paired with the solute, as well 12 | as the composition of the diluent. 13 | 14 | While ``pairing`` can be used in isolation, it is meant to be used 15 | as an attribute of the Solute class. This makes instantiating it and calculating the 16 | solvation data a non-issue. 17 | """ 18 | 19 | import pandas as pd 20 | import numpy as np 21 | 22 | import solvation_analysis 23 | from solvation_analysis._column_names import ( 24 | FRAME, 25 | SOLUTE_IX, 26 | SOLVENT, 27 | SOLVENT_IX, 28 | DISTANCE, 29 | ) 30 | 31 | 32 | class Pairing: 33 | """ 34 | Calculate the fraction of solutes that are coordinated with each solvent. 35 | 36 | The pairing fraction is the fraction of solutes that are coordinated with 37 | ANY solvent with matching type. So if the pairing of mol1 is 0.5, then 50% of 38 | solutes are coordinated with at least 1 mol1. 39 | 40 | The pairing fractions are made available as an mean over the whole 41 | simulation and by frame. 42 | 43 | Parameters 44 | ---------- 45 | solvation_data : pandas.DataFrame 46 | The solvation dataframe output by Solute. 47 | n_frames : int 48 | The number of frames in solvation_data. 49 | n_solutes : int 50 | The number of solutes in solvation_data. 51 | n_solvents : dict of {str: int} 52 | The number of each kind of solvent. 53 | 54 | Examples 55 | -------- 56 | 57 | .. code-block:: python 58 | 59 | # first define Li, BN, and FEC AtomGroups 60 | >>> solute = Solute(Li, {'BN': BN, 'FEC': FEC, 'PF6': PF6}) 61 | >>> solute.run() 62 | >>> solute.pairing.solvent_pairing 63 | {'BN': 1.0, 'FEC': 0.210, 'PF6': 0.120} 64 | """ 65 | 66 | def __init__( 67 | self, 68 | solvation_data: pd.DataFrame, 69 | n_frames: int, 70 | n_solutes: int, 71 | n_solvents: dict[str, int], 72 | ) -> None: 73 | self.solvation_data = solvation_data 74 | self.n_frames = n_frames 75 | self.n_solutes = n_solutes 76 | self.solvent_counts = n_solvents 77 | self._solvent_pairing, self._pairing_by_frame = self._fraction_coordinated() 78 | self._fraction_free_solvents = self._fraction_free_solvent() 79 | ( 80 | self._diluent_composition, 81 | self._diluent_composition_by_frame, 82 | self._diluent_counts, 83 | ) = self._diluent_composition() 84 | 85 | @staticmethod 86 | def from_solute(solute: "solvation_analysis.Solute") -> "Pairing": 87 | """ 88 | Generate a Pairing object from a solute. 89 | 90 | Parameters 91 | ---------- 92 | solute : Solute 93 | 94 | Returns 95 | ------- 96 | Pairing 97 | """ 98 | assert solute.has_run, "The solute must be run before calling from_solute" 99 | return Pairing( 100 | solute.solvation_data, 101 | solute.n_frames, 102 | solute.n_solutes, 103 | solute.solvent_counts, 104 | ) 105 | 106 | def _fraction_coordinated(self) -> tuple[dict[str, float], pd.DataFrame]: 107 | # calculate the fraction of solute coordinated with each solvent 108 | counts = self.solvation_data.groupby([FRAME, SOLUTE_IX, SOLVENT]).count()[ 109 | SOLVENT_IX 110 | ] 111 | pairing_series = counts.astype(bool).groupby([SOLVENT, FRAME]).sum() / ( 112 | self.n_solutes 113 | ) # mean coordinated overall 114 | pairing_by_frame = pairing_series.unstack() 115 | pairing_normalized = pairing_series / self.n_frames 116 | pairing_dict = pairing_normalized.groupby([SOLVENT]).sum().to_dict() 117 | return pairing_dict, pairing_by_frame 118 | 119 | def _fraction_free_solvent(self) -> dict[str, float]: 120 | # calculate the fraction of each solvent NOT coordinated with the solute 121 | counts = self.solvation_data.groupby([FRAME, SOLVENT_IX, SOLVENT]).count()[ 122 | DISTANCE 123 | ] 124 | totals = counts.groupby([SOLVENT]).count() / self.n_frames 125 | n_solvents = np.array( 126 | [self.solvent_counts[name] for name in totals.index.values] 127 | ) 128 | free_solvents = np.ones(len(totals)) - totals / n_solvents 129 | return free_solvents.to_dict() 130 | 131 | def _diluent_composition( 132 | self, 133 | ) -> tuple[dict[str, float], pd.DataFrame, pd.DataFrame]: 134 | coordinated_solvents = self.solvation_data.groupby([FRAME, SOLVENT]).nunique()[ 135 | SOLVENT_IX 136 | ] 137 | solvent_counts = pd.Series(self.solvent_counts) 138 | total_solvents = solvent_counts.reindex(coordinated_solvents.index, level=1) 139 | diluent_solvents = total_solvents - coordinated_solvents 140 | diluent_series = diluent_solvents / diluent_solvents.groupby([FRAME]).sum() 141 | diluent_by_frame = diluent_series.unstack().T 142 | diluent_counts = diluent_solvents.unstack().T 143 | diluent_dict = diluent_by_frame.mean(axis=1).to_dict() 144 | return diluent_dict, diluent_by_frame, diluent_counts 145 | 146 | @property 147 | def solvent_pairing(self) -> dict[str, float]: 148 | """ 149 | A dictionary where keys are residue names (str) and values are the 150 | fraction of solutes that contain that residue (float). 151 | """ 152 | return self._solvent_pairing 153 | 154 | @property 155 | def pairing_by_frame(self) -> pd.DataFrame: 156 | """ 157 | A pd.Dataframe tracking the mean fraction of each residue across frames. 158 | """ 159 | return self._pairing_by_frame 160 | 161 | @property 162 | def fraction_free_solvents(self) -> dict[str, float]: 163 | """ 164 | A dictionary containing the fraction of each solvent that is free. e.g. 165 | not coordinated to a solute. 166 | """ 167 | return self._fraction_free_solvents 168 | 169 | @property 170 | def diluent_composition(self) -> dict[str, float]: 171 | """ 172 | The fraction of the diluent constituted by each solvent. The diluent is 173 | defined as everything that is not coordinated with the solute. 174 | """ 175 | return self._diluent_composition 176 | 177 | @property 178 | def diluent_composition_by_frame(self) -> pd.DataFrame: 179 | """ 180 | A DataFrame of the diluent composition in each frame of the trajectory. 181 | """ 182 | return self._diluent_composition_by_frame 183 | 184 | @property 185 | def diluent_counts(self) -> pd.DataFrame: 186 | """ 187 | A DataFrame of the raw solvent counts in the diluent in each frame of the trajectory. 188 | """ 189 | return self._diluent_counts 190 | -------------------------------------------------------------------------------- /solvation_analysis/residence.py: -------------------------------------------------------------------------------- 1 | """ 2 | ================ 3 | Residence 4 | ================ 5 | :Author: Orion Cohen, Tingzheng Hou, Kara Fong 6 | :Year: 2021 7 | :Copyright: GNU Public License v3 8 | 9 | Understand the dynamic coordination of solvents with the solute. 10 | 11 | Residence times for all solvents are automatically calculated from autocovariance 12 | of the solvent-solute adjacency matrix. 13 | 14 | While ``residence`` can be used in isolation, it is meant to be used 15 | as an attribute of the Solute class. This makes instantiating it and calculating the 16 | solvation data a non-issue. 17 | """ 18 | 19 | import math 20 | import warnings 21 | 22 | import pandas as pd 23 | import numpy as np 24 | import matplotlib.pyplot as plt 25 | from statsmodels.tsa.stattools import acovf 26 | from scipy.optimize import curve_fit 27 | 28 | import solvation_analysis 29 | from solvation_analysis._column_names import ( 30 | SOLVENT, 31 | SOLUTE_ATOM_IX, 32 | SOLVENT_ATOM_IX, 33 | SOLUTE_IX, 34 | ) 35 | from solvation_analysis._utils import calculate_adjacency_dataframe 36 | 37 | 38 | class Residence: 39 | """ 40 | Calculate the residence times of solvents. 41 | 42 | This class calculates the residence time of each solvent on the solute. 43 | The residence time is in units of Solute frames, so if the Solute object 44 | has 1000 frames over 1 nanosecond, then each frame will be 1 picosecond. 45 | Thus a residence time of 100 would translate to 100 picoseconds. 46 | 47 | Two residence time implementations are available. Both calculate the 48 | solute-solvent autocorrelation function for each solute-solvent pair, 49 | take and take the mean over the solvents of each type, this should yield 50 | an exponentially decaying autocorrelation function. 51 | 52 | The first implementation fits an exponential curve to the autocorrelation 53 | function and extract the time constant, which is inversely proportional to the 54 | residence time. This result is saved in the ``residence_times_fit`` attribute. 55 | Unfortunately, the fit often fails to converge (value is set to np.nan), 56 | making this method unreliable. 57 | 58 | Instead, the default implementation is to simply find point where the 59 | value of the autocorrelation function is 1/e, which is the time constant 60 | of an exact exponential. These values are saved in ``residence_times``. 61 | 62 | It is recommended that the user visually inspect the autocorrelation function 63 | with ``Residence.plot_autocorrelation_function`` to ensure an approximately 64 | exponential decay. The residence times are only valid insofar as the autocorrelation 65 | function resembles an exact exponential, it should decays to zero with a long tail. 66 | If the exponential does not decay to zero or its slope does not level off, increasing 67 | the simulation time may help. For this technique to be appropriate, the simulation time 68 | should exceed the residence time. 69 | 70 | A fuller description of the method can be found in 71 | `Self, Fong, and Persson `_ 72 | 73 | Parameters 74 | ---------- 75 | solvation_data : pandas.DataFrame 76 | The solvation data frame output by Solute. 77 | step : int 78 | The spacing of frames in solvation_data. This should be equal 79 | to solute.step. 80 | 81 | Examples 82 | -------- 83 | 84 | .. code-block:: python 85 | 86 | # first define Li, BN, and FEC AtomGroups 87 | >>> solute = Solute(Li, {'BN': BN, 'FEC': FEC, 'PF6': PF6}) 88 | >>> residence = Residence.from_solute(solute) 89 | >>> residence.residence_times_cutoff 90 | {'BN': 4.02, 'FEC': 3.79, 'PF6': 1.15} 91 | """ 92 | 93 | def __init__(self, solvation_data: pd.DataFrame, step: int) -> None: 94 | self.solvation_data = solvation_data 95 | self._auto_covariances = self._calculate_auto_covariance_dict() 96 | self._residence_times_cutoff = self._calculate_residence_times_with_cutoff( 97 | self._auto_covariances, step 98 | ) 99 | self._residence_times_fit, self._fit_parameters = ( 100 | self._calculate_residence_times_with_fit(self._auto_covariances, step) 101 | ) 102 | 103 | @staticmethod 104 | def from_solute(solute: "solvation_analysis.Solute") -> "Residence": 105 | """ 106 | Generate a Residence object from a solute. 107 | 108 | Parameters 109 | ---------- 110 | solute : Solute 111 | 112 | Returns 113 | ------- 114 | Residence 115 | """ 116 | assert solute.has_run, "The solute must be run before calling from_solute" 117 | return Residence(solute.solvation_data, solute.step) 118 | 119 | def _calculate_auto_covariance_dict(self) -> dict[str, np.ndarray]: 120 | partial_index = self.solvation_data.index.droplevel(SOLVENT_ATOM_IX) 121 | unique_indices = np.unique(partial_index) 122 | frame_solute_index = pd.MultiIndex.from_tuples( 123 | unique_indices, names=partial_index.names 124 | ) 125 | auto_covariance_dict = {} 126 | for res_name, res_solvation_data in self.solvation_data.groupby([SOLVENT]): 127 | if isinstance(res_name, tuple): 128 | res_name = res_name[0] 129 | adjacency_mini = calculate_adjacency_dataframe(res_solvation_data) 130 | adjacency_df = adjacency_mini.reindex(frame_solute_index, fill_value=0) 131 | auto_covariance = Residence._calculate_auto_covariance(adjacency_df) 132 | # normalize 133 | auto_covariance = auto_covariance / np.max(auto_covariance) 134 | auto_covariance_dict[res_name] = auto_covariance 135 | return auto_covariance_dict 136 | 137 | @staticmethod 138 | def _calculate_residence_times_with_cutoff( 139 | auto_covariances: dict[str, np.ndarray], 140 | step: int, 141 | convergence_cutoff: float = 0.1, 142 | ) -> dict[str, float]: 143 | residence_times = {} 144 | for res_name, auto_covariance in auto_covariances.items(): 145 | if np.min(auto_covariance) > convergence_cutoff: 146 | residence_times[res_name] = np.nan 147 | warnings.warn( 148 | f"the autocovariance for {res_name} does not converge to zero " 149 | "so a residence time cannot be calculated. A longer simulation " 150 | "is required to get a valid estimate of the residence time." 151 | ) 152 | unassigned = True 153 | for frame, val in enumerate(auto_covariance): 154 | if val < 1 / math.e: 155 | residence_times[res_name] = frame * step 156 | unassigned = False 157 | break 158 | if unassigned: 159 | residence_times[res_name] = np.nan 160 | return residence_times 161 | 162 | @staticmethod 163 | def _calculate_residence_times_with_fit( 164 | auto_covariances: dict[str, np.ndarray], step: int 165 | ) -> tuple[dict[str, float], dict[str, tuple[float, float, float]]]: 166 | # calculate the residence times 167 | residence_times = {} 168 | fit_parameters = {} 169 | for res_name, auto_covariance in auto_covariances.items(): 170 | res_time, params = Residence._fit_exponential(auto_covariance, res_name) 171 | residence_times[res_name], fit_parameters[res_name] = ( 172 | round(res_time * step, 2), 173 | params, 174 | ) 175 | return residence_times, fit_parameters 176 | 177 | def plot_auto_covariance(self, res_name: str) -> tuple[plt.Figure, plt.Axes]: 178 | """ 179 | Plot the autocovariance of a solvent on the solute. 180 | 181 | See the discussion in the class docstring for more information. 182 | 183 | Parameters 184 | ---------- 185 | res_name : str 186 | the name of a solvent in the solute. 187 | 188 | Returns 189 | ------- 190 | fig : matplotlib.Figure 191 | ax : matplotlib.Axes 192 | """ 193 | auto_covariance = self.auto_covariances[res_name] 194 | frames = np.arange(len(auto_covariance)) 195 | params = self.fit_parameters[res_name] 196 | 197 | def exp_func(x): 198 | return self._exponential_decay(x, *params) 199 | 200 | exp_fit = np.array(map(exp_func, frames)) 201 | fig, ax = plt.subplots() 202 | ax.plot(frames, auto_covariance, "b-", label="auto covariance") 203 | try: 204 | ax.scatter(frames, exp_fit, label="exponential fit") 205 | except RuntimeError: 206 | warnings.warn( 207 | f"The fit for {res_name} failed so the exponential " 208 | f"fit will not be plotted." 209 | ) 210 | ax.hlines(y=1 / math.e, xmin=frames[0], xmax=frames[-1], label="1/e cutoff") 211 | ax.set_xlabel("Timestep (frames)") 212 | ax.set_ylabel("Normalized Autocovariance") 213 | ax.set_ylim(0, 1) 214 | ax.legend() 215 | return fig, ax 216 | 217 | @staticmethod 218 | def _exponential_decay(x: np.ndarray, a: float, b: float, c: float) -> np.ndarray: 219 | """ 220 | An exponential decay function. 221 | 222 | Args: 223 | x: Independent variable. 224 | a: Initial quantity. 225 | b: Exponential decay constant. 226 | c: Constant. 227 | 228 | Returns: 229 | The acf 230 | """ 231 | return a * np.exp(-b * x) + c 232 | 233 | @staticmethod 234 | def _fit_exponential( 235 | auto_covariance: np.ndarray, res_name: str 236 | ) -> tuple[float, tuple[float, float, float]]: 237 | auto_covariance_norm = auto_covariance / auto_covariance[0] 238 | try: 239 | params, param_covariance = curve_fit( 240 | Residence._exponential_decay, 241 | np.arange(len(auto_covariance_norm)), 242 | auto_covariance_norm, 243 | p0=(1, 0.1, 0.01), 244 | ) 245 | tau = 1 / params[1] # p 246 | except RuntimeError: 247 | warnings.warn( 248 | f"The fit for {res_name} failed so its values in" 249 | f"residence_time_fits and fit_parameters will be" 250 | f"set to np.nan." 251 | ) 252 | tau, params = np.nan, (np.nan, np.nan, np.nan) 253 | return tau, params 254 | 255 | @staticmethod 256 | def _calculate_auto_covariance(adjacency_matrix: pd.DataFrame) -> np.ndarray: 257 | auto_covariances = [] 258 | timesteps = adjacency_matrix.index.levels[0] 259 | 260 | for solute_ix, solute_df in adjacency_matrix.groupby( 261 | [SOLUTE_IX, SOLUTE_ATOM_IX] 262 | ): 263 | # this is needed to make sure auto-covariances can be concatenated later 264 | new_solute_df = solute_df.droplevel([SOLUTE_IX, SOLUTE_ATOM_IX]).reindex( 265 | timesteps, fill_value=0 266 | ) 267 | non_zero_cols = new_solute_df.loc[:, (solute_df != 0).any(axis=0)] 268 | auto_covariance_df = non_zero_cols.apply( 269 | acovf, 270 | axis=0, 271 | result_type="expand", 272 | demean=False, 273 | adjusted=True, 274 | fft=True, 275 | ) 276 | # timesteps with no binding are getting skipped, we need to make sure to include all timesteps 277 | auto_covariances.append(auto_covariance_df.values) 278 | 279 | auto_covariance = np.mean(np.concatenate(auto_covariances, axis=1), axis=1) 280 | return auto_covariance 281 | 282 | @property 283 | def auto_covariances(self) -> dict[str, np.ndarray]: 284 | """ 285 | A dictionary where keys are residue names and values are the 286 | autocovariance of the that residue on the solute. 287 | """ 288 | return self._auto_covariances 289 | 290 | @property 291 | def residence_times_cutoff(self) -> dict[str, float]: 292 | """ 293 | A dictionary where keys are residue names and values are the 294 | residence times of the that residue on the solute, calculated 295 | with the 1/e cutoff method. 296 | """ 297 | return self._residence_times_cutoff 298 | 299 | @property 300 | def residence_times_fit(self) -> dict[str, float]: 301 | """ 302 | A dictionary where keys are residue names and values are the 303 | residence times of the that residue on the solute, calculated 304 | with the exponential fit method. 305 | """ 306 | return self._residence_times_fit 307 | 308 | @property 309 | def fit_parameters(self) -> dict[str, tuple[float, float, float]]: 310 | """ 311 | A dictionary where keys are residue names and values are the 312 | parameters for the exponential fit to the autocorrelation function. 313 | """ 314 | return self._fit_parameters 315 | -------------------------------------------------------------------------------- /solvation_analysis/speciation.py: -------------------------------------------------------------------------------- 1 | """ 2 | ================ 3 | Speciation 4 | ================ 5 | :Author: Orion Cohen, Tingzheng Hou, Kara Fong 6 | :Year: 2021 7 | :Copyright: GNU Public License v3 8 | 9 | Explore the precise solvation shell of every solute. 10 | 11 | Speciation tabulates the unique solvation shell compositions, their fraction, 12 | and their temporal locations. 13 | 14 | From this, it provides search functionality to query for specific solvation shell 15 | compositions. Extremely convenient for visualization. 16 | 17 | While ``speciation`` can be used in isolation, it is meant to be used 18 | as an attribute of the Solute class. This makes instantiating it and calculating the 19 | solvation data a non-issue. 20 | """ 21 | 22 | import pandas as pd 23 | 24 | import solvation_analysis 25 | from solvation_analysis._column_names import ( 26 | FRAME, 27 | SOLUTE_IX, 28 | SOLVENT, 29 | SOLVENT_IX, 30 | COUNT, 31 | ) 32 | 33 | 34 | class Speciation: 35 | """ 36 | Calculate the solvation shells of every solute. 37 | 38 | Speciation organizes the solvation data by the type of residue 39 | coordinated with the central solvent. It collects this information in a 40 | pandas.DataFrame indexed by the frame and solute number. Each column is 41 | one of the solvents in the solvent_name column of the solvation data. The 42 | column value is how many residue of that type are in the solvation shell. 43 | 44 | Speciation provides the speciation of each solute in the speciation 45 | attribute, it also calculates the fraction of each unique 46 | shell and makes it available in the speciation_fraction attribute. 47 | 48 | Additionally, there are methods for finding solvation shells of 49 | interest and computing how common certain shell configurations are. 50 | 51 | Parameters 52 | ---------- 53 | solvation_data : pandas.DataFrame 54 | The solvation dataframe output by Solute. 55 | n_frames : int 56 | The number of frames in solvation_data. 57 | n_solutes : int 58 | The number of solutes in solvation_data. 59 | """ 60 | 61 | def __init__( 62 | self, solvation_data: pd.DataFrame, n_frames: int, n_solutes: int 63 | ) -> None: 64 | self.solvation_data = solvation_data 65 | self.n_frames = n_frames 66 | self.n_solutes = n_solutes 67 | self._speciation_df, self._speciation_fraction = self._compute_speciation() 68 | self._solvent_co_occurrence = self._solvent_co_occurrence() 69 | 70 | @staticmethod 71 | def from_solute(solute: "solvation_analysis.Solute") -> "Speciation": 72 | """ 73 | Generate a Speciation object from a solute. 74 | 75 | Parameters 76 | ---------- 77 | solute : Solute 78 | 79 | Returns 80 | ------- 81 | Pairing 82 | """ 83 | assert solute.has_run, "The solute must be run before calling from_solute" 84 | return Speciation( 85 | solute.solvation_data, 86 | solute.n_frames, 87 | solute.n_solutes, 88 | ) 89 | 90 | def _compute_speciation(self) -> tuple[pd.DataFrame, pd.DataFrame]: 91 | counts = self.solvation_data.groupby([FRAME, SOLUTE_IX, SOLVENT]).count()[ 92 | SOLVENT_IX 93 | ] 94 | counts_re = counts.reset_index([SOLVENT]) 95 | speciation_data = counts_re.pivot(columns=[SOLVENT]).fillna(0).astype(int) 96 | res_names = speciation_data.columns.levels[1] 97 | speciation_data.columns = res_names 98 | sum_series = speciation_data.groupby(speciation_data.columns.to_list()).size() 99 | sum_sorted = sum_series.sort_values(ascending=False) 100 | speciation_fraction = sum_sorted.reset_index().rename(columns={0: COUNT}) 101 | speciation_fraction[COUNT] = speciation_fraction[COUNT] / ( 102 | self.n_frames * self.n_solutes 103 | ) 104 | return speciation_data, speciation_fraction 105 | 106 | @classmethod 107 | def _mean_speciation( 108 | cls, speciation_frames: pd.DataFrame, solute_number: int, frame_number: int 109 | ) -> pd.Series: 110 | means = speciation_frames.sum(axis=1) / (solute_number * frame_number) 111 | return means 112 | 113 | def calculate_shell_fraction(self, shell_dict: dict[str, int]) -> float: 114 | """ 115 | Calculate the fraction of shells matching shell_dict. 116 | 117 | This function computes the fraction of solvation shells that exist with a particular 118 | composition. The composition is specified by the shell_dict. The fraction 119 | will be of all shells that match that specification. 120 | 121 | Attributes 122 | ---------- 123 | shell_dict : dict of {str: int} 124 | a specification for a shell composition. Keys are residue names (str) 125 | and values are the number of desired residues. e.g. if shell_dict = 126 | {'mol1': 4} then the function will return the fraction of shells 127 | that have 4 mol1. Note that this may include shells with 4 mol1 and 128 | any number of other solvents. To specify a shell with 4 mol1 and nothing 129 | else, enter a dict such as {'mol1': 4, 'mol2': 0, 'mol3': 0}. 130 | 131 | Returns 132 | ------- 133 | float 134 | the fraction of shells 135 | 136 | Examples 137 | -------- 138 | 139 | .. code-block:: python 140 | 141 | # first define Li, BN, and FEC AtomGroups 142 | >>> solute = Solute(Li, {'BN': BN, 'FEC': FEC, 'PF6': PF6}) 143 | >>> solute.run() 144 | >>> solute.speciation.calculate_shell_fraction({'BN': 4, 'PF6': 1}) 145 | 0.0898 146 | """ 147 | query_list = [f"{name} == {str(count)}" for name, count in shell_dict.items()] 148 | query = " and ".join(query_list) 149 | query_counts = self.speciation_fraction.query(query) 150 | return query_counts[COUNT].sum() 151 | 152 | def get_shells(self, shell_dict: dict[str, int]) -> pd.DataFrame: 153 | """ 154 | Find all solvation shells that match shell_dict. 155 | 156 | This returns the frame, solute index, and composition of all solutes 157 | that match the composition given in shell_dict. 158 | 159 | Attributes 160 | ---------- 161 | shell_dict : dict of {str: int} 162 | a specification for a shell composition. Keys are residue names (str) 163 | and values are the number of desired residues. e.g. if shell_dict = 164 | {'mol1': 4} then the function will return all shells 165 | that have 4 mol1. Note that this may include shells with 4 mol1 and 166 | any number of other solvents. To specify a shell with 4 mol1 and nothing 167 | else, enter a dict such as {'mol1': 4, 'mol2': 0, 'mol3': 0}. 168 | 169 | Returns 170 | ------- 171 | pandas.DataFrame 172 | the index and composition of all shells that match shell_dict 173 | """ 174 | query_list = [f"{name} == {str(count)}" for name, count in shell_dict.items()] 175 | query = " and ".join(query_list) 176 | query_counts = self.speciation_data.query(query) 177 | return query_counts 178 | 179 | def _solvent_co_occurrence(self) -> pd.DataFrame: 180 | # calculate the co-occurrence of solvent molecules. 181 | expected_solvents_list = [] 182 | actual_solvents_list = [] 183 | for solvent in self.speciation_data.columns.values: 184 | # calculate number of available coordinating solvent slots 185 | shells_w_solvent = self.speciation_data.query(f"`{solvent}` > 0") 186 | n_solvents = shells_w_solvent.sum() 187 | # calculate expected number of coordinating solvents 188 | n_coordination_slots = n_solvents.sum() - len(shells_w_solvent) 189 | coordination_fraction = ( 190 | self.speciation_data.sum() / self.speciation_data.sum().sum() 191 | ) 192 | expected_solvents = coordination_fraction * n_coordination_slots 193 | # calculate actual number of coordinating solvents 194 | actual_solvents = n_solvents.copy() 195 | actual_solvents[solvent] = actual_solvents[solvent] - len(shells_w_solvent) 196 | # name series and append to list 197 | expected_solvents.name = solvent 198 | actual_solvents.name = solvent 199 | expected_solvents_list.append(expected_solvents) 200 | actual_solvents_list.append(actual_solvents) 201 | if len(actual_solvents_list) == 0 or len(expected_solvents_list) == 0: 202 | # we return this if nothing is solvated 203 | return pd.DataFrame() 204 | # make DataFrames 205 | actual_df = pd.concat(actual_solvents_list, axis=1) 206 | expected_df = pd.concat(expected_solvents_list, axis=1) 207 | # calculate correlation matrix 208 | correlation = actual_df / expected_df 209 | return correlation 210 | 211 | @property 212 | def speciation_data(self) -> pd.DataFrame: 213 | """ 214 | A dataframe containing the speciation of every solute at 215 | every trajectory frame. Indexed by timestep and solute numbers. 216 | Columns are the solvent molecules and values are the number 217 | of solvent in the shell. 218 | """ 219 | return self._speciation_df 220 | 221 | @property 222 | def speciation_fraction(self) -> pd.DataFrame: 223 | """ 224 | The fraction of shells of each type. Columns are the solvent 225 | molecules and values are the number of solvent in the shell. 226 | The final column is the fraction of total shell of that 227 | particular composition. 228 | """ 229 | return self._speciation_fraction 230 | 231 | @property 232 | def solvent_co_occurrence(self) -> pd.DataFrame: 233 | """ 234 | The actual co-occurrence of solvents divided by the expected co-occurrence. 235 | In other words, given one molecule of solvent i in the shell, what is the 236 | probability of finding a solvent j relative to choosing a solvent at random 237 | from the pool of all coordinated solvents. This matrix will 238 | likely not be symmetric. 239 | """ 240 | return self._solvent_co_occurrence 241 | -------------------------------------------------------------------------------- /solvation_analysis/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Empty init file in case you choose a package besides PyTest such as Nose which may look for such a file 3 | """ 4 | -------------------------------------------------------------------------------- /solvation_analysis/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import MDAnalysis as mda 3 | import numpy as np 4 | import pandas as pd 5 | import pytest 6 | 7 | from MDAnalysis import transformations 8 | from solvation_analysis.networking import Networking 9 | from solvation_analysis.residence import Residence 10 | 11 | from solvation_analysis.rdf_parser import identify_cutoff_poly 12 | from solvation_analysis.tests.datafiles import ( 13 | bn_fec_data, 14 | bn_fec_dcd_wrap, 15 | bn_fec_atom_types, 16 | eax_data, 17 | iba_data, 18 | iba_dcd, 19 | ) 20 | from solvation_analysis.tests.datafiles import ( 21 | easy_rdf_bins, 22 | easy_rdf_data, 23 | hard_rdf_bins, 24 | hard_rdf_data, 25 | non_solv_rdf_bins, 26 | non_solv_rdf_data, 27 | bn_fec_solv_df_large, 28 | ) 29 | from solvation_analysis.solute import Solute 30 | import pathlib 31 | 32 | 33 | def test_solvation_analysis_imported(): 34 | """Sample test, will always pass so long as import statement worked""" 35 | assert "solvation_analysis" in sys.modules 36 | 37 | 38 | def make_grid_universe(n_grid, residue_size, n_frames=10): 39 | """ 40 | Will make a universe of atoms with specified attributes. 41 | 42 | Parameters 43 | ---------- 44 | n_grid - dimension of grid sides 45 | residue_size - size of mda residues 46 | n_frames - number of frames 47 | 48 | Returns 49 | ------- 50 | A constructed MDanalysis.Universe 51 | """ 52 | n_particles = n_grid**3 53 | assert ( 54 | n_particles % residue_size == 0 55 | ), "residue_size must be a factor of n_particles" 56 | n_residues = n_particles // residue_size 57 | atom_residues = np.array(range(0, n_particles)) // residue_size 58 | 59 | grid = np.mgrid[0:n_grid, 0:n_grid, 0:n_grid] # make the grid 60 | frame = grid.reshape([3, n_particles]).T # make it the right shape 61 | 62 | traj = np.empty([n_frames, n_particles, 3]) 63 | for i in range(n_frames): 64 | traj[i, :, :] = frame # copy the coordinates to n frames 65 | u = mda.Universe.empty( 66 | n_particles, n_residues=n_residues, atom_resindex=atom_residues, trajectory=True 67 | ) # jam it into a universe 68 | u.load_new(traj) 69 | u.add_TopologyAttr("masses", np.ones(n_particles)) 70 | u.add_TopologyAttr("resid") 71 | return u 72 | 73 | 74 | @pytest.fixture 75 | def u_grid_3(): 76 | """Creates a 6x6x6 grid with residues containing 3 atoms""" 77 | return make_grid_universe(6, 3) 78 | 79 | 80 | @pytest.fixture 81 | def u_grid_1(): 82 | """Creates a 6x6x6 grid with residues containing 1 atom""" 83 | return make_grid_universe(6, 1) 84 | 85 | 86 | @pytest.fixture(scope="module") 87 | def u_real(): 88 | """Returns a universe of a BN FEC trajectory""" 89 | return mda.Universe(bn_fec_data, bn_fec_dcd_wrap) 90 | 91 | 92 | @pytest.fixture(scope="module") 93 | def u_real_named(u_real): 94 | """Returns a universe of a BN FEC trajectory with residues and atoms named""" 95 | types = np.loadtxt(bn_fec_atom_types, dtype=str) 96 | u_real.add_TopologyAttr("name", values=types) 97 | resnames = ["BN"] * 363 + ["FEC"] * 237 + ["PF6"] * 49 + ["Li"] * 49 98 | u_real.add_TopologyAttr("resnames", values=resnames) 99 | return u_real 100 | 101 | 102 | @pytest.fixture(scope="module") 103 | def atom_groups(u_real): 104 | """Returns pre-selected atom groups in the BN FEC universe""" 105 | li_atoms = u_real.atoms.select_atoms("type 22") 106 | pf6_atoms = u_real.atoms.select_atoms("byres type 20") 107 | bn_atoms = u_real.atoms.select_atoms("byres type 5") 108 | fec_atoms = u_real.atoms.select_atoms("byres type 19") 109 | atom_groups = {"li": li_atoms, "pf6": pf6_atoms, "bn": bn_atoms, "fec": fec_atoms} 110 | return atom_groups 111 | 112 | 113 | def rdf_loading_helper(bins_files, data_files): 114 | """ 115 | Creates dictionary of bin and data arrays with a rdf tag as key 116 | """ 117 | rdf_bins = {key: list(np.load(npz).values())[0] for key, npz in bins_files.items()} 118 | rdf_data = {key: list(np.load(npz).values())[0] for key, npz in data_files.items()} 119 | shared_keys = set(rdf_data.keys()) & set(rdf_bins.keys()) 120 | rdf_bins_and_data = {key: (rdf_bins[key], rdf_data[key]) for key in shared_keys} 121 | return rdf_bins_and_data 122 | 123 | 124 | @pytest.fixture 125 | def rdf_bins_and_data_easy(): 126 | return rdf_loading_helper(easy_rdf_bins, easy_rdf_data) 127 | 128 | 129 | @pytest.fixture 130 | def rdf_bins_and_data_hard(): 131 | return rdf_loading_helper(hard_rdf_bins, hard_rdf_data) 132 | 133 | 134 | @pytest.fixture 135 | def rdf_bins_and_data_non_solv(): 136 | return rdf_loading_helper(non_solv_rdf_bins, non_solv_rdf_data) 137 | 138 | 139 | @pytest.fixture(scope="module") 140 | def pre_solute(atom_groups): 141 | li = atom_groups["li"] 142 | pf6 = atom_groups["pf6"] 143 | bn = atom_groups["bn"] 144 | fec = atom_groups["fec"] 145 | return Solute.from_atoms( 146 | li, 147 | {"pf6": pf6, "bn": bn, "fec": fec}, 148 | radii={"pf6": 2.8, "bn": 2.61468, "fec": 2.43158}, 149 | rdf_init_kwargs={"range": (0, 8.0)}, 150 | ) 151 | 152 | 153 | @pytest.fixture(scope="function") 154 | def pre_solute_mutable(atom_groups): 155 | li = atom_groups["li"] 156 | pf6 = atom_groups["pf6"] 157 | bn = atom_groups["bn"] 158 | fec = atom_groups["fec"] 159 | return Solute.from_atoms( 160 | li, 161 | {"pf6": pf6, "bn": bn, "fec": fec}, 162 | radii={"pf6": 2.8, "bn": 2.61468, "fec": 2.43158}, 163 | rdf_init_kwargs={"range": (0, 8.0)}, 164 | rdf_kernel=identify_cutoff_poly, 165 | ) 166 | 167 | 168 | @pytest.fixture(scope="module") 169 | def run_solute(pre_solute): 170 | pre_solute.run(step=1) 171 | return pre_solute 172 | 173 | 174 | @pytest.fixture(scope="module") 175 | def u_eax_series(): 176 | boxes = { 177 | "ea": [45.760393, 45.760393, 45.760393, 90, 90, 90], 178 | "eaf": [47.844380, 47.844380, 47.844380, 90, 90, 90], 179 | "fea": [48.358954, 48.358954, 48.358954, 90, 90, 90], 180 | "feaf": [50.023129, 50.023129, 50.023129, 90, 90, 90], 181 | } 182 | us = {} 183 | for solvent_dir in pathlib.Path(eax_data).iterdir(): 184 | u_solv = mda.Universe( 185 | str(solvent_dir / "topology.pdb"), str(solvent_dir / "trajectory_equil.dcd") 186 | ) 187 | # our dcd lacks dimensions so we must manually set them 188 | box = boxes[solvent_dir.stem] 189 | set_dim = transformations.boxdimensions.set_dimensions(box) 190 | u_solv.trajectory.add_transformations(set_dim) 191 | us[solvent_dir.stem] = u_solv 192 | return us 193 | 194 | 195 | @pytest.fixture(scope="module") 196 | def u_eax_atom_groups(u_eax_series): 197 | atom_groups_dict = {} 198 | for name, u in u_eax_series.items(): 199 | atom_groups = {} 200 | atom_groups["li"] = u.atoms.select_atoms("element Li") 201 | atom_groups["pf6"] = u.atoms.select_atoms("byres element P") 202 | residue_lengths = np.array([len(elements) for elements in u.residues.elements]) 203 | eax_fec_cutoff = np.unique(residue_lengths, return_index=True)[1][2] 204 | atom_groups[name] = u.atoms.select_atoms(f"resid 1:{eax_fec_cutoff}") 205 | atom_groups["fec"] = u.atoms.select_atoms(f"resid {eax_fec_cutoff + 1}:600") 206 | atom_groups_dict[name] = atom_groups 207 | return atom_groups_dict 208 | 209 | 210 | @pytest.fixture(scope="module") 211 | def eax_solutes(u_eax_atom_groups): 212 | solutes = {} 213 | for name, atom_groups in u_eax_atom_groups.items(): 214 | solute = Solute.from_atoms( 215 | atom_groups["li"], 216 | { 217 | "pf6": atom_groups["pf6"], 218 | name: atom_groups[name], 219 | "fec": atom_groups["fec"], 220 | }, 221 | analysis_classes=["pairing", "coordination", "speciation", "networking"], 222 | networking_solvents=["pf6"], 223 | ) 224 | solute.run() 225 | solutes[name] = solute 226 | return solutes 227 | 228 | 229 | @pytest.fixture(scope="module") 230 | def iba_u(): 231 | return mda.Universe(iba_data, iba_dcd) 232 | 233 | 234 | @pytest.fixture(scope="module") 235 | def iba_solvents(iba_u): 236 | iba = iba_u.select_atoms("byres element C") 237 | H2O = iba_u.atoms - iba 238 | return {"iba": iba, "H2O": H2O} 239 | 240 | 241 | @pytest.fixture(scope="module") 242 | def iba_atom_groups(iba_solvents): 243 | iba = iba_solvents["iba"] 244 | return { 245 | "iba_alcohol_O": iba[5::12], 246 | "iba_alcohol_H": iba[11::12], 247 | "iba_ketone": iba[4::12], 248 | "iba_C0": iba[0::12], 249 | "iba_C1": iba[1::12], 250 | "iba_C2": iba[2::12], 251 | "iba_C3": iba[3::12], 252 | "iba_H6": iba[6::12], 253 | "iba_H7": iba[7::12], 254 | "iba_H8": iba[8::12], 255 | "iba_H9": iba[9::12], 256 | "iba_H10": iba[10::12], 257 | } 258 | 259 | 260 | @pytest.fixture(scope="module") 261 | def H2O_atom_groups(iba_solvents): 262 | H2O = iba_solvents["H2O"] 263 | return { 264 | "H2O_O": H2O[0::3], 265 | "H2O_H1": H2O[1::3], 266 | "H2O_H2": H2O[2::3], 267 | } 268 | 269 | 270 | @pytest.fixture(scope="module") 271 | def iba_solutes(iba_atom_groups, iba_solvents): 272 | solutes = {} 273 | for name, atom_group in iba_atom_groups.items(): 274 | radii = ( 275 | {"iba": 1.9, "H2O": 1.9} if ("iba_H" in name or "iba_C" in name) else None 276 | ) 277 | solute = Solute.from_atoms( 278 | atom_group, 279 | iba_solvents, 280 | solute_name=name, 281 | radii=radii, 282 | ) 283 | solute.run() 284 | solutes[name] = solute 285 | return solutes 286 | 287 | 288 | @pytest.fixture(scope="module") 289 | def iba_small_solute(iba_atom_groups, iba_solvents): 290 | solute_atoms = { 291 | "iba_ketone": iba_atom_groups["iba_ketone"], 292 | "iba_alcohol_O": iba_atom_groups["iba_alcohol_O"], 293 | "iba_alcohol_H": iba_atom_groups["iba_alcohol_H"], 294 | } 295 | solute = Solute.from_atoms_dict( 296 | solute_atoms, 297 | iba_solvents, 298 | solute_name="iba", 299 | ) 300 | solute.run() 301 | return solute 302 | 303 | 304 | @pytest.fixture 305 | def solvation_results(run_solute): 306 | return run_solute._solvation_frames 307 | 308 | 309 | @pytest.fixture 310 | def solvation_data(run_solute): 311 | return run_solute.solvation_data 312 | 313 | 314 | @pytest.fixture(scope="module") 315 | def solvation_data_large(): 316 | return pd.read_csv(bn_fec_solv_df_large, index_col=[0, 1, 2, 3]) 317 | 318 | 319 | @pytest.fixture(scope="module") 320 | def solvation_data_sparse(solvation_data_large): 321 | step = 10 322 | return solvation_data_large.loc[pd.IndexSlice[::step, :, :], :] 323 | 324 | 325 | @pytest.fixture(scope="module") 326 | def residence(solvation_data_sparse): 327 | return Residence(solvation_data_sparse, step=10) 328 | 329 | 330 | @pytest.fixture(scope="module") 331 | def networking(run_solute): 332 | return Networking.from_solute(run_solute, "pf6") 333 | -------------------------------------------------------------------------------- /solvation_analysis/tests/data/README.md: -------------------------------------------------------------------------------- 1 | # Data Generation Process 2 | 3 | This README describes the data-generating process for the test data used in this package. 4 | 5 | ## Trajectory Files 6 | 7 | ### bn_fec_data 8 | 9 | Molecular dynamics runs were performed on a simulated Li-ion battery electrolyte composed 10 | of 363 Butryro-Nitride (BN), 49 Ethylene Carbonate (EC), and 49 Lithium Hexafluorophosphate (LiPF66). 11 | The energy was minimized in PACKMOL and the trajectory was generated with LAMMPS. There is a 5 ns equilibration 12 | period followed by a 5 ns production run. 13 | 14 | `bn_fec.data` was generated with the Pymatgen Python package. OPLS parameters for BN were downloaded 15 | from LigParGen and parameters for FEC were provided by Tingzheng Hou, see https://doi.org/10.1016/j.nanoen.2019.103881. 16 | `bn_fec_short_wrap.dcd` is the wrapped trajectory file from LAMMPS 17 | `bn_fec_short_unwrap.dcd` is the unwrapped trajectory file from LAMMPS 18 | `bn_fec_elements.csv` is a csv file with the element name of every atom name in the 19 | trajectory file. It is used to add names to the Universe. 20 | `bn_solv_df_large.csv` is a csv file of the solvation_data over a longer simulation, 500 frames 21 | 22 | ### ea_fec_data 23 | 24 | Molecular dynamics runs were performed on a simulated Li-ion battery electrolyte composed 25 | of Ethyl Acetate (EA), Fluorinated Ethylene Carbonate (FEC), and Lithium Hexafluorophosphate (LiPF66). 26 | The energy was minimized in PACKMOL and the trajectory was generated with OpenMM. There is a 5 ns equilibration 27 | period followed by a 5 ns production run. 28 | 29 | `ea_fec.dcd` is an abbreviated dcd file of the trajectory, 10 frames long 30 | `ea_fec.pdb` contains the topology of the trajectory 31 | 32 | ### eax_data 33 | 34 | Molecular dynamics runs were performed on a simulated Li-ion battery electrolyte composed 35 | of Fluorinated Ethylene Carbonate (FEC), Lithium Hexafluorophosphate (LiPF66), and one of four 36 | fluorinated Ethyl Acetate species (EA, EAf, fEA, and fEAf, abbreviated in general as EAx). 37 | The energy was minimized in PACKMOL and the trajectory was generated with OpenMM. There is a 5 ns equilibration 38 | period followed by a 5 ns production run. 39 | 40 | Each simulation has a `dcd` file for the trajectory and a `pdb` file for the topology. 41 | 42 | 43 | ## Radial Distribution Functions 44 | 45 | The radial distribution functions were generated with MDAnalysis.rdf.interRDF(). 46 | 47 | The `rdf_vs_li_easy` and `rdf_vs_li_hard` directories contain RDFs of various molecular 48 | species against the Li ions, most of these are fairly well-defined RDFs. The `easy` RDFs are 49 | generated from 500 frames of the trajectory while the `hard` RDFs are generated from 50 | only 50 frames, so they are noisier. 51 | 52 | The `rdf_non_solvated` directory contains RDFs of non-Li molecular species against eachother. 53 | These RDFs have no clear structure and should not register a solvation shell. These are added 54 | to provide a negative test of the solvation shell identification kernel. 55 | -------------------------------------------------------------------------------- /solvation_analysis/tests/data/bn_fec_data/bn_fec_short_unwrap.dcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/bn_fec_data/bn_fec_short_unwrap.dcd -------------------------------------------------------------------------------- /solvation_analysis/tests/data/bn_fec_data/bn_fec_short_wrap.dcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/bn_fec_data/bn_fec_short_wrap.dcd -------------------------------------------------------------------------------- /solvation_analysis/tests/data/ea_fec_data/ea_fec.dcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/ea_fec_data/ea_fec.dcd -------------------------------------------------------------------------------- /solvation_analysis/tests/data/eax_data/ea/trajectory_equil.dcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/eax_data/ea/trajectory_equil.dcd -------------------------------------------------------------------------------- /solvation_analysis/tests/data/eax_data/eaf/trajectory_equil.dcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/eax_data/eaf/trajectory_equil.dcd -------------------------------------------------------------------------------- /solvation_analysis/tests/data/eax_data/fea/trajectory_equil.dcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/eax_data/fea/trajectory_equil.dcd -------------------------------------------------------------------------------- /solvation_analysis/tests/data/eax_data/feaf/trajectory_equil.dcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/eax_data/feaf/trajectory_equil.dcd -------------------------------------------------------------------------------- /solvation_analysis/tests/data/iba_data/isobutyric_acid.dcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/iba_data/isobutyric_acid.dcd -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_fec_F_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_fec_F_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_fec_F_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_fec_F_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_fec_O_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_fec_O_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_fec_O_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_fec_O_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_fec_all_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_fec_all_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_fec_all_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_fec_all_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_pf6_F_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_pf6_F_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_pf6_F_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_pf6_F_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_pf6_all_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_pf6_all_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_pf6_all_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_bn_N_vs_pf6_all_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_F_vs_bn_N_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_F_vs_bn_N_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_F_vs_bn_N_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_F_vs_bn_N_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_F_vs_bn_all_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_F_vs_bn_all_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_F_vs_bn_all_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_F_vs_bn_all_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_F_vs_pf6_F_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_F_vs_pf6_F_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_F_vs_pf6_F_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_F_vs_pf6_F_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_F_vs_pf6_all_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_F_vs_pf6_all_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_F_vs_pf6_all_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_F_vs_pf6_all_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_O_vs_bn_N_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_O_vs_bn_N_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_O_vs_bn_N_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_O_vs_bn_N_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_O_vs_bn_all_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_O_vs_bn_all_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_O_vs_bn_all_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_O_vs_bn_all_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_O_vs_pf6_F_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_O_vs_pf6_F_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_O_vs_pf6_F_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_O_vs_pf6_F_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_O_vs_pf6_all_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_O_vs_pf6_all_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_O_vs_pf6_all_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_fec_O_vs_pf6_all_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_bn_N_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_bn_N_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_bn_N_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_bn_N_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_bn_all_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_bn_all_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_bn_all_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_bn_all_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_fec_F_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_fec_F_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_fec_F_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_fec_F_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_fec_O_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_fec_O_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_fec_O_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_fec_O_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_fec_all_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_fec_all_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_fec_all_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_non_solvated/rdf_pf6_F_vs_fec_all_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_easy/rdf_bn_N_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_easy/rdf_bn_N_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_easy/rdf_bn_N_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_easy/rdf_bn_N_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_easy/rdf_bn_all_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_easy/rdf_bn_all_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_easy/rdf_bn_all_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_easy/rdf_bn_all_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_easy/rdf_fec_F_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_easy/rdf_fec_F_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_easy/rdf_fec_F_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_easy/rdf_fec_F_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_easy/rdf_fec_O_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_easy/rdf_fec_O_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_easy/rdf_fec_O_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_easy/rdf_fec_O_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_easy/rdf_fec_all_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_easy/rdf_fec_all_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_easy/rdf_fec_all_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_easy/rdf_fec_all_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_easy/rdf_pf6_F_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_easy/rdf_pf6_F_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_easy/rdf_pf6_F_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_easy/rdf_pf6_F_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_easy/rdf_pf6_all_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_easy/rdf_pf6_all_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_easy/rdf_pf6_all_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_easy/rdf_pf6_all_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_easy/rdf_universe_all_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_easy/rdf_universe_all_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_easy/rdf_universe_all_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_easy/rdf_universe_all_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_hard/rdf_bn_N_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_hard/rdf_bn_N_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_hard/rdf_bn_N_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_hard/rdf_bn_N_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_hard/rdf_bn_all_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_hard/rdf_bn_all_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_hard/rdf_bn_all_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_hard/rdf_bn_all_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_hard/rdf_fec_F_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_hard/rdf_fec_F_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_hard/rdf_fec_F_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_hard/rdf_fec_F_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_hard/rdf_fec_O_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_hard/rdf_fec_O_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_hard/rdf_fec_O_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_hard/rdf_fec_O_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_hard/rdf_fec_all_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_hard/rdf_fec_all_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_hard/rdf_fec_all_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_hard/rdf_fec_all_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_hard/rdf_pf6_F_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_hard/rdf_pf6_F_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_hard/rdf_pf6_F_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_hard/rdf_pf6_F_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_hard/rdf_pf6_all_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_hard/rdf_pf6_all_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_hard/rdf_pf6_all_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_hard/rdf_pf6_all_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_hard/rdf_universe_all_bins.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_hard/rdf_universe_all_bins.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/data/rdf_vs_li_hard/rdf_universe_all_data.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MDAnalysis/solvation-analysis/fd378755a5c0b0a855ca247d5d374db8142358da/solvation_analysis/tests/data/rdf_vs_li_hard/rdf_universe_all_data.npz -------------------------------------------------------------------------------- /solvation_analysis/tests/datafiles.py: -------------------------------------------------------------------------------- 1 | from pkg_resources import resource_filename 2 | import pathlib 3 | from pathlib import Path 4 | 5 | bn_fec_data = resource_filename(__name__, "data/bn_fec_data/bn_fec.data") 6 | bn_fec_dcd_unwrap = resource_filename(__name__, "data/bn_fec_data/bn_fec_short_unwrap.dcd") 7 | bn_fec_dcd_wrap = resource_filename(__name__, "data/bn_fec_data/bn_fec_short_wrap.dcd") 8 | bn_fec_atom_types = resource_filename(__name__, "data/bn_fec_data/bn_fec_elements.csv") 9 | bn_fec_solv_df_large = resource_filename(__name__, "data/bn_fec_data/bn_solv_df_large.csv") 10 | ea_fec_dcd = resource_filename(__name__, "data/ea_fec_data/ea_fec.dcd") 11 | ea_fec_pdb = resource_filename(__name__, "data/ea_fec_data/ea_fec.pdb") 12 | eax_data = resource_filename(__name__, "data/eax_data/") 13 | iba_data = resource_filename(__name__, "data/iba_data/isobutyric_acid.pdb") 14 | iba_dcd = resource_filename(__name__, "data/iba_data/isobutyric_acid.dcd") 15 | 16 | test_dir = Path(__file__).parent 17 | data_dir = test_dir / "data" 18 | easy_rdf_dir = data_dir / "rdf_vs_li_easy" 19 | hard_rdf_dir = data_dir / "rdf_vs_li_hard" 20 | fail_rdf_dir = data_dir / "rdf_non_solvated" 21 | 22 | 23 | def generate_short_tag(filename): 24 | # generates an ad-hoc tag for the li-ion rdf data from the file name 25 | tag_list = filename.stem.split("_") 26 | rdf_tag = f"{tag_list[1]}_{tag_list[2]}" 27 | return rdf_tag 28 | 29 | 30 | easy_rdf_bins = { 31 | generate_short_tag(rdf_path): resource_filename( 32 | __name__, str(rdf_path.relative_to(test_dir)) 33 | ) 34 | for rdf_path in easy_rdf_dir.glob("*bins.npz") 35 | } 36 | 37 | easy_rdf_data = { 38 | generate_short_tag(rdf_path): resource_filename( 39 | __name__, str(rdf_path.relative_to(test_dir)) 40 | ) 41 | for rdf_path in easy_rdf_dir.glob("*data.npz") 42 | } 43 | 44 | hard_rdf_bins = { 45 | generate_short_tag(rdf_path): resource_filename(__name__, str(rdf_path.relative_to(test_dir))) 46 | for rdf_path in hard_rdf_dir.glob("*bins.npz") 47 | } 48 | 49 | hard_rdf_data = { 50 | generate_short_tag(rdf_path): resource_filename(__name__, str(rdf_path.relative_to(test_dir))) 51 | for rdf_path in hard_rdf_dir.glob("*data.npz") 52 | } 53 | 54 | 55 | def generate_long_tag(filename): 56 | # generates an ad-hoc tag for the non-solv rdf data from the file name 57 | tag_list = filename.stem.split("_") 58 | rdf_tag = f"{'_'.join(tag_list[1:3])}_{'_'.join(tag_list[4:6])}" 59 | return rdf_tag 60 | 61 | 62 | non_solv_rdf_bins = { 63 | generate_long_tag(rdf_path): resource_filename(__name__, str(rdf_path.relative_to(test_dir))) 64 | for rdf_path in fail_rdf_dir.glob("*bins.npz") 65 | } 66 | 67 | non_solv_rdf_data = { 68 | generate_long_tag(rdf_path): resource_filename(__name__, str(rdf_path.relative_to(test_dir))) 69 | for rdf_path in fail_rdf_dir.glob("*data.npz") 70 | } 71 | 72 | del resource_filename 73 | -------------------------------------------------------------------------------- /solvation_analysis/tests/test_coordination.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from solvation_analysis.coordination import Coordination 5 | 6 | 7 | def test_coordination_from_solute(run_solute): 8 | coordination = Coordination.from_solute(run_solute) 9 | assert len(coordination.coordination_numbers) == 3 10 | 11 | 12 | @pytest.mark.parametrize( 13 | "name, cn", 14 | [ 15 | ("fec", 0.25), 16 | ("bn", 4.33), 17 | ("pf6", 0.15), 18 | ], 19 | ) 20 | def test_coordination(name, cn, solvation_data, run_solute): 21 | coordination = Coordination.from_solute(run_solute) 22 | np.testing.assert_allclose(cn, coordination.coordination_numbers[name], atol=0.05) 23 | assert len(coordination.coordination_numbers_by_frame) == 3 24 | 25 | 26 | @pytest.mark.parametrize( 27 | "name, atom_type, fraction", 28 | [ 29 | ("fec", '19', 0.008), 30 | ("bn", '5', 0.9976), 31 | ("pf6", '21', 1.000), 32 | ], 33 | ) 34 | def test_coordinating_atoms(name, atom_type, fraction, solvation_data, run_solute): 35 | coordination = Coordination.from_solute(run_solute) 36 | calculated_fraction = coordination._coordinating_atoms.loc[(name, atom_type)] 37 | np.testing.assert_allclose(fraction, calculated_fraction, atol=0.05) 38 | 39 | 40 | @pytest.mark.parametrize( 41 | "name, coord", 42 | [ 43 | ("fec", 0.15), 44 | ("bn", 1.64), 45 | ("pf6", 0.38), 46 | ], 47 | ) 48 | def test_coordination_relative_to_random(name, coord, solvation_data, run_solute): 49 | atoms = run_solute.u.atoms 50 | coordination = Coordination(solvation_data, 10, 49, run_solute.solvent_counts, atoms) 51 | np.testing.assert_allclose(coord, coordination.coordination_vs_random[name], atol=0.05) 52 | assert len(coordination.coordination_numbers_by_frame) == 3 53 | -------------------------------------------------------------------------------- /solvation_analysis/tests/test_networking.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from solvation_analysis.networking import Networking 5 | 6 | from solvation_analysis._column_names import * 7 | 8 | 9 | def test_networking_from_solute(run_solute): 10 | networking = Networking.from_solute(run_solute, 'pf6') 11 | assert len(networking.network_df) == 128 12 | 13 | 14 | @pytest.mark.parametrize( 15 | "status, fraction", 16 | [ 17 | (ISOLATED, 0.876), 18 | (PAIRED, 0.112), 19 | (NETWORKED, 0.012), 20 | ], 21 | ) 22 | def test_get_cluster_res_ix(status, fraction, networking): 23 | np.testing.assert_almost_equal(networking.solute_status[status], fraction, 3) 24 | 25 | 26 | @pytest.mark.parametrize( 27 | "network_ix, frame, n_res", 28 | [ 29 | (0, 0, 3), 30 | (5, 1, 2), 31 | (1, 8, 3), 32 | ], 33 | ) 34 | def test_get_network_res_ix(network_ix, frame, n_res, networking): 35 | res_ix = networking.get_network_res_ix(network_ix, frame) 36 | assert len(res_ix) == n_res 37 | -------------------------------------------------------------------------------- /solvation_analysis/tests/test_pairing.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import pytest 4 | 5 | from solvation_analysis.pairing import Pairing 6 | 7 | 8 | def test_pairing_from_solute(run_solute): 9 | pairing = Pairing.from_solute(run_solute) 10 | assert len(pairing.solvent_pairing) == 3 11 | assert len(pairing.fraction_free_solvents) == 3 12 | 13 | 14 | @pytest.mark.parametrize( 15 | "name, fraction", 16 | [ 17 | ("fec", 0.21), 18 | ("bn", 1.0), 19 | ("pf6", 0.14), 20 | ], 21 | ) 22 | def test_pairing_dict(name, fraction, solvation_data): 23 | pairing = Pairing(solvation_data, 10, 49, {'fec': 237, 'bn': 363, 'pf6': 49}) 24 | np.testing.assert_allclose(fraction, pairing.solvent_pairing[name], atol=0.05) 25 | assert len(pairing.pairing_by_frame) == 3 26 | 27 | 28 | @pytest.mark.parametrize( 29 | "name, fraction", 30 | [ 31 | ("fec", 0.947), 32 | ("bn", 0.415), 33 | ("pf6", 0.853), 34 | ], 35 | ) 36 | def test_pairing_participating(name, fraction, solvation_data): 37 | pairing = Pairing(solvation_data, 10, 49, {'fec': 237, 'bn': 363, 'pf6': 49}) 38 | np.testing.assert_allclose(fraction, pairing.fraction_free_solvents[name], atol=0.05) 39 | 40 | 41 | @pytest.mark.parametrize( 42 | "name, diluent_fraction", 43 | [ 44 | ("fec", 0.54), 45 | ("bn", 0.36), 46 | ("pf6", 0.10), 47 | ], 48 | ) 49 | def test_diluent_composition(name, diluent_fraction, solvation_data): 50 | pairing = Pairing(solvation_data, 10, 49, {'fec': 237, 'bn': 363, 'pf6': 49}) 51 | np.testing.assert_allclose(diluent_fraction, pairing.diluent_composition[name], atol=0.05) 52 | np.testing.assert_allclose(sum(pairing.diluent_composition.values()), 1, atol=0.05) 53 | 54 | -------------------------------------------------------------------------------- /solvation_analysis/tests/test_plotting.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from solvation_analysis.plotting import ( 3 | plot_network_size_histogram, 4 | plot_shell_composition_by_size, 5 | plot_co_occurrence, 6 | plot_speciation, 7 | plot_rdfs, 8 | _compare_function_generator, 9 | compare_free_solvents, 10 | compare_pairing, 11 | compare_coordination_numbers, 12 | compare_residence_times_cutoff, 13 | compare_residence_times_fit, 14 | compare_diluent, 15 | compare_networking, 16 | ) 17 | 18 | from solvation_analysis.networking import Networking 19 | from solvation_analysis.residence import Residence 20 | from solvation_analysis.speciation import Speciation 21 | 22 | 23 | def test_plot_network_size_histogram(run_solute): 24 | run_solute.networking = Networking.from_solute(run_solute, "pf6") 25 | plot_network_size_histogram(run_solute) 26 | plot_network_size_histogram(run_solute.networking) 27 | assert True 28 | 29 | 30 | def test_plot_shell_size_histogram(run_solute): 31 | plot_shell_composition_by_size(run_solute) 32 | plot_shell_composition_by_size(run_solute.speciation) 33 | assert True 34 | 35 | 36 | def test_plot_speciation(run_solute): 37 | plot_speciation(run_solute) 38 | plot_speciation(run_solute.speciation) 39 | assert True 40 | 41 | 42 | def test_plot_rdfs(run_solute, iba_small_solute): 43 | plot_rdfs(iba_small_solute) 44 | plot_rdfs(iba_small_solute, merge_on_x=True) 45 | plot_rdfs(iba_small_solute, merge_on_y=True) 46 | plot_rdfs(iba_small_solute, merge_on_x=True, merge_on_y=True) 47 | plot_rdfs(iba_small_solute, x_axis_solute=True) 48 | plot_rdfs(iba_small_solute, x_axis_solute=True, merge_on_y=True) 49 | plot_rdfs(iba_small_solute, x_axis_solute=True, merge_on_x=True) 50 | plot_rdfs(iba_small_solute, x_axis_solute=True, merge_on_x=True, merge_on_y=True) 51 | plot_rdfs(run_solute) 52 | 53 | 54 | # compare_solvent_dicts tests 55 | def test_compare_solvent_dicts_rename_exception(eax_solutes): 56 | # invalid solvents_to_plot because solvent names were already renamed to the generic "EAx" form 57 | # solvents_to_plot here references the former names of solvents, which is wrong 58 | # this test should handle an exception 59 | with pytest.raises(Exception): 60 | compare_pairing( 61 | eax_solutes, 62 | rename_solvent_dict={ 63 | "ea": "EAx", 64 | "fea": "EAx", 65 | "eaf": "EAx", 66 | "feaf": "EAx", 67 | }, 68 | solvents_to_plot=["pf6", "fec", "ea", "fea", "eaf", "feaf"], 69 | x_label="Species", 70 | y_label="Pairing", 71 | title="Graph", 72 | ) 73 | 74 | 75 | def test_compare_solvent_dicts_sensitivity(eax_solutes): 76 | # solvent names are case-sensitive, so names in solvents_to_plot and rename_solvent_dict should be consistent 77 | # this test should handle an exception 78 | with pytest.raises(Exception): 79 | compare_pairing( 80 | eax_solutes, 81 | rename_solvent_dict={ 82 | "EA": "EAx", 83 | "fEA": "EAx", 84 | "EAf": "EAx", 85 | "fEAf": "EAx", 86 | }, 87 | solvents_to_plot=["PF6", "FEC", "EAx"], 88 | x_label="Species", 89 | y_label="Pairing", 90 | title="Graph", 91 | ) 92 | 93 | 94 | def test_compare_networking(eax_solutes): 95 | compare_networking(eax_solutes) 96 | 97 | 98 | # compare_pairing tests 99 | def test_compare_pairing_default_eax(eax_solutes): 100 | # call compare_pairing with only one required argument 101 | # also tests how the code handles eax systems 102 | fig = compare_pairing(eax_solutes) 103 | assert len(fig.data) == 6 104 | 105 | 106 | def test_compare_pairing_case1(eax_solutes): 107 | # solvents_to_plot on x axis, each bar is a solute 108 | fig = compare_pairing( 109 | eax_solutes, 110 | solvents_to_plot=["fec", "pf6"], 111 | x_label="Species", 112 | y_label="Pairing", 113 | title="Bar Graph of Solvent Pairing", 114 | ) 115 | assert len(fig.data) == 2 116 | 117 | 118 | def test_compare_pairing_case2(eax_solutes): 119 | # solutes on x axis, each bar is an element of solvents_to_plot 120 | fig = compare_pairing( 121 | eax_solutes, 122 | solvents_to_plot=["pf6", "fec"], 123 | x_label="Solute", 124 | y_label="Pairing", 125 | title="Bar Graph of Solvent Pairing", 126 | x_axis_solute=True, 127 | ) 128 | assert len(fig.data) == 2 129 | for bar in fig.data: 130 | assert set(bar.x) == {"feaf", "eaf", "fea", "ea"} 131 | 132 | 133 | def test_compare_pairing_case3(eax_solutes): 134 | # solvents_to_plot on x axis, each line is a solute 135 | fig = compare_pairing( 136 | eax_solutes, 137 | solvents_to_plot=["pf6", "fec"], 138 | x_label="Solute", 139 | y_label="Pairing", 140 | title="Line Graph of Solvent Pairing", 141 | series=True, 142 | ) 143 | assert len(fig.data) == 2 144 | 145 | 146 | def test_compare_pairing_case4(eax_solutes): 147 | # solutes on x axis, each line is an element of solvents_to_plot 148 | fig = compare_pairing( 149 | eax_solutes, 150 | solvents_to_plot=["pf6", "fec"], 151 | x_label="Solute", 152 | y_label="Pairing", 153 | title="Line Graph of Solvent Pairing", 154 | x_axis_solute=True, 155 | series=True, 156 | ) 157 | assert len(fig.data) == 2 158 | for line in fig.data: 159 | assert set(line.x) == {"feaf", "eaf", "fea", "ea"} 160 | 161 | 162 | def test_compare_pairing_switch_solvents_to_plot_order(eax_solutes): 163 | # same test as test_compare_pairing_case4, except order for solvents_to_plot is switched 164 | fig = compare_pairing( 165 | eax_solutes, 166 | solvents_to_plot=["fec", "pf6"], 167 | x_label="Solute", 168 | y_label="Pairing", 169 | title="Line Graph of Solvent Pairing", 170 | x_axis_solute=True, 171 | series=True, 172 | ) 173 | assert len(fig.data) == 2 174 | for line in fig.data: 175 | assert set(line.x) == {"feaf", "eaf", "fea", "ea"} 176 | 177 | 178 | def test_compare_pairing_rename_solvent_dict(eax_solutes): 179 | # rename solvent names into the generic "EAx" form 180 | fig = compare_pairing( 181 | eax_solutes, 182 | rename_solvent_dict={"ea": "EAx", "fea": "EAx", "eaf": "EAx", "feaf": "EAx"}, 183 | solvents_to_plot=["pf6", "fec", "EAx"], 184 | x_label="Species", 185 | y_label="Pairing", 186 | title="Bar Graph of Solvent Pairing", 187 | ) 188 | assert len(fig.data) == 3 189 | 190 | 191 | def test_compare_free_solvents(eax_solutes): 192 | compare_free_solvents(eax_solutes, solvents_to_plot=["fec", "pf6"]) 193 | 194 | 195 | def test_compare_diluent(eax_solutes): 196 | compare_diluent(eax_solutes, solvents_to_plot=["fec", "pf6"]) 197 | 198 | 199 | # compare_coordination_numbers tests 200 | def test_compare_coordination_numbers_default_eax(eax_solutes): 201 | # call compare_coordination_numbers with only one required argument 202 | # also tests how the code handles eax systems 203 | fig = compare_coordination_numbers(eax_solutes) 204 | assert len(fig.data) == 6 205 | 206 | 207 | def test_compare_coordination_numbers_solute_four_cases(eax_solutes): 208 | fig = compare_coordination_numbers(eax_solutes, x_axis_solute=True) 209 | assert len(fig.data) == 6 210 | 211 | fig = compare_coordination_numbers(eax_solutes, x_axis_solute=True, series=True) 212 | assert len(fig.data) == 6 213 | 214 | rename = { 215 | "ea": "EAx", 216 | "fea": "EAx", 217 | "eaf": "EAx", 218 | "feaf": "EAx", 219 | } 220 | fig = compare_coordination_numbers( 221 | eax_solutes, x_axis_solute=True, rename_solvent_dict=rename 222 | ) 223 | assert len(fig.data) == 3 224 | 225 | fig = compare_coordination_numbers( 226 | eax_solutes, 227 | x_axis_solute=True, 228 | series=True, 229 | rename_solvent_dict=rename, 230 | ) 231 | assert len(fig.data) == 3 232 | 233 | fig = compare_coordination_numbers( 234 | eax_solutes, 235 | x_axis_solute=True, 236 | rename_solvent_dict=rename, 237 | series=True, 238 | solvents_to_plot=["EAx", "pf6"], 239 | ) 240 | assert len(fig.data) == 2 241 | 242 | 243 | def test_compare_coordination_numbers_case1(eax_solutes): 244 | # solvents_to_plot on x axis, each bar is a solute 245 | fig = compare_coordination_numbers( 246 | eax_solutes, 247 | solvents_to_plot=["fec", "pf6"], 248 | x_label="Species", 249 | y_label="Coordination", 250 | title="Bar Graph of Coordination Numbers", 251 | ) 252 | assert len(fig.data) == 2 253 | 254 | 255 | def test_compare_coordination_numbers_case2(eax_solutes): 256 | # solutes on x axis, each bar is an element of solvents_to_plot 257 | fig = compare_coordination_numbers( 258 | eax_solutes, 259 | solvents_to_plot=["pf6", "fec"], 260 | x_label="solute", 261 | y_label="Coordination", 262 | title="Bar Graph of Coordination Numbers", 263 | x_axis_solute=True, 264 | ) 265 | assert len(fig.data) == 2 266 | for bar in fig.data: 267 | assert set(bar.x) == {"feaf", "eaf", "fea", "ea"} 268 | 269 | 270 | def test_compare_coordination_numbers_case3(eax_solutes): 271 | # solvents_to_plot on x axis, each line is a solute 272 | fig = compare_coordination_numbers( 273 | eax_solutes, 274 | solvents_to_plot=["pf6", "fec"], 275 | x_label="solute", 276 | y_label="Coordination", 277 | title="Line Graph of Coordination Numbers", 278 | series=True, 279 | ) 280 | assert len(fig.data) == 2 281 | 282 | 283 | def test_compare_coordination_numbers_case4(eax_solutes): 284 | # solutes on x axis, each line is an element of solvents_to_plot 285 | fig = compare_coordination_numbers( 286 | eax_solutes, 287 | solvents_to_plot=["pf6", "fec"], 288 | x_label="solute", 289 | y_label="Coordination", 290 | title="Line Graph of Coordination Numbers", 291 | x_axis_solute=True, 292 | series=True, 293 | ) 294 | assert len(fig.data) == 2 295 | for line in fig.data: 296 | assert set(line.x) == {"feaf", "eaf", "fea", "ea"} 297 | 298 | 299 | # compare_residence_times tests 300 | def test_compare_residence_times(eax_solutes): 301 | # this test should handle an exception relating to the acceptable arguments for res_type 302 | for solute in eax_solutes.values(): 303 | residence = Residence.from_solute(solute) 304 | solute.residence = residence 305 | 306 | # with pytest.raises(Exception): 307 | compare_residence_times_cutoff(eax_solutes, solvents_to_plot=["fec", "pf6"]) 308 | compare_residence_times_fit(eax_solutes, solvents_to_plot=["fec", "pf6"]) 309 | 310 | 311 | def test_compare_generic(eax_solutes): 312 | compare = _compare_function_generator( 313 | "pairing", "solvent_pairing", "hello", "This is a function" 314 | ) 315 | fig = compare( 316 | eax_solutes, 317 | rename_solvent_dict={"ea": "EAx", "fea": "EAx", "eaf": "EAx", "feaf": "EAx"}, 318 | solvents_to_plot=["pf6", "fec", "EAx"], 319 | x_label="Species", 320 | y_label="Pairing", 321 | title="Bar Graph of Solvent Pairing", 322 | ) 323 | assert len(fig.data) == 3 324 | 325 | 326 | def test_plot_co_occurrence(solvation_data): 327 | speciation = Speciation(solvation_data, 10, 49) 328 | plot_co_occurrence(speciation) 329 | -------------------------------------------------------------------------------- /solvation_analysis/tests/test_rdf_parser.py: -------------------------------------------------------------------------------- 1 | import MDAnalysis as mda 2 | import numpy as np 3 | 4 | import matplotlib.pyplot as plt 5 | import pytest 6 | from solvation_analysis.rdf_parser import ( 7 | identify_minima, 8 | interpolate_rdf, 9 | plot_interpolation_fit, 10 | plot_scipy_find_peaks_troughs, 11 | identify_cutoff_poly, 12 | identify_cutoff_scipy, 13 | good_cutoff, 14 | ) 15 | from scipy.interpolate import UnivariateSpline 16 | import scipy 17 | 18 | rdf_minima = [ 19 | ("fec_F", []), 20 | ("fec_O", [2.9]), 21 | ("fec_all", [2.9]), 22 | ("bn_all", []), 23 | ("bn_N", []), 24 | ("pf6_all", []), 25 | ("pf6_F", []), 26 | ] 27 | 28 | 29 | @pytest.mark.parametrize( 30 | "rdf_tag", 31 | ["fec_F", "fec_O", "fec_all", "bn_all", "bn_N", "pf6_all", "pf6_F"], 32 | ) 33 | def test_plot_interpolation_fit(rdf_tag, rdf_bins_and_data_hard): 34 | """This is essentially a visually confirmed regression test to ensure 35 | behavior is approximately correct.""" 36 | bins, rdf = rdf_bins_and_data_hard[rdf_tag] 37 | fig, ax = plot_interpolation_fit(bins, rdf) 38 | ax.set_title(f"Interpolation of RDF: {rdf_tag}") 39 | # plt.show() # this should only be uncommented for local testing 40 | 41 | 42 | @pytest.mark.parametrize( 43 | "rdf_tag", ["fec_F", "fec_O", "fec_all", "bn_all", "bn_N", "pf6_all", "pf6_F"] 44 | ) 45 | def test_interpolate_rdf(rdf_tag, rdf_bins_and_data_easy): 46 | # this is difficult to test so only very basic checks are implemented 47 | bins, rdf = rdf_bins_and_data_easy[rdf_tag] 48 | f, bounds = interpolate_rdf(bins, rdf) 49 | assert bounds[0] > 0 50 | assert bounds[1] <= 5 51 | assert isinstance(f, scipy.interpolate.fitpack2.InterpolatedUnivariateSpline) 52 | 53 | 54 | @pytest.mark.parametrize( 55 | "rdf_tag, test_min", 56 | [ 57 | ("fec_O", 3.30), 58 | ("fec_all", 2.74), 59 | ("bn_all", 2.64), 60 | ("pf6_all", 2.77), 61 | ("pf6_F", 3.03), 62 | ], # the above values are not real 63 | ) 64 | def test_identify_minima_first_min(rdf_tag, test_min, rdf_bins_and_data_easy): 65 | bins, rdf = rdf_bins_and_data_easy[rdf_tag] 66 | f, bounds = interpolate_rdf(bins, rdf) 67 | cr_pts, cr_vals = identify_minima(f) 68 | min = cr_pts[1] 69 | np.testing.assert_allclose(test_min, min, atol=0.01) 70 | 71 | 72 | @pytest.mark.parametrize( 73 | "cutoff_region, cr_pts, cr_vals, expected", 74 | [ 75 | ((1, 4), [2], [2], False), 76 | ((1, 4), [2, 3], [1.0, 1.1], False), 77 | ((1, 1.5), [2, 3], [2, 1], False), 78 | ((1.5, 4), [2, 3], [2, 1], True), 79 | ], 80 | ) 81 | def test_good_cutoff(cutoff_region, cr_pts, cr_vals, expected): 82 | assert good_cutoff(cutoff_region, cr_pts, cr_vals) == expected 83 | 84 | 85 | @pytest.mark.parametrize( 86 | "rdf_tag, cutoff", 87 | [ 88 | ("fec_F", np.NaN), 89 | ("fec_O", 3.30), 90 | ("fec_all", 2.74), 91 | ("bn_all", 2.64), 92 | ("bn_N", 3.42), 93 | ("pf6_all", 2.77), 94 | ("pf6_F", 3.03), 95 | ], # the above values are not real 96 | ) 97 | def test_identify_cutoff_poly_easy( 98 | rdf_tag, cutoff, rdf_bins_and_data_easy, rdf_bins_and_data_hard 99 | ): 100 | bins, rdf = rdf_bins_and_data_easy[rdf_tag] 101 | np.testing.assert_allclose( 102 | identify_cutoff_poly(bins, rdf, failure_behavior="warn"), 103 | cutoff, 104 | atol=0.01, 105 | equal_nan=True, 106 | ) 107 | 108 | @pytest.mark.parametrize( 109 | "rdf_tag, cutoff", 110 | [ 111 | ("fec_F", np.NaN), 112 | ("fec_O", 3.30), 113 | ("fec_all", 2.74), 114 | ("bn_all", 2.64), 115 | ("bn_N", 3.5), 116 | ("pf6_all", 2.77), 117 | ("pf6_F", 3.03), 118 | ], # the above values are not real 119 | ) 120 | def test_identify_cutoff_scipy_easy( 121 | rdf_tag, cutoff, rdf_bins_and_data_easy, rdf_bins_and_data_hard 122 | ): 123 | bins, rdf = rdf_bins_and_data_easy[rdf_tag] 124 | np.testing.assert_allclose( 125 | identify_cutoff_scipy(bins, rdf, failure_behavior="warn", default=np.NaN), 126 | cutoff, 127 | atol=0.2, 128 | equal_nan=True, 129 | ) 130 | 131 | @pytest.mark.parametrize("rdf_tag", ["fec_F", "fec_all", "bn_all", "pf6_all", "pf6_F"]) 132 | def test_identify_cutoff_poly_hard( 133 | rdf_tag, rdf_bins_and_data_easy, rdf_bins_and_data_hard 134 | ): 135 | bins_ez, rdf_ez = rdf_bins_and_data_easy[rdf_tag] 136 | bins_hd, rdf_hd = rdf_bins_and_data_hard[rdf_tag] 137 | np.testing.assert_allclose( 138 | identify_cutoff_poly(bins_hd, rdf_hd, failure_behavior="warn"), 139 | identify_cutoff_poly(bins_ez, rdf_ez, failure_behavior="warn"), 140 | atol=0.1, 141 | equal_nan=True, 142 | ) 143 | 144 | @pytest.mark.parametrize("rdf_tag", ["fec_F", "fec_all", "bn_all", "pf6_all", "pf6_F"]) 145 | def test_identify_scipy_hard( 146 | rdf_tag, rdf_bins_and_data_easy, rdf_bins_and_data_hard 147 | ): 148 | bins_ez, rdf_ez = rdf_bins_and_data_easy[rdf_tag] 149 | bins_hd, rdf_hd = rdf_bins_and_data_hard[rdf_tag] 150 | np.testing.assert_allclose( 151 | identify_cutoff_scipy(bins_hd, rdf_hd, failure_behavior="warn", default=np.NaN), 152 | identify_cutoff_scipy(bins_ez, rdf_ez, failure_behavior="warn", default=np.NaN), 153 | atol=0.2, 154 | equal_nan=True, 155 | ) 156 | 157 | 158 | @pytest.mark.parametrize( 159 | "rdf_tag", 160 | ["fec_F", "fec_O", "fec_all", "bn_all", "bn_N", "pf6_all", "pf6_F"], 161 | ) 162 | def test_plot_scripy_find_peaks_troughs(rdf_tag, rdf_bins_and_data_hard): 163 | """This is essentially a visually confirmed regression test to ensure 164 | behavior is approximately correct.""" 165 | bins, rdf = rdf_bins_and_data_hard[rdf_tag] 166 | fig, ax = plot_scipy_find_peaks_troughs(bins, rdf) 167 | ax.set_title(f"Find peaks of RDF: {rdf_tag}") 168 | # plt.show() # leave in, uncomment for debugging 169 | 170 | 171 | def test_identify_cutoff_scipy_pf6(run_solute): 172 | pf6_bins, pf6_data = run_solute.rdf_data["solute_0"]["pf6"] 173 | radius = identify_cutoff_scipy(pf6_bins, pf6_data, failure_behavior="warn"), 174 | np.testing.assert_allclose(radius, 2.8, atol=0.2) 175 | 176 | 177 | @pytest.mark.parametrize( 178 | "rdf_tag", 179 | [ 180 | "fec_F_bn_N", 181 | "fec_O_bn_all", 182 | "fec_F_bn_all", 183 | "fec_F_pf6_all", 184 | "fec_F_pf6_F", 185 | "fec_O_pf6_F", 186 | "pf6_F_bn_N", 187 | "bn_N_fec_F", 188 | "bn_N_fec_all", 189 | "pf6_F_fec_all", 190 | "pf6_F_bn_all", 191 | "bn_N_pf6_F", 192 | "bn_N_pf6_all", 193 | "fec_O_bn_N", 194 | "fec_O_pf6_all", 195 | "pf6_F_fec_F", 196 | "bn_N_fec_O", 197 | "pf6_F_fec_O", 198 | ], 199 | ) 200 | def test_identify_cutoff_non_solv(rdf_tag, rdf_bins_and_data_non_solv): 201 | bins, rdf = rdf_bins_and_data_non_solv[rdf_tag] 202 | np.testing.assert_allclose( 203 | identify_cutoff_poly(bins, rdf, failure_behavior="warn"), 204 | np.NaN, 205 | equal_nan=True, 206 | ) 207 | np.testing.assert_allclose( 208 | identify_cutoff_scipy(bins, rdf, failure_behavior="warn", default=np.NaN), 209 | np.NaN, 210 | equal_nan=True, 211 | ) 212 | -------------------------------------------------------------------------------- /solvation_analysis/tests/test_residence.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from solvation_analysis.residence import Residence 5 | 6 | 7 | def test_residence_from_solute(run_solute): 8 | residence = Residence.from_solute(run_solute) 9 | assert len(residence.residence_times_cutoff) == 3 10 | assert len(residence.residence_times_fit) == 3 11 | 12 | 13 | @pytest.mark.parametrize( 14 | "name, res_time", 15 | [ 16 | ("fec", 10), 17 | ("bn", 80), 18 | ("pf6", np.nan), 19 | ], 20 | ) 21 | def test_residence_times(name, res_time, residence): 22 | np.testing.assert_almost_equal(residence.residence_times_cutoff[name], res_time, 3) 23 | 24 | 25 | @pytest.mark.parametrize("name", ["fec", "bn", "pf6"]) 26 | def test_plot_auto_covariance(name, residence): 27 | residence.plot_auto_covariance(name) 28 | 29 | 30 | def test_residence_time_warning(solvation_data_sparse): 31 | # we step through the dataframe to speed up the tests 32 | with pytest.warns( 33 | UserWarning, match="the autocovariance for pf6 does not converge" 34 | ): 35 | Residence(solvation_data_sparse, step=10) 36 | -------------------------------------------------------------------------------- /solvation_analysis/tests/test_selection.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit and regression test for the solvation_analysis package. 3 | """ 4 | 5 | 6 | # Import package, test suite, and other packages as needed 7 | import MDAnalysis as mda 8 | import numpy as np 9 | import pytest 10 | 11 | from solvation_analysis._utils import get_atom_group, get_closest_n_mol, get_radial_shell 12 | 13 | 14 | def test_get_atom_group(u_real_named): 15 | # test that atoms, residues, and groups are being correctly converted to AtomGroup 16 | u = u_real_named 17 | res_group = u.residues[1:5] 18 | atom_group = u.atoms[1:5] 19 | res = u.residues[1] 20 | atom = u.atoms[1] 21 | groups = [res_group, atom_group, res, atom] 22 | for group in groups: 23 | assert isinstance(get_atom_group(group), mda.core.groups.AtomGroup) 24 | 25 | 26 | @pytest.mark.parametrize("shell_size", [2, 3, 4, 5, 6, 7]) 27 | def test_get_closest_n_mol_correct_number_real(shell_size, u_real, atom_groups): 28 | # test that the correct number of residues are being returned, on real system 29 | test_li = atom_groups["li"][0] 30 | shell = get_closest_n_mol(test_li, n_mol=shell_size) 31 | assert len(shell.residues) == shell_size + 1 32 | 33 | 34 | @pytest.mark.parametrize("test_ix", [0, 3, 10, 16, 42]) 35 | def test_get_closest_n_mol_correct_number_grid(test_ix, u_grid_1): 36 | # test that the correct number of residues are being returned, on grid system 37 | test_atom = u_grid_1.atoms[test_ix] 38 | atoms = get_closest_n_mol(test_atom, n_mol=5) 39 | assert len(atoms) == 6 40 | 41 | 42 | @pytest.mark.parametrize( 43 | "center_ix, expected_ix", 44 | [ 45 | (10, [4, 9, 10, 11, 16, 46]), 46 | (16, [10, 15, 16, 17, 22, 52]), 47 | (42, [6, 36, 42, 43, 48, 78]), 48 | ], 49 | ) 50 | def test_get_closest_n_mol_correct_ix(center_ix, expected_ix, u_grid_1): 51 | # test that the correct atoms are being returned, on grid system 52 | test_atom = u_grid_1.atoms[center_ix] # only atoms on side of box 53 | shell_ix = get_closest_n_mol(test_atom, n_mol=5).resindices 54 | np.testing.assert_array_equal(shell_ix, expected_ix) 55 | 56 | 57 | @pytest.mark.parametrize("radius", [0, 1, 2, 3, 4, 5]) 58 | def test_get_closest_n_mol_radii_invariance(radius, u_real, atom_groups): 59 | # test that the return_radii does not effect behavior, on real system 60 | test_li = atom_groups["li"][0] 61 | default_shell, default_resix, default_radii = get_closest_n_mol( 62 | test_li, 5, return_ordered_resix=True, return_radii=True 63 | ) 64 | shell, resix, radii = get_closest_n_mol( 65 | test_li, 5, guess_radius=radius, return_ordered_resix=True, return_radii=True 66 | ) 67 | assert shell == default_shell 68 | np.testing.assert_allclose(resix, default_resix) 69 | np.testing.assert_allclose(radii, default_radii) 70 | 71 | 72 | @pytest.mark.parametrize( 73 | "distance, expected_sizes", 74 | [(0.95, [1, 1, 1, 1]), (1.05, [4, 5, 6, 7]), (1.45, [7, 10, 14, 19])], 75 | ) 76 | def test_get_radial_shell_correct_number_grid(distance, expected_sizes, u_grid_1): 77 | # test that the correct shell sizes are being returned, on grid system 78 | test_atoms = u_grid_1.atoms[[0, 3, 10, 44]] # corner, edge, side, center 79 | shell_sizes = [len(get_radial_shell(atom, distance)) for atom in test_atoms] 80 | np.testing.assert_allclose(expected_sizes, shell_sizes) 81 | 82 | 83 | @pytest.mark.parametrize( 84 | "radius, shell_size", [(2, 1), (3, 59), (4, 81), (5, 123), (6, 142), (7, 191)] 85 | ) 86 | def test_get_radial_shell_correct_number_real(radius, shell_size, u_real, atom_groups): 87 | # test that the correct sizes are being returned, on real system 88 | test_li = atom_groups["li"][0] 89 | assert shell_size == len(get_radial_shell(test_li, radius=radius)) 90 | assert len(get_radial_shell(test_li, radius=100)) == len(u_real.atoms) 91 | 92 | 93 | @pytest.mark.parametrize( 94 | "center_ix, expected_ix", 95 | [ 96 | (10, [4, 9, 10, 11, 16, 46]), 97 | (16, [10, 15, 16, 17, 22, 52]), 98 | (42, [6, 36, 42, 43, 48, 78]), 99 | ], 100 | ) 101 | def test_get_radial_shell_correct_ix_grid(center_ix, expected_ix, u_grid_1): 102 | # test that the correct ix are being returned, on grid system 103 | test_atom = u_grid_1.atoms[center_ix] 104 | shell_ix = get_radial_shell(test_atom, 1.05).resindices 105 | np.testing.assert_array_equal(shell_ix, expected_ix) 106 | -------------------------------------------------------------------------------- /solvation_analysis/tests/test_solute.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | 3 | import pytest 4 | from solvation_analysis.solute import Solute 5 | import numpy as np 6 | from MDAnalysis import Universe 7 | 8 | 9 | def test_instantiate_solute_from_atoms(pre_solute): 10 | # these check basic properties of the instantiation 11 | assert len(pre_solute.radii) == 3 12 | assert callable(pre_solute.kernel) 13 | assert pre_solute.solute_atoms.n_residues == 49 14 | assert pre_solute.solvents["pf6"].n_residues == 49 15 | assert pre_solute.solvents["fec"].n_residues == 237 16 | assert pre_solute.solvents["bn"].n_residues == 363 17 | 18 | 19 | def test_init_fail(atom_groups): 20 | with pytest.raises(RuntimeError): 21 | Solute(atom_groups["li"], {"pf6": atom_groups["pf6"]}) 22 | 23 | 24 | def test_networking_instantiation_error(atom_groups): 25 | li = atom_groups["li"] 26 | pf6 = atom_groups["pf6"] 27 | bn = atom_groups["bn"] 28 | fec = atom_groups["fec"] 29 | with pytest.raises(Exception): 30 | Solute.from_atoms( 31 | li, {"pf6": pf6, "bn": bn, "fec": fec}, analysis_classes=["networking"] 32 | ) 33 | 34 | 35 | def test_plot_solvation_distance(rdf_bins_and_data_easy): 36 | bins, data = rdf_bins_and_data_easy["pf6_all"] 37 | Solute._plot_solvation_radius(bins, data, 2) 38 | 39 | 40 | def test_radii_finding(run_solute): 41 | # checks that the solvation radii are plotted 42 | assert len(run_solute.radii) == 3 43 | assert len(run_solute.rdf_data["solute_0"]) == 3 44 | # checks that the identified solvation radii are approximately correct 45 | assert 2 < run_solute.radii["pf6"] < 3 46 | assert 2 < run_solute.radii["fec"] < 3 47 | assert 2 < run_solute.radii["bn"] < 3 48 | 49 | 50 | def test_run(pre_solute_mutable): 51 | # checks that run is run correctly 52 | pre_solute_mutable.run(step=1) 53 | assert len(pre_solute_mutable._solvation_frames) == 10 54 | assert len(pre_solute_mutable._solvation_frames[0]) == 228 55 | assert len(pre_solute_mutable.solvation_data) == 2312 56 | 57 | 58 | def test_run_w_all(pre_solute_mutable): 59 | # checks that run is run correctly 60 | pre_solute_mutable.analysis_classes = [ 61 | "pairing", 62 | "coordination", 63 | "speciation", 64 | "residence", 65 | "networking", 66 | ] 67 | pre_solute_mutable.networking_solvents = "pf6" 68 | pre_solute_mutable.run(step=1) 69 | assert len(pre_solute_mutable._solvation_frames) == 10 70 | assert len(pre_solute_mutable._solvation_frames[0]) == 228 71 | assert len(pre_solute_mutable.solvation_data) == 2312 72 | 73 | 74 | @pytest.mark.parametrize( 75 | "solute_index, radius, frame, expected_res_ids", 76 | [ 77 | (1, 3, 5, [46, 100, 171, 255, 325, 521, 650]), 78 | (2, 3, 6, [13, 59, 177, 264, 314, 651]), 79 | (40, 3.5, 0, [101, 126, 127, 360, 368, 305, 689]), 80 | ], 81 | ) 82 | def test_radial_shell(solute_index, radius, frame, expected_res_ids, run_solute): 83 | run_solute.u.trajectory[frame] 84 | shell = run_solute.radial_shell(solute_index, radius) 85 | assert set(shell.resindices) == set(expected_res_ids) 86 | 87 | 88 | @pytest.mark.parametrize( 89 | "solute_index, n_mol, frame, expected_res_ids", 90 | [ 91 | (6741, 4, 5, [46, 100, 171, 255, 650]), 92 | (6749, 5, 6, [13, 59, 177, 264, 314, 651]), 93 | (7053, 6, 0, [101, 126, 127, 360, 368, 305, 689]), 94 | ], 95 | ) 96 | def test_closest_n_mol(solute_index, n_mol, frame, expected_res_ids, run_solute): 97 | run_solute.u.trajectory[frame] 98 | shell = run_solute.get_closest_n_mol(solute_index, n_mol) 99 | assert set(shell.resindices) == set(expected_res_ids) 100 | 101 | 102 | @pytest.mark.parametrize( 103 | "solute_index, step, expected_res_ids", 104 | [ 105 | (650, 5, [46, 100, 171, 255, 650]), 106 | (651, 6, [13, 59, 177, 264, 314, 651]), 107 | (689, 0, [101, 126, 127, 360, 689]), 108 | ], 109 | ) 110 | def test_solvation_shell(solute_index, step, expected_res_ids, run_solute): 111 | # TODO: something is broken in the tutorial here 112 | shell = run_solute.get_shell(solute_index, step) 113 | assert set(shell.resindices) == set(expected_res_ids) 114 | 115 | 116 | @pytest.mark.parametrize( 117 | "solute_index, step, remove, expected_res_ids", 118 | [ 119 | (650, 5, {"bn": 1}, [46, 171, 255, 650]), 120 | (651, 6, {"bn": 2, "fec": 1}, [13, 177, 314, 651]), 121 | (689, 0, {"fec": 1}, [101, 126, 127, 360, 689]), 122 | ], 123 | ) 124 | def test_solvation_shell_remove_mols( 125 | solute_index, step, remove, expected_res_ids, run_solute 126 | ): 127 | shell = run_solute.get_shell(solute_index, step, remove_mols=remove) 128 | assert set(shell.resindices) == set(expected_res_ids) 129 | 130 | 131 | @pytest.mark.parametrize( 132 | "solute_index, step, n, expected_res_ids", 133 | [ 134 | (650, 5, 3, [46, 171, 255, 650]), 135 | (651, 6, 3, [13, 177, 314, 651]), 136 | (689, 0, 4, [101, 126, 127, 360, 689]), 137 | (689, 0, 1, [101, 689]), 138 | ], 139 | ) 140 | def test_solvation_shell_remove_closest( 141 | solute_index, step, n, expected_res_ids, run_solute 142 | ): 143 | shell = run_solute.get_shell(solute_index, step, closest_n_only=n) 144 | assert set(shell.resindices) == set(expected_res_ids) 145 | 146 | 147 | @pytest.mark.parametrize( 148 | "shell, n_shells", 149 | [ 150 | ({"bn": 5, "fec": 0, "pf6": 0}, 175), 151 | ({"bn": 3, "fec": 3, "pf6": 0}, 2), 152 | ({"bn": 3, "fec": 0, "pf6": 1}, 13), 153 | ({"bn": 4}, 260), 154 | ], 155 | ) 156 | def test_speciation_find_shells(shell, n_shells, run_solute): 157 | # duplicated to test in solute 158 | df = run_solute.speciation.get_shells(shell) 159 | assert len(df) == n_shells 160 | 161 | 162 | @pytest.mark.parametrize( 163 | "name, cn", 164 | [ 165 | ("fec", 0.25), 166 | ("bn", 4.33), 167 | ("pf6", 0.15), 168 | ], 169 | ) 170 | def test_coordination_numbers(name, cn, run_solute): 171 | # duplicated to test in solute 172 | coord_dict = run_solute.coordination.coordination_numbers 173 | np.testing.assert_allclose(cn, coord_dict[name], atol=0.05) 174 | 175 | 176 | @pytest.mark.parametrize( 177 | "name, fraction", 178 | [ 179 | ("fec", 0.21), 180 | ("bn", 1.0), 181 | ("pf6", 0.14), 182 | ], 183 | ) 184 | def test_pairing(name, fraction, run_solute): 185 | # duplicated to test in solute 186 | pairing_dict = run_solute.pairing.solvent_pairing 187 | np.testing.assert_allclose([fraction], pairing_dict[name], atol=0.05) 188 | 189 | 190 | @pytest.mark.parametrize("name", ["ea", "eaf", "fea", "feaf"]) 191 | def test_instantiate_eax_solvents(name, u_eax_series): 192 | assert isinstance(u_eax_series[name], Universe) 193 | 194 | 195 | @pytest.mark.parametrize("name", ["ea", "eaf", "fea", "feaf"]) 196 | def test_instantiate_eax_atom_groups(name, u_eax_atom_groups): 197 | all_atoms = len(u_eax_atom_groups[name]["li"].universe.atoms) 198 | all_atoms_in_groups = sum([len(ag) for ag in u_eax_atom_groups[name].values()]) 199 | assert all_atoms_in_groups == all_atoms 200 | 201 | 202 | @pytest.mark.parametrize("name", ["ea", "eaf", "fea", "feaf"]) 203 | def test_instantiate_eax_solutes(name, eax_solutes): 204 | assert isinstance(eax_solutes[name], Solute) 205 | 206 | 207 | def test_plot_solvation_radius(run_solute, iba_small_solute): 208 | run_solute.plot_solvation_radius("solute_0", "fec") 209 | iba_small_solute.plot_solvation_radius("iba_ketone", "iba") 210 | 211 | 212 | @pytest.mark.parametrize("residue", ["iba_ketone", "solute", "H2O", "iba"]) 213 | def test_draw_molecule_string(iba_solutes, residue): 214 | iba_solutes["iba_ketone"].draw_molecule(residue) 215 | 216 | 217 | def test_draw_molecule_residue(iba_solutes): 218 | solute = iba_solutes["iba_ketone"] 219 | residue = solute.u.atoms.residues[0] 220 | solute.draw_molecule(residue) 221 | 222 | 223 | def test_iba_solutes(iba_solutes): 224 | for solute in iba_solutes.values(): 225 | assert isinstance(solute, Solute) 226 | 227 | 228 | def test_from_atoms(iba_atom_groups, iba_solvents): 229 | solute_atoms = ( 230 | iba_atom_groups["iba_ketone"] 231 | + iba_atom_groups["iba_alcohol_O"] 232 | + iba_atom_groups["iba_alcohol_H"] 233 | ) 234 | solute = Solute.from_atoms(solute_atoms, iba_solvents) 235 | solute.run() 236 | assert set(solute.atom_solutes.keys()) == {"solute_0", "solute_1", "solute_2"} 237 | 238 | 239 | def test_from_atoms_errors(iba_atom_groups, H2O_atom_groups, iba_solvents): 240 | solute_atoms = ( 241 | iba_atom_groups["iba_ketone"] 242 | + iba_atom_groups["iba_alcohol_O"] 243 | + iba_atom_groups["iba_alcohol_H"] 244 | ) 245 | with pytest.raises(AssertionError): 246 | bad_atoms = solute_atoms[:-2] 247 | Solute.from_atoms(bad_atoms, iba_solvents) 248 | 249 | with pytest.raises(AssertionError): 250 | bad_atoms = solute_atoms + H2O_atom_groups["H2O_O"] 251 | Solute.from_atoms(bad_atoms, iba_solvents) 252 | 253 | 254 | def test_from_atoms_dict(iba_atom_groups, iba_solvents): 255 | solute_atoms = { 256 | "iba_ketone": iba_atom_groups["iba_ketone"], 257 | "iba_alcohol_O": iba_atom_groups["iba_alcohol_O"], 258 | "iba_alcohol_H": iba_atom_groups["iba_alcohol_H"], 259 | } 260 | solute = Solute.from_atoms_dict(solute_atoms, iba_solvents) 261 | assert set(solute.atom_solutes.keys()) == { 262 | "iba_ketone", 263 | "iba_alcohol_O", 264 | "iba_alcohol_H", 265 | } 266 | solute.run() 267 | 268 | 269 | def test_from_atoms_dict_errors(iba_atom_groups, H2O_atom_groups, iba_solvents): 270 | solute_atoms = { 271 | "iba_ketone": iba_atom_groups["iba_ketone"], 272 | "iba_alcohol_O": iba_atom_groups["iba_alcohol_O"], 273 | "iba_alcohol_H": iba_atom_groups["iba_alcohol_H"], 274 | } 275 | with pytest.raises(AssertionError): 276 | bad_atoms = {**solute_atoms} 277 | bad_atoms["iba_ketone"] = bad_atoms["iba_ketone"][:-2] 278 | Solute.from_atoms_dict(bad_atoms, iba_solvents) 279 | 280 | with pytest.raises(AssertionError): 281 | bad_atoms = {**solute_atoms} 282 | bad_atoms["iba_ketone"] = bad_atoms["iba_ketone"] + bad_atoms["iba_alcohol_O"] 283 | Solute.from_atoms_dict(bad_atoms, iba_solvents) 284 | 285 | with pytest.raises(AssertionError): 286 | bad_atoms = {**solute_atoms} 287 | bad_atoms["iba_ketone"] = bad_atoms["iba_alcohol_O"] 288 | Solute.from_atoms_dict(bad_atoms, iba_solvents) 289 | 290 | with pytest.raises(AssertionError): 291 | bad_atoms = {**solute_atoms} 292 | bad_atoms["H2O_O"] = H2O_atom_groups["H2O_O"] 293 | Solute.from_atoms_dict(bad_atoms, iba_solvents) 294 | 295 | 296 | def test_from_solute_list(iba_solutes, iba_solvents): 297 | solute_list = [ 298 | iba_solutes["iba_ketone"], 299 | iba_solutes["iba_alcohol_O"], 300 | iba_solutes["iba_alcohol_H"], 301 | ] 302 | solute = Solute.from_solute_list(solute_list, iba_solvents) 303 | solute.run() 304 | assert set(solute.atom_solutes.keys()) == { 305 | "iba_ketone", 306 | "iba_alcohol_O", 307 | "iba_alcohol_H", 308 | } 309 | 310 | 311 | def test_from_solute_list_restepped(iba_solutes, iba_atom_groups, iba_solvents): 312 | new_solvent = {"H2O": iba_solvents["H2O"]} 313 | new_ketone = Solute.from_atoms( 314 | iba_atom_groups["iba_ketone"], new_solvent, solute_name="iba_ketone" 315 | ) 316 | new_ketone.run(step=2) 317 | solute_list = [iba_solutes["iba_alcohol_O"], new_ketone] 318 | solute = Solute.from_solute_list(solute_list, iba_solvents) 319 | with pytest.warns(UserWarning, match="re-run") as record: 320 | solute.run(step=2) 321 | user_warnings = 0 322 | for warning in record: 323 | if warning.category == UserWarning: 324 | user_warnings += 1 325 | assert user_warnings == 2 326 | assert set(solute.atom_solutes.keys()) == {"iba_ketone", "iba_alcohol_O"} 327 | 328 | 329 | def test_from_solute_list_errors(iba_solutes, H2O_atom_groups, iba_solvents): 330 | solute_list = [ 331 | iba_solutes["iba_ketone"], 332 | iba_solutes["iba_alcohol_O"], 333 | iba_solutes["iba_alcohol_H"], 334 | ] 335 | 336 | H2O_solute = Solute.from_atoms(H2O_atom_groups["H2O_O"], iba_solvents) 337 | with pytest.raises(AssertionError): 338 | bad_solute_list = [*solute_list] 339 | bad_solute_list.append(H2O_solute) 340 | Solute.from_solute_list(bad_solute_list, iba_solvents) 341 | 342 | iba_ketone_renamed = Solute.from_atoms( 343 | iba_solutes["iba_ketone"].solute_atoms, 344 | iba_solvents, 345 | solute_name="iba_alcohol_O", 346 | ) 347 | with pytest.raises(AssertionError): 348 | bad_solute_list = [*solute_list] 349 | bad_solute_list[0] = iba_ketone_renamed 350 | Solute.from_solute_list(bad_solute_list, iba_solvents) 351 | 352 | with pytest.raises(AssertionError): 353 | bad_solute_list = [1, 2, 3] 354 | Solute.from_solute_list(bad_solute_list, iba_solvents) 355 | 356 | 357 | def test_iba_all_analysis(iba_atom_groups, iba_solvents): 358 | solute_atoms = reduce( 359 | lambda x, y: x | y, [solute for solute in iba_atom_groups.values()] 360 | ) 361 | solute = Solute.from_atoms( 362 | solute_atoms, 363 | iba_solvents, 364 | networking_solvents=["iba"], 365 | analysis_classes="all", 366 | radii={"iba": 1.9, "H2O": 1.9}, 367 | ) 368 | # TODO: get this passing 369 | solute.run(step=4) 370 | return 371 | -------------------------------------------------------------------------------- /solvation_analysis/tests/test_speciation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from solvation_analysis.speciation import Speciation 5 | 6 | 7 | def test_speciation_from_solute(run_solute): 8 | speciation = Speciation.from_solute(run_solute) 9 | assert len(speciation.speciation_data) == 490 10 | 11 | 12 | @pytest.mark.parametrize( 13 | "shell, fraction", 14 | [ 15 | ({'bn': 5, 'fec': 0, 'pf6': 0}, 0.357), 16 | ({'bn': 3, 'fec': 3, 'pf6': 0}, 0.004), 17 | ({'bn': 3, 'fec': 0, 'pf6': 1}, 0.016), 18 | ({'bn': 4}, 0.531), 19 | ], 20 | ) 21 | def test_speciation_shell_fraction(shell, fraction, solvation_data): 22 | speciation = Speciation(solvation_data, 10, 49) 23 | fraction = speciation.calculate_shell_fraction(shell) 24 | np.testing.assert_allclose(fraction, fraction, atol=0.05) 25 | 26 | 27 | @pytest.mark.parametrize( 28 | "shell, n_shells", 29 | [ 30 | ({'bn': 5, 'fec': 0, 'pf6': 0}, 175), 31 | ({'bn': 3, 'fec': 3, 'pf6': 0}, 2), 32 | ({'bn': 3, 'fec': 0, 'pf6': 1}, 13), 33 | ({'bn': 4}, 260), 34 | ], 35 | ) 36 | def test_speciation_find_shells(shell, n_shells, solvation_data): 37 | speciation = Speciation(solvation_data, 10, 49) 38 | df = speciation.get_shells(shell) 39 | assert len(df) == n_shells 40 | 41 | 42 | @pytest.mark.parametrize( 43 | "solvent_one, solvent_two, correlation", 44 | [ 45 | ('bn', 'bn', 0.98), 46 | ('fec', 'bn', 1.03), 47 | ('fec', 'pf6', 0.15), 48 | ], 49 | ) 50 | def test_speciation_correlation(solvent_one, solvent_two, correlation, solvation_data): 51 | speciation = Speciation(solvation_data, 10, 49) 52 | df = speciation.solvent_co_occurrence 53 | np.testing.assert_allclose(df[solvent_one][solvent_two], correlation, atol=0.05) 54 | 55 | --------------------------------------------------------------------------------