├── .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 | [](https://www.numfocus.org/)
6 | [](https://www.mdanalysis.org)
7 | [](https://github.com/MDAnalysis/solvation-analysis/actions?query=workflow%3ACI)
8 | [](https://codecov.io/gh/MDAnalysis/solvation-analysis//branch/main)
9 | [](https://solvation-analysis.readthedocs.io/en/latest/)
10 | [](https://doi.org/10.21105/joss.05183)
11 |
12 | [//]: # ([](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 | 
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 | 
47 |
48 | 
49 |
50 | 
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 | 
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 |
--------------------------------------------------------------------------------
|