molify 0.2.5__tar.gz → 0.2.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {molify-0.2.5 → molify-0.2.7}/.github/workflows/pytest.yaml +2 -2
- {molify-0.2.5 → molify-0.2.7}/.pre-commit-config.yaml +2 -2
- {molify-0.2.5 → molify-0.2.7}/PKG-INFO +13 -2
- {molify-0.2.5 → molify-0.2.7}/README.md +9 -0
- {molify-0.2.5 → molify-0.2.7}/docs/source/index.rst +9 -0
- {molify-0.2.5 → molify-0.2.7}/docs/source/packmol_tools.ipynb +39 -0
- {molify-0.2.5 → molify-0.2.7}/paper/paper.md +1 -4
- {molify-0.2.5 → molify-0.2.7}/pyproject.toml +5 -0
- {molify-0.2.5 → molify-0.2.7}/src/molify/__init__.py +2 -0
- molify-0.2.7/src/molify/__main__.py +6 -0
- {molify-0.2.5 → molify-0.2.7}/src/molify/_version.py +2 -2
- {molify-0.2.5 → molify-0.2.7}/src/molify/ase2x.py +50 -48
- molify-0.2.7/src/molify/cli.py +100 -0
- molify-0.2.7/src/molify/compress.py +52 -0
- {molify-0.2.5 → molify-0.2.7}/src/molify/pack.py +29 -12
- molify-0.2.7/src/molify/repeat.py +117 -0
- {molify-0.2.5 → molify-0.2.7}/src/molify/smiles2x.py +19 -1
- {molify-0.2.5 → molify-0.2.7}/src/molify/substructure.py +15 -15
- {molify-0.2.5 → molify-0.2.7}/src/molify/utils.py +94 -31
- {molify-0.2.5 → molify-0.2.7}/tests/conftest.py +14 -0
- {molify-0.2.5 → molify-0.2.7}/tests/integration/test_ase2x.py +192 -0
- molify-0.2.7/tests/test_cli.py +78 -0
- molify-0.2.7/tests/test_connectivity.py +156 -0
- molify-0.2.7/tests/test_repeat.py +295 -0
- {molify-0.2.5 → molify-0.2.7}/tests/test_smiles2conformers.py +32 -1
- {molify-0.2.5 → molify-0.2.7}/tests/test_solvate.py +34 -0
- {molify-0.2.5 → molify-0.2.7}/tests/test_substructure.py +20 -0
- {molify-0.2.5 → molify-0.2.7}/tests/test_utils.py +6 -13
- molify-0.2.7/uv.lock +3631 -0
- molify-0.2.5/src/molify/compress.py +0 -74
- molify-0.2.5/tests/test_connectivity.py +0 -68
- molify-0.2.5/uv.lock +0 -2675
- {molify-0.2.5 → molify-0.2.7}/.github/workflows/build-wheels.yaml +0 -0
- {molify-0.2.5 → molify-0.2.7}/.github/workflows/pages.yaml +0 -0
- {molify-0.2.5 → molify-0.2.7}/.gitignore +0 -0
- {molify-0.2.5 → molify-0.2.7}/.python-version +0 -0
- {molify-0.2.5 → molify-0.2.7}/CITATION.cff +0 -0
- {molify-0.2.5 → molify-0.2.7}/CONTRIBUTING.md +0 -0
- {molify-0.2.5 → molify-0.2.7}/LICENSE +0 -0
- {molify-0.2.5 → molify-0.2.7}/docs/Makefile +0 -0
- {molify-0.2.5 → molify-0.2.7}/docs/make.bat +0 -0
- {molify-0.2.5 → molify-0.2.7}/docs/source/ase_tools.ipynb +0 -0
- {molify-0.2.5 → molify-0.2.7}/docs/source/atom_selection.ipynb +0 -0
- {molify-0.2.5 → molify-0.2.7}/docs/source/conf.py +0 -0
- {molify-0.2.5 → molify-0.2.7}/docs/source/modules.rst +0 -0
- {molify-0.2.5 → molify-0.2.7}/docs/source/networkx_tools.ipynb +0 -0
- {molify-0.2.5 → molify-0.2.7}/docs/source/rdkit_tools.ipynb +0 -0
- {molify-0.2.5 → molify-0.2.7}/paper/.gitignore +0 -0
- {molify-0.2.5 → molify-0.2.7}/paper/bibliography.bib +0 -0
- {molify-0.2.5 → molify-0.2.7}/paper/build.sh +0 -0
- {molify-0.2.5 → molify-0.2.7}/paper/zndraw_rdkit.svg +0 -0
- {molify-0.2.5 → molify-0.2.7}/src/molify/bond_order.py +0 -0
- {molify-0.2.5 → molify-0.2.7}/src/molify/com.py +0 -0
- {molify-0.2.5 → molify-0.2.7}/src/molify/constants.py +0 -0
- {molify-0.2.5 → molify-0.2.7}/src/molify/networkx2x.py +0 -0
- {molify-0.2.5 → molify-0.2.7}/src/molify/rdkit2x.py +0 -0
- {molify-0.2.5 → molify-0.2.7}/tests/data/ec_emc.xyz +0 -0
- {molify-0.2.5 → molify-0.2.7}/tests/integration/test_networkx2x.py +0 -0
- {molify-0.2.5 → molify-0.2.7}/tests/integration/test_rdkit2x.py +0 -0
- {molify-0.2.5 → molify-0.2.7}/tests/test_com.py +0 -0
- {molify-0.2.5 → molify-0.2.7}/tests/test_compress.py +0 -0
- {molify-0.2.5 → molify-0.2.7}/tests/test_edgecases.py +0 -0
- {molify-0.2.5 → molify-0.2.7}/tests/test_examples.py +0 -0
- {molify-0.2.5 → molify-0.2.7}/tests/test_rdkit2ase.py +0 -0
|
@@ -21,8 +21,8 @@ jobs:
|
|
|
21
21
|
- "3.12"
|
|
22
22
|
- "3.11"
|
|
23
23
|
- "3.10"
|
|
24
|
-
os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-
|
|
25
|
-
# windows-11-arm -> rdkit support missing
|
|
24
|
+
os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-latest]
|
|
25
|
+
# windows-11-arm, macos-15-intel -> rdkit support missing
|
|
26
26
|
|
|
27
27
|
steps:
|
|
28
28
|
- uses: actions/checkout@v5
|
|
@@ -20,13 +20,13 @@ repos:
|
|
|
20
20
|
- id: sort-simple-yaml
|
|
21
21
|
- id: trailing-whitespace
|
|
22
22
|
- repo: https://github.com/codespell-project/codespell
|
|
23
|
-
rev: v2.4.
|
|
23
|
+
rev: v2.4.3
|
|
24
24
|
hooks:
|
|
25
25
|
- id: codespell
|
|
26
26
|
additional_dependencies: ["tomli"]
|
|
27
27
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
28
28
|
# Ruff version.
|
|
29
|
-
rev: v0.
|
|
29
|
+
rev: v0.16.10
|
|
30
30
|
hooks:
|
|
31
31
|
# Run the linter.
|
|
32
32
|
- id: ruff
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: molify
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Molecular Structure Interface with RDKit, ASE, Packmol and NetworkX
|
|
5
5
|
Project-URL: Repository, https://github.com/zincware/molify
|
|
6
6
|
Project-URL: Releases, https://github.com/zincware/molify/releases
|
|
@@ -15,6 +15,8 @@ Requires-Dist: matplotlib>=3.10.1
|
|
|
15
15
|
Requires-Dist: networkx>=3.4.2
|
|
16
16
|
Requires-Dist: packmol>=21.1.2
|
|
17
17
|
Requires-Dist: rdkit>=2024
|
|
18
|
+
Requires-Dist: typer>=0.27.1
|
|
19
|
+
Requires-Dist: typing-extensions>=4.15.0
|
|
18
20
|
Provides-Extra: vesin
|
|
19
21
|
Requires-Dist: vesin>=0.3.7; extra == 'vesin'
|
|
20
22
|
Description-Content-Type: text/markdown
|
|
@@ -58,6 +60,15 @@ print(atoms)
|
|
|
58
60
|
>>> Atoms(symbols='OH2', pbc=False)
|
|
59
61
|
```
|
|
60
62
|
|
|
63
|
+
## Command Line Interface
|
|
64
|
+
|
|
65
|
+
Commands mirror the molify functions and write to standard output. `--format`
|
|
66
|
+
takes any ASE format name or extension, defaulting to extxyz.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
molify smiles2atoms CCO > etoh.xyz
|
|
70
|
+
```
|
|
71
|
+
|
|
61
72
|
## Packmol Interface
|
|
62
73
|
|
|
63
74
|
Given the molecular units, you can build periodic boxes with a given density
|
|
@@ -37,6 +37,15 @@ print(atoms)
|
|
|
37
37
|
>>> Atoms(symbols='OH2', pbc=False)
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
## Command Line Interface
|
|
41
|
+
|
|
42
|
+
Commands mirror the molify functions and write to standard output. `--format`
|
|
43
|
+
takes any ASE format name or extension, defaulting to extxyz.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
molify smiles2atoms CCO > etoh.xyz
|
|
47
|
+
```
|
|
48
|
+
|
|
40
49
|
## Packmol Interface
|
|
41
50
|
|
|
42
51
|
Given the molecular units, you can build periodic boxes with a given density
|
|
@@ -100,6 +100,15 @@ The networkx.Graph representation can be obtained from either RDKit or ASE objec
|
|
|
100
100
|
nh3_graph = molify.ase2networkx(nh3_atoms)
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
**Command Line Interface**
|
|
104
|
+
|
|
105
|
+
Commands mirror the molify functions and write to standard output. ``--format``
|
|
106
|
+
takes any ASE format name or extension, defaulting to extxyz.
|
|
107
|
+
|
|
108
|
+
.. code-block:: console
|
|
109
|
+
|
|
110
|
+
$ molify smiles2atoms CCO > etoh.xyz
|
|
111
|
+
|
|
103
112
|
Advanced Features
|
|
104
113
|
-----------------
|
|
105
114
|
|
|
@@ -284,6 +284,45 @@
|
|
|
284
284
|
"print(f\"Density: {actual_density:.1f} kg/m³\")"
|
|
285
285
|
]
|
|
286
286
|
},
|
|
287
|
+
{
|
|
288
|
+
"cell_type": "markdown",
|
|
289
|
+
"metadata": {},
|
|
290
|
+
"source": [
|
|
291
|
+
"## Repeating a Box\n",
|
|
292
|
+
"\n",
|
|
293
|
+
"`molify.repeat()` builds a supercell of a periodic box and tiles `info['connectivity']` with it. Every copy carries its own bonds, bonds across the periodic boundary link neighbouring copies, and bond orders are kept. Use it to repeat any structure that carries connectivity."
|
|
294
|
+
]
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
"cell_type": "code",
|
|
298
|
+
"execution_count": 9,
|
|
299
|
+
"metadata": {},
|
|
300
|
+
"outputs": [
|
|
301
|
+
{
|
|
302
|
+
"name": "stdout",
|
|
303
|
+
"output_type": "stream",
|
|
304
|
+
"text": [
|
|
305
|
+
"Atoms: 60 -> 120\n",
|
|
306
|
+
"Bonds: 50 -> 100\n",
|
|
307
|
+
"Molecules: 10 -> 20\n"
|
|
308
|
+
]
|
|
309
|
+
}
|
|
310
|
+
],
|
|
311
|
+
"source": [
|
|
312
|
+
"# Tile the mixture twice along the first cell vector\n",
|
|
313
|
+
"supercell = molify.repeat(mixture, (2, 1, 1))\n",
|
|
314
|
+
"\n",
|
|
315
|
+
"print(f\"Atoms: {len(mixture)} -> {len(supercell)}\")\n",
|
|
316
|
+
"print(\n",
|
|
317
|
+
" f\"Bonds: {len(mixture.info['connectivity'])} \"\n",
|
|
318
|
+
" f\"-> {len(supercell.info['connectivity'])}\"\n",
|
|
319
|
+
")\n",
|
|
320
|
+
"print(\n",
|
|
321
|
+
" f\"Molecules: {len(list(molify.iter_fragments(mixture)))} \"\n",
|
|
322
|
+
" f\"-> {len(list(molify.iter_fragments(supercell)))}\"\n",
|
|
323
|
+
")"
|
|
324
|
+
]
|
|
325
|
+
},
|
|
287
326
|
{
|
|
288
327
|
"cell_type": "markdown",
|
|
289
328
|
"metadata": {},
|
|
@@ -110,10 +110,7 @@ For example, `molify` streamlines the extraction of the CH$_3$ alkyl group from
|
|
|
110
110
|
```python
|
|
111
111
|
from molify import get_substructures
|
|
112
112
|
|
|
113
|
-
frames: list[ase.Atoms] = get_substructures(
|
|
114
|
-
atoms=box,
|
|
115
|
-
smiles="[C]([H])([H])[H]"
|
|
116
|
-
)
|
|
113
|
+
frames: list[ase.Atoms] = get_substructures(atoms=box, smiles="[C]([H])([H])[H]")
|
|
117
114
|
```
|
|
118
115
|
|
|
119
116
|
# Related software
|
|
@@ -14,6 +14,8 @@ dependencies = [
|
|
|
14
14
|
"networkx>=3.4.2",
|
|
15
15
|
"packmol>=21.1.2",
|
|
16
16
|
"rdkit>=2024",
|
|
17
|
+
"typer>=0.27.1",
|
|
18
|
+
"typing-extensions>=4.15.0",
|
|
17
19
|
]
|
|
18
20
|
|
|
19
21
|
[dependency-groups]
|
|
@@ -32,6 +34,9 @@ docs = [
|
|
|
32
34
|
"sphinxcontrib-mermaid>=1.0.0",
|
|
33
35
|
]
|
|
34
36
|
|
|
37
|
+
[project.scripts]
|
|
38
|
+
molify = "molify.cli:app"
|
|
39
|
+
|
|
35
40
|
[project.optional-dependencies]
|
|
36
41
|
|
|
37
42
|
vesin = [
|
|
@@ -6,6 +6,7 @@ from molify.compress import compress
|
|
|
6
6
|
from molify.networkx2x import networkx2ase, networkx2rdkit
|
|
7
7
|
from molify.pack import pack
|
|
8
8
|
from molify.rdkit2x import rdkit2ase, rdkit2networkx
|
|
9
|
+
from molify.repeat import repeat
|
|
9
10
|
from molify.smiles2x import smiles2atoms, smiles2conformers
|
|
10
11
|
from molify.substructure import (
|
|
11
12
|
get_substructures,
|
|
@@ -27,6 +28,7 @@ __all__ = [
|
|
|
27
28
|
"pack",
|
|
28
29
|
"smiles2conformers",
|
|
29
30
|
"compress",
|
|
31
|
+
"repeat",
|
|
30
32
|
"match_substructure",
|
|
31
33
|
"group_matches_by_fragment",
|
|
32
34
|
"get_substructures",
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.7'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 7)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import ase
|
|
2
2
|
import networkx as nx
|
|
3
3
|
import numpy as np
|
|
4
|
-
from ase.
|
|
4
|
+
from ase.data import covalent_radii
|
|
5
|
+
from ase.neighborlist import neighbor_list
|
|
5
6
|
from rdkit import Chem
|
|
6
7
|
|
|
7
8
|
from molify.constants import EdgeAttr, GraphAttr, NodeAttr
|
|
9
|
+
from molify.utils import read_connectivity
|
|
8
10
|
|
|
9
11
|
try:
|
|
10
12
|
import vesin
|
|
11
13
|
except ImportError:
|
|
12
14
|
vesin = None
|
|
13
15
|
|
|
16
|
+
# Li, Na, K, Rb, Cs, Fr
|
|
17
|
+
_NON_BONDING_ATOMIC_NUMBERS = (3, 11, 19, 37, 55, 87)
|
|
18
|
+
|
|
14
19
|
|
|
15
20
|
def _create_graph_from_connectivity(
|
|
16
21
|
atoms: ase.Atoms, connectivity, charges
|
|
@@ -39,17 +44,30 @@ def _create_graph_from_connectivity(
|
|
|
39
44
|
return graph
|
|
40
45
|
|
|
41
46
|
|
|
42
|
-
def
|
|
43
|
-
"""Compute
|
|
44
|
-
# non-bonding positive charged atoms / ions.
|
|
45
|
-
non_bonding_atomic_numbers = {3, 11, 19, 37, 55, 87}
|
|
47
|
+
def _compute_bonded_pairs(atoms: ase.Atoms, scale: float, pbc: bool) -> np.ndarray:
|
|
48
|
+
"""Compute bonded atom pairs from distance-based cutoffs.
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
atoms : ase.Atoms
|
|
53
|
+
Structure to search for bonds.
|
|
54
|
+
scale : float
|
|
55
|
+
Factor applied to the covalent radii.
|
|
56
|
+
pbc : bool
|
|
57
|
+
Whether bonds may cross periodic boundaries.
|
|
49
58
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
numpy.ndarray
|
|
62
|
+
Shape ``(M, 2)``; unique ``(i, j)`` rows with ``i < j``, sorted
|
|
63
|
+
lexicographically.
|
|
64
|
+
"""
|
|
65
|
+
radii = covalent_radii[atoms.numbers] * scale
|
|
66
|
+
bonding = ~np.isin(atoms.numbers, _NON_BONDING_ATOMIC_NUMBERS)
|
|
67
|
+
if not bonding.any():
|
|
68
|
+
return np.empty((0, 2), dtype=np.intp)
|
|
69
|
+
# Neighbor lists keep d < cutoff; one ulp more keeps pairs at exactly the cutoff.
|
|
70
|
+
max_cutoff = np.nextafter(2 * radii[bonding].max(), np.inf)
|
|
53
71
|
|
|
54
72
|
if vesin is not None:
|
|
55
73
|
try:
|
|
@@ -66,31 +84,13 @@ def _compute_connectivity_matrix(atoms: ase.Atoms, scale: float, pbc: bool):
|
|
|
66
84
|
"ijdS", atoms, cutoff=max_cutoff, self_interaction=False
|
|
67
85
|
)
|
|
68
86
|
|
|
69
|
-
|
|
87
|
+
keep = (i < j) & bonding[i] & bonding[j] & (d <= radii[i] + radii[j])
|
|
70
88
|
if not pbc:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
j = j[non_periodic_mask]
|
|
74
|
-
d = d[non_periodic_mask]
|
|
89
|
+
keep &= ~s.any(axis=1)
|
|
90
|
+
return np.unique(np.stack([i[keep], j[keep]], axis=1), axis=0)
|
|
75
91
|
|
|
76
|
-
d_ij = np.full((len(atoms), len(atoms)), np.inf)
|
|
77
|
-
d_ij[i, j] = d
|
|
78
|
-
np.fill_diagonal(d_ij, 0.0)
|
|
79
92
|
|
|
80
|
-
|
|
81
|
-
d_ij[excluded_mask, :] = np.inf
|
|
82
|
-
d_ij[:, excluded_mask] = np.inf
|
|
83
|
-
|
|
84
|
-
connectivity_matrix = np.zeros((len(atoms), len(atoms)), dtype=int)
|
|
85
|
-
np.fill_diagonal(d_ij, np.inf)
|
|
86
|
-
connectivity_matrix[d_ij <= pairwise_cutoffs] = 1
|
|
87
|
-
|
|
88
|
-
return connectivity_matrix, non_bonding_atomic_numbers
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _add_node_properties(
|
|
92
|
-
graph: nx.Graph, atoms: ase.Atoms, charges, non_bonding_atomic_numbers
|
|
93
|
-
):
|
|
93
|
+
def _add_node_properties(graph: nx.Graph, atoms: ase.Atoms, charges):
|
|
94
94
|
"""Add node properties to the graph."""
|
|
95
95
|
stored_indices = atoms.info.get(NodeAttr.ORIGINAL_INDEX)
|
|
96
96
|
|
|
@@ -100,7 +100,7 @@ def _add_node_properties(
|
|
|
100
100
|
graph.nodes[i][NodeAttr.ATOMIC_NUMBER] = int(atom.number)
|
|
101
101
|
graph.nodes[i][NodeAttr.ORIGINAL_INDEX] = original_index
|
|
102
102
|
graph.nodes[i][NodeAttr.CHARGE] = float(charges[i])
|
|
103
|
-
if atom.number in
|
|
103
|
+
if atom.number in _NON_BONDING_ATOMIC_NUMBERS:
|
|
104
104
|
graph.nodes[i][NodeAttr.CHARGE] = 1.0
|
|
105
105
|
|
|
106
106
|
|
|
@@ -132,6 +132,12 @@ def ase2networkx(
|
|
|
132
132
|
networkx.Graph
|
|
133
133
|
An undirected NetworkX graph with connectivity information.
|
|
134
134
|
|
|
135
|
+
Raises
|
|
136
|
+
------
|
|
137
|
+
ValueError
|
|
138
|
+
For an invalid bond in ``atoms.info['connectivity']``, see
|
|
139
|
+
:func:`molify.utils.read_connectivity`.
|
|
140
|
+
|
|
135
141
|
Notes
|
|
136
142
|
-----
|
|
137
143
|
The graph contains the following information:
|
|
@@ -150,7 +156,11 @@ def ase2networkx(
|
|
|
150
156
|
Connectivity is determined by:
|
|
151
157
|
|
|
152
158
|
1. Using explicit connectivity if present in atoms.info
|
|
153
|
-
2. Otherwise using distance-based cutoffs
|
|
159
|
+
2. Otherwise using distance-based cutoffs: atoms *i* and *j* are bonded
|
|
160
|
+
when any periodic image of *j* lies within ``scale * (r_i + r_j)`` of
|
|
161
|
+
*i*, with *r* the covalent radius. Li, Na, K, Rb, Cs and Fr are
|
|
162
|
+
non-bonding ions. Edges have ``bond_order=None``; memory scales
|
|
163
|
+
linearly with the number of atoms.
|
|
154
164
|
|
|
155
165
|
To get bond orders, pass the graph to networkx2rdkit().
|
|
156
166
|
|
|
@@ -169,24 +179,16 @@ def ase2networkx(
|
|
|
169
179
|
charges = atoms.get_initial_charges()
|
|
170
180
|
|
|
171
181
|
if GraphAttr.CONNECTIVITY in atoms.info:
|
|
172
|
-
connectivity = atoms
|
|
173
|
-
# ensure connectivity is list[tuple[int, int, float|None]] and
|
|
174
|
-
# does not contain np.generic
|
|
175
|
-
connectivity = [
|
|
176
|
-
(int(i), int(j), float(bond_order) if bond_order is not None else None)
|
|
177
|
-
for i, j, bond_order in connectivity
|
|
178
|
-
]
|
|
182
|
+
connectivity = read_connectivity(atoms)
|
|
179
183
|
return _create_graph_from_connectivity(atoms, connectivity, charges)
|
|
180
184
|
|
|
181
|
-
|
|
182
|
-
atoms, scale, pbc
|
|
183
|
-
)
|
|
185
|
+
pairs = _compute_bonded_pairs(atoms, scale, pbc)
|
|
184
186
|
|
|
185
|
-
graph = nx.
|
|
186
|
-
|
|
187
|
-
|
|
187
|
+
graph = nx.Graph()
|
|
188
|
+
graph.add_nodes_from(range(len(atoms)))
|
|
189
|
+
graph.add_edges_from(pairs.tolist(), **{EdgeAttr.BOND_ORDER: None})
|
|
188
190
|
|
|
189
|
-
_add_node_properties(graph, atoms, charges
|
|
191
|
+
_add_node_properties(graph, atoms, charges)
|
|
190
192
|
|
|
191
193
|
graph.graph[GraphAttr.PBC] = atoms.pbc
|
|
192
194
|
graph.graph[GraphAttr.CELL] = atoms.cell
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Command line interface for molify."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import ase.io
|
|
8
|
+
import typer
|
|
9
|
+
from ase.io.formats import extension2format
|
|
10
|
+
|
|
11
|
+
from molify import __version__
|
|
12
|
+
from molify import smiles2atoms as _smiles2atoms
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
no_args_is_help=True,
|
|
16
|
+
add_completion=False,
|
|
17
|
+
pretty_exceptions_show_locals=False,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _resolve_format(value: str) -> str:
|
|
22
|
+
"""Map a format name or file extension onto an ASE format name.
|
|
23
|
+
|
|
24
|
+
Extensions resolve first, so ``XYZ`` selects ASE's ``extxyz`` writer, which
|
|
25
|
+
keeps the cell and the ``atoms.info`` payload such as the SMILES string and
|
|
26
|
+
the connectivity.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
value : str
|
|
31
|
+
Format name or file extension, in any capitalisation.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
str
|
|
36
|
+
The format name passed on to :func:`ase.io.write`.
|
|
37
|
+
"""
|
|
38
|
+
key = value.strip().lower()
|
|
39
|
+
fmt = extension2format.get(key)
|
|
40
|
+
return fmt.name if fmt is not None else key
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _version(value: bool) -> None:
|
|
44
|
+
"""Print the molify version and exit."""
|
|
45
|
+
if value:
|
|
46
|
+
typer.echo(__version__)
|
|
47
|
+
raise typer.Exit
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@app.callback()
|
|
51
|
+
def main(
|
|
52
|
+
version: Annotated[
|
|
53
|
+
bool,
|
|
54
|
+
typer.Option(
|
|
55
|
+
"--version",
|
|
56
|
+
callback=_version,
|
|
57
|
+
is_eager=True,
|
|
58
|
+
help="Show the molify version and exit.",
|
|
59
|
+
),
|
|
60
|
+
] = False,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Build molecular structures from the command line.
|
|
63
|
+
|
|
64
|
+
Each command mirrors the molify function of the same name and writes the
|
|
65
|
+
structure to standard output.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.command()
|
|
70
|
+
def smiles2atoms(
|
|
71
|
+
smiles: Annotated[
|
|
72
|
+
str, typer.Argument(help="SMILES string of the molecule, such as 'CCO'.")
|
|
73
|
+
],
|
|
74
|
+
fmt: Annotated[
|
|
75
|
+
str,
|
|
76
|
+
typer.Option(
|
|
77
|
+
"--format",
|
|
78
|
+
"-f",
|
|
79
|
+
help=(
|
|
80
|
+
"Output format, given as an ASE format name or file extension."
|
|
81
|
+
" 'XYZ' selects extxyz, which keeps the SMILES string and the"
|
|
82
|
+
" connectivity."
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
] = "extxyz",
|
|
86
|
+
seed: Annotated[
|
|
87
|
+
int, typer.Option(help="Random seed for conformer generation.")
|
|
88
|
+
] = 42,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Convert a SMILES string into a single 3D structure on standard output.
|
|
91
|
+
|
|
92
|
+
Example: molify smiles2atoms CCO --format XYZ > etoh.xyz
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
atoms = _smiles2atoms(smiles, seed=seed)
|
|
96
|
+
except ValueError as err:
|
|
97
|
+
raise typer.BadParameter(str(err), param_hint=["smiles"]) from err
|
|
98
|
+
with io.StringIO() as handle:
|
|
99
|
+
ase.io.write(handle, atoms, format=_resolve_format(fmt))
|
|
100
|
+
sys.stdout.write(handle.getvalue())
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import ase
|
|
2
|
+
import numpy as np
|
|
3
|
+
from ase.cell import Cell
|
|
4
|
+
|
|
5
|
+
from molify.constants import GraphAttr
|
|
6
|
+
from molify.utils import calculate_box_dimensions, fragment_indices
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def compress(
|
|
10
|
+
atoms: ase.Atoms,
|
|
11
|
+
density: float,
|
|
12
|
+
freeze_molecules: bool = False,
|
|
13
|
+
) -> ase.Atoms:
|
|
14
|
+
"""Compress an ASE Atoms object to a target density.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
atoms : ase.Atoms
|
|
19
|
+
The Atoms object to compress.
|
|
20
|
+
density : float
|
|
21
|
+
The target density in kg/m^3.
|
|
22
|
+
freeze_molecules : bool
|
|
23
|
+
If True, freeze the internal degrees of freedom of the molecules
|
|
24
|
+
during compression, to prevent bond compression.
|
|
25
|
+
|
|
26
|
+
Raises
|
|
27
|
+
------
|
|
28
|
+
ValueError
|
|
29
|
+
With ``freeze_molecules=True``, for a missing ``info['connectivity']``
|
|
30
|
+
or an invalid bond in it, see :func:`molify.utils.read_connectivity`.
|
|
31
|
+
"""
|
|
32
|
+
atoms = atoms.copy()
|
|
33
|
+
new_dimensions = np.array(calculate_box_dimensions([atoms], density))
|
|
34
|
+
|
|
35
|
+
if freeze_molecules:
|
|
36
|
+
if atoms.info.get(GraphAttr.CONNECTIVITY) is None:
|
|
37
|
+
raise ValueError("No connectivity info found for freeze_molecules=True")
|
|
38
|
+
fragments = fragment_indices(atoms)
|
|
39
|
+
new_cell = Cell.new(new_dimensions)
|
|
40
|
+
t_mat = np.linalg.solve(atoms.get_cell().T, new_cell.T)
|
|
41
|
+
|
|
42
|
+
positions = atoms.get_positions()
|
|
43
|
+
for fragment in fragments:
|
|
44
|
+
com = atoms[fragment].get_center_of_mass()
|
|
45
|
+
positions[fragment] += com @ t_mat - com
|
|
46
|
+
|
|
47
|
+
atoms.set_positions(positions)
|
|
48
|
+
atoms.set_cell(new_cell, scale_atoms=False)
|
|
49
|
+
else:
|
|
50
|
+
atoms.set_cell(new_dimensions, scale_atoms=True)
|
|
51
|
+
|
|
52
|
+
return atoms
|
|
@@ -10,7 +10,7 @@ from ase.io.proteindatabank import write_proteindatabank
|
|
|
10
10
|
from rdkit import Chem
|
|
11
11
|
|
|
12
12
|
from molify.constants import GraphAttr
|
|
13
|
-
from molify.utils import calculate_box_dimensions
|
|
13
|
+
from molify.utils import calculate_box_dimensions, read_connectivity
|
|
14
14
|
|
|
15
15
|
log = logging.getLogger(__name__)
|
|
16
16
|
|
|
@@ -90,13 +90,29 @@ def _write_molecule_files(
|
|
|
90
90
|
ase.io.write(filepath, atoms)
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
def _combine_bonds(
|
|
94
|
+
selected_images: list[ase.Atoms],
|
|
95
|
+
) -> list[tuple[int, int, float | None]] | None:
|
|
96
|
+
"""Offset each molecule's bonds into the packed box.
|
|
97
|
+
|
|
98
|
+
Returns None when a molecule lacks ``info['connectivity']``.
|
|
99
|
+
"""
|
|
100
|
+
if not all(GraphAttr.CONNECTIVITY in atoms.info for atoms in selected_images):
|
|
101
|
+
return None
|
|
102
|
+
bonds = []
|
|
103
|
+
offset = 0
|
|
104
|
+
for atoms in selected_images:
|
|
105
|
+
for i, j, order in read_connectivity(atoms):
|
|
106
|
+
bonds.append((i + offset, j + offset, order))
|
|
107
|
+
offset += len(atoms)
|
|
108
|
+
return bonds
|
|
109
|
+
|
|
110
|
+
|
|
93
111
|
def _extract_atom_arrays(
|
|
94
112
|
selected_images: list[ase.Atoms], packed_atoms: ase.Atoms
|
|
95
113
|
) -> ase.Atoms:
|
|
96
114
|
"""Extracts and adds relevant atom arrays (if present)
|
|
97
115
|
|
|
98
|
-
Add bonds from the input structures to the packed structure if available.
|
|
99
|
-
|
|
100
116
|
Parameters
|
|
101
117
|
----------
|
|
102
118
|
selected_images : list[ase.Atoms]
|
|
@@ -107,7 +123,7 @@ def _extract_atom_arrays(
|
|
|
107
123
|
Returns
|
|
108
124
|
-------
|
|
109
125
|
ase.Atoms
|
|
110
|
-
The packed ASE Atoms object with the copied arrays
|
|
126
|
+
The packed ASE Atoms object with the copied arrays.
|
|
111
127
|
"""
|
|
112
128
|
array_keys = [
|
|
113
129
|
"occupancy",
|
|
@@ -126,14 +142,6 @@ def _extract_atom_arrays(
|
|
|
126
142
|
)
|
|
127
143
|
packed_atoms.arrays[key] = concatenated_array
|
|
128
144
|
|
|
129
|
-
if all(GraphAttr.CONNECTIVITY in atoms.info for atoms in selected_images):
|
|
130
|
-
bonds = []
|
|
131
|
-
offset = 0
|
|
132
|
-
for atoms in selected_images:
|
|
133
|
-
for bond in atoms.info[GraphAttr.CONNECTIVITY]:
|
|
134
|
-
bonds.append((bond[0] + offset, bond[1] + offset, bond[2]))
|
|
135
|
-
offset += len(atoms)
|
|
136
|
-
packed_atoms.info[GraphAttr.CONNECTIVITY] = bonds
|
|
137
145
|
charges = np.concatenate([atom.get_initial_charges() for atom in selected_images])
|
|
138
146
|
if any(charge != 0 for charge in charges):
|
|
139
147
|
packed_atoms.set_initial_charges(charges)
|
|
@@ -189,6 +197,12 @@ def pack(
|
|
|
189
197
|
ase.Atoms
|
|
190
198
|
An ASE Atoms object representing the packed system.
|
|
191
199
|
|
|
200
|
+
Raises
|
|
201
|
+
------
|
|
202
|
+
ValueError
|
|
203
|
+
For an invalid bond in a molecule's ``info['connectivity']``, see
|
|
204
|
+
:func:`molify.utils.read_connectivity`.
|
|
205
|
+
|
|
192
206
|
Example
|
|
193
207
|
-------
|
|
194
208
|
>>> from molify import pack, smiles2conformers
|
|
@@ -200,6 +214,7 @@ def pack(
|
|
|
200
214
|
Atoms(symbols='C10H44O12', pbc=True, cell=[8.4, 8.4, 8.4])
|
|
201
215
|
"""
|
|
202
216
|
selected_images = _select_conformers(data, counts, seed)
|
|
217
|
+
bonds = _combine_bonds(selected_images)
|
|
203
218
|
cell = calculate_box_dimensions(images=selected_images, density=density)
|
|
204
219
|
|
|
205
220
|
# Adjust cell dimensions according to ratio while keeping volume unchanged
|
|
@@ -240,4 +255,6 @@ def pack(
|
|
|
240
255
|
packed_atoms.cell = cell
|
|
241
256
|
packed_atoms.pbc = True
|
|
242
257
|
packed_atoms = _extract_atom_arrays(selected_images, packed_atoms)
|
|
258
|
+
if bonds is not None:
|
|
259
|
+
packed_atoms.info[GraphAttr.CONNECTIVITY] = bonds
|
|
243
260
|
return packed_atoms
|