mdinterface 2.0.0__tar.gz → 2.0.1__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.
- {mdinterface-2.0.0 → mdinterface-2.0.1}/CHANGELOG.md +12 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/CONTRIBUTING.md +4 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/MANIFEST.in +1 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/PKG-INFO +1 -1
- {mdinterface-2.0.0 → mdinterface-2.0.1}/docs/guide/polymer.md +1 -1
- {mdinterface-2.0.0 → mdinterface-2.0.1}/docs/guide/simcell.md +24 -9
- {mdinterface-2.0.0 → mdinterface-2.0.1}/docs/guide/specie.md +1 -1
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/__init__.py +1 -1
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/build/builder.py +22 -22
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/core/polymer.py +8 -6
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/core/specie.py +6 -2
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/externals/ligpargen.py +7 -5
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/io/gromacswriter.py +132 -43
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/utils/auxiliary.py +6 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface.egg-info/PKG-INFO +1 -1
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface.egg-info/SOURCES.txt +6 -1
- mdinterface-2.0.1/tests/data/gromacs/README.md +5 -0
- mdinterface-2.0.1/tests/data/gromacs/benzene.json +729 -0
- mdinterface-2.0.1/tests/data/gromacs/ethanol.json +446 -0
- mdinterface-2.0.1/tests/data/gromacs/methane.json +172 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/tests/test_chemistry.py +14 -1
- mdinterface-2.0.1/tests/test_gromacswriter.py +351 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/tests/test_parameterization.py +29 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/LICENSE +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/README.md +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/assets/mdinterface.png +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/docs/api/database.md +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/docs/api/externals.md +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/docs/api/io.md +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/docs/api/polymer.md +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/docs/api/simcell.md +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/docs/api/specie.md +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/docs/assets/mdinterface.png +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/docs/development/releasing.md +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/docs/guide/database.md +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/docs/guide/logging.md +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/docs/guide/migration-2.md +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/docs/index.md +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/docs/installation.md +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/docs/quickstart.md +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/docs/requirements.txt +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/environment-full.yml +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/examples/data/membrane.data.lammps +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/examples/data/membrane.production.dump +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/examples/data/water_frame.dump +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/examples/electrode_interface.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/examples/multilayer.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/examples/multisolvent_box.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/examples/polymer/data/mon1/monomer_1.lammps.lmp +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/examples/polymer/data/mon2/monomer_2.lammps.lmp +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/examples/polymer/polymer_from_smiles.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/examples/polymer/polymer_piperion.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/examples/sandwich_from_traj.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/examples/smiles_box.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/examples/solvent_box.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/examples/solvent_pocket.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/build/__init__.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/build/box.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/build/compartment.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/build/continuum2sim.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/build/polymerize.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/build/regions.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/build/snippets.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/build/solvent.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/config.ini +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/config.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/core/__init__.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/core/chemistry.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/core/topology.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/database/__init__.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/database/graphene.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/database/ions.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/database/metals.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/database/molecules.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/database/nobles.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/externals/__init__.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/externals/aimd.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/externals/ase.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/externals/obabel.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/externals/optimization.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/externals/pyscf.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/io/__init__.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/io/lammpswriter.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/io/packmol.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/io/read.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/io/structure_metadata.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/read/cp2ktraj.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/read/lammpstraj.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/read/read.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/read/trajectory.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/read/xyztraj.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/utils/__init__.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/utils/draw.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/utils/graphs.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/utils/logger.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/utils/map.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/utils/poisson.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/utils/rings.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface/utils/units.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface.egg-info/dependency_links.txt +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface.egg-info/requires.txt +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mdinterface.egg-info/top_level.txt +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/mkdocs.yml +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/pyproject.toml +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/requirements.txt +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/scripts/extract_changelog_summary.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/scripts/verify_distribution_contents.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/setup.cfg +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/setup.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/tests/__init__.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/tests/test_auxiliary.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/tests/test_box.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/tests/test_builder.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/tests/test_changelog_summary.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/tests/test_compartment.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/tests/test_database.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/tests/test_distribution_contents.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/tests/test_imports.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/tests/test_ligpargen.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/tests/test_regions.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/tests/test_solvent.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/tests/test_specie.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/tests/test_structure_metadata.py +0 -0
- {mdinterface-2.0.0 → mdinterface-2.0.1}/tests/test_topology.py +0 -0
|
@@ -4,6 +4,18 @@ All notable changes to mdinterface are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.0.1] - 2026-09-28
|
|
8
|
+
|
|
9
|
+
Fixes GROMACS topology generation and validation for single- and multi-species systems, with LAMMPS/GROMACS energy and force regression coverage. Charge audits now report numerical noise near zero as `0.0` without changing atomic partial charges.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Parameterization and polymer charge audits normalize numerical noise within `1e-12 e` of zero without altering atomic partial charges.
|
|
13
|
+
- GROMACS exports restore scaled 1-4 interactions using explicit pairs derived from bond connectivity, including zero-valued torsions.
|
|
14
|
+
- `SimCell.write_gromacs()` writes shared atom types before molecule definitions so mixed-species topologies can be preprocessed.
|
|
15
|
+
- GROMACS system topologies preserve consecutive molecule order to match the coordinate file.
|
|
16
|
+
- GROMACS exports reject incomplete or unsupported parameters and conflicting species definitions before writing files.
|
|
17
|
+
- `SimCell.write_gromacs()` rejects assembled residues whose atom types, charges, masses, or atom counts differ from their species definitions.
|
|
18
|
+
|
|
7
19
|
## [2.0.0] - 2026-09-28
|
|
8
20
|
|
|
9
21
|
Makes `SimCell` the sole system builder, removing the deprecated `SimulationBox` and `BoxBuilder` APIs and adding spatially constrained solvent regions and optional LAMMPS structural metadata. This major release also changes explicit centering behavior, requires Python 3.10 or newer, and adds RDKit molecular preparation, atomic force-field parameterization, export validation, and improved external-tool diagnostics.
|
|
@@ -65,3 +65,7 @@ Update the relevant guide when changing a documented public API or workflow. API
|
|
|
65
65
|
Add every user-visible change to `CHANGELOG.md` under `Unreleased`, grouped under `Added`, `Changed`, `Fixed`, or `Deprecated`. Write one concise, unwrapped sentence per bullet and describe one observable change. Omit internal refactoring, agent instructions, contributor-only documentation, CI maintenance, and release-process changes unless they affect what package users install or receive.
|
|
66
66
|
|
|
67
67
|
See `docs/development/releasing.md` for the release process.
|
|
68
|
+
|
|
69
|
+
### GROMACS conversion checks
|
|
70
|
+
|
|
71
|
+
`pytest -q tests/test_gromacswriter.py` checks topology structure, parameter validation, molecule ordering, and unit conversions. With `gmx_d` (preferred), or `gmx`, and `lmp` on `PATH`, it also runs GROMACS preprocessing and compares single-point energies and forces against LAMMPS. Engine-dependent tests use the `integration` marker and skip when the executables are unavailable. Hosted integration CI installs both engines. `GMX_BINARY` can select a specific GROMACS executable; the packed electrolyte force comparison requires double precision. Frozen common-molecule fixtures avoid a BOSS dependency in CI. These tests use matched coordinates and explicit OPLS settings; they do not select production simulation protocols.
|
|
@@ -123,7 +123,7 @@ Refinement stages topology and charge changes and applies them only after every
|
|
|
123
123
|
|
|
124
124
|
Refinement requires a connected graph and `snippet_radius >= 4`. Snippets preserve touched rings, formal-charge sites and their immediate neighbors, and multiple bonds; artificial caps are neutral. Charges and topology are recalculated for every junction without the former element-string cache. RDKit-backed snippets use the MOL handoff described in the [species guide](specie.md).
|
|
125
125
|
|
|
126
|
-
Assembly removes leaving-atom partial charges, so an unrefined chain need not have its intended total charge. `round_charges()` only rounds the existing total. Refinement returns a charge audit containing the initial total, formal target, refined total, residual (refined minus formal), per-atom correction, final total, and junction count. It also logs the residual before applying the optional uniform `charge_correction="uniform"` correction; this correction enforces the chain total, not an integer charge on each repeat unit. Check local charge changes as well as the total. The piperion example runs refinement and checks both the chain charge and final membrane neutrality before export. Its whole-chain RDKit geometry step replaces the former optional ML-relaxation recipe.
|
|
126
|
+
Assembly removes leaving-atom partial charges, so an unrefined chain need not have its intended total charge. `round_charges()` only rounds the existing total. Refinement returns a charge audit containing the initial total, formal target, refined total, residual (refined minus formal), per-atom correction, final total, and junction count. It also logs the residual before applying the optional uniform `charge_correction="uniform"` correction; this correction enforces the chain total, not an integer charge on each repeat unit. Check local charge changes as well as the total. The piperion example runs refinement and checks both the chain charge and final membrane neutrality before export. Its whole-chain RDKit geometry step replaces the former optional ML-relaxation recipe. Charge-audit totals and residuals within `1e-12 e` of zero are reported as `0.0`, without applying a numerical-noise correction or changing individual atomic partial charges.
|
|
127
127
|
|
|
128
128
|
### Serialization (optional)
|
|
129
129
|
|
|
@@ -246,27 +246,42 @@ universe = simbox.universe
|
|
|
246
246
|
### GROMACS output *(experimental)*
|
|
247
247
|
|
|
248
248
|
!!! warning
|
|
249
|
-
GROMACS output is
|
|
250
|
-
have been validated for OPLS-AA molecules generated by LigParGen, but
|
|
251
|
-
other force fields or atom styles may need adjustments. Verify the output
|
|
252
|
-
against a reference before production use.
|
|
249
|
+
GROMACS output assumes harmonic bonds and angles, four-term OPLS proper torsions, CVFF impropers, geometric Lennard-Jones mixing, and 0.5 scaling of both LJ and Coulomb 1-4 interactions. It is not a general force-field converter. Constraints, rigid-water settings, electrostatics methods, cutoffs, and simulation protocols must be chosen separately for your model.
|
|
253
250
|
|
|
254
|
-
`write_gromacs()` produces
|
|
251
|
+
`write_gromacs()` produces these files in the directory of your choice:
|
|
255
252
|
|
|
256
|
-
- `{prefix}.gro`
|
|
257
|
-
- `{resname}.itp`
|
|
258
|
-
- `{prefix}.top`
|
|
253
|
+
- `{prefix}.gro` - atomic coordinates and box vectors, with standard GRO precision (0.001 nm).
|
|
254
|
+
- `{resname}.itp` - per-species molecule definitions, bonded terms, and explicit 1-4 pairs derived from shortest bond-graph distances.
|
|
255
|
+
- `{prefix}.top` - shared atom types before all molecule includes, OPLS defaults, and consecutive molecule counts matching the GRO coordinate order.
|
|
259
256
|
|
|
260
257
|
```python
|
|
261
258
|
simbox.write_gromacs(prefix="system", outdir="gromacs_input/")
|
|
262
259
|
```
|
|
263
260
|
|
|
264
|
-
|
|
261
|
+
The exporter validates force-field completeness before creating files. Missing or nonfinite parameters, unsupported fifth torsion coefficients, invalid CVFF parameters, and conflicting atom types raise `ValueError`. Species sharing a residue name must have identical atom order, charges, masses, and topology parameters; use distinct `name=` values for different models. Assembled residues must match their species definitions, including charges and atom types.
|
|
262
|
+
|
|
263
|
+
Individual ITP files include atom types by default:
|
|
265
264
|
|
|
266
265
|
```python
|
|
267
266
|
water.write_gromacs_itp("water.itp")
|
|
268
267
|
```
|
|
269
268
|
|
|
269
|
+
For manual multi-species assembly, keep molecule files separate from shared atom-type definitions:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
from mdinterface.io import write_gromacs_top
|
|
273
|
+
|
|
274
|
+
species = [water, sodium, chloride]
|
|
275
|
+
itp_files = []
|
|
276
|
+
for specie in species:
|
|
277
|
+
filename = f"{specie.resname}.itp"
|
|
278
|
+
specie.write_gromacs_itp(filename, include_atomtypes=False)
|
|
279
|
+
itp_files.append(filename)
|
|
280
|
+
write_gromacs_top(universe, itp_files, species=species)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
GROMACS preprocessing tests cover mixtures, chains, and rings. Single-point energy and force comparisons against LAMMPS cover harmonic terms, OPLS torsions, scaled 1-4 interactions, and CVFF impropers, plus water, benzene, ethanol, methane, dimers, mixed molecular systems, and a packed water/NaCl box. Strict packed-system comparisons use double-precision GROMACS to limit rounding of small force components. These checks do not validate arbitrary force fields or production dynamics. Compare engines at identical coordinates because standard GRO rounding can change bonded energies.
|
|
284
|
+
|
|
270
285
|
## Verbosity
|
|
271
286
|
|
|
272
287
|
```python
|
|
@@ -96,7 +96,7 @@ report = cation.parameterize(charge_correction="none")
|
|
|
96
96
|
cation.validate_force_field()
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
Coordinates stay unchanged. The charge audit reports the initial, raw refined and final totals, formal target, residual, correction per atom and number of junctions. Charge correction is always explicit: `"none"` is the default, and `"uniform"` distributes the residual over all atoms. `ligpargen=True` uses this same parameterization path.
|
|
99
|
+
Coordinates stay unchanged. The charge audit reports the initial, raw refined and final totals, formal target, residual, correction per atom and number of junctions. Charge correction is always explicit: `"none"` is the default, and `"uniform"` distributes the residual over all atoms. `ligpargen=True` uses this same parameterization path. Charge-audit totals and residuals within `1e-12 e` of zero are reported as `0.0`, without applying a numerical-noise correction or changing individual atomic partial charges.
|
|
100
100
|
|
|
101
101
|
**Large molecules (>200 atoms):** `parameterize()` splits the molecule into chemically valid capped segments and refines the junctions. `segment_size` counts caps and cannot exceed 200. All segments and expanded junction snippets are checked before any LigParGen calculation. If the molecule cannot be split safely or a junction remains too large, parameterization fails with guidance rather than sending an oversized structure to BOSS. `snippet_radius` controls junction extent, and `cap_element` selects a neutral monovalent cap. All calculations and validation must succeed before parameters are applied. `refine_large_topology()` delegates to the same workflow with the same options and report.
|
|
102
102
|
|
|
@@ -6,7 +6,7 @@ Initially developed to construct electrolyte/electrode interfaces, it is also we
|
|
|
6
6
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
__version__ = '2.0.
|
|
9
|
+
__version__ = '2.0.1'
|
|
10
10
|
__date__ = '28 Sep. 2026'
|
|
11
11
|
__author__ = 'Fabrice Roncoroni'
|
|
12
12
|
__all__ = ['SimCell', "Specie", "Polymer", "PackmolError", "LigParGenError"]
|
|
@@ -16,7 +16,7 @@ from typing import List, Optional, Union, Tuple, Any
|
|
|
16
16
|
|
|
17
17
|
from mdinterface.utils.auxiliary import find_smallest_missing, label_to_element
|
|
18
18
|
from mdinterface.io.lammpswriter import DATAWriter, write_lammps_coefficients
|
|
19
|
-
from mdinterface.io.gromacswriter import write_gromacs_itp, write_gromacs_top
|
|
19
|
+
from mdinterface.io.gromacswriter import write_gromacs_itp, write_gromacs_top, _prepare_species
|
|
20
20
|
from mdinterface.build.box import make_interface_slab, add_component
|
|
21
21
|
from mdinterface.build.regions import FilledRegion
|
|
22
22
|
from mdinterface.build.compartment import (
|
|
@@ -557,11 +557,11 @@ class SimCell:
|
|
|
557
557
|
|
|
558
558
|
Produces (all written to *outdir*):
|
|
559
559
|
|
|
560
|
-
- ``{prefix}.gro``
|
|
561
|
-
- ``{resname}.itp``
|
|
562
|
-
species), with ``[
|
|
563
|
-
``[ bonds ]``, ``[ angles ]``, and ``[ dihedrals ]`` sections.
|
|
564
|
-
- ``{prefix}.top``
|
|
560
|
+
- ``{prefix}.gro`` - atomic coordinates and box vectors.
|
|
561
|
+
- ``{resname}.itp`` - per-species include topology (one per unique
|
|
562
|
+
species), with ``[ moleculetype ]``, ``[ atoms ]``,
|
|
563
|
+
``[ bonds ]``, ``[ pairs ]``, ``[ angles ]``, and ``[ dihedrals ]`` sections.
|
|
564
|
+
- ``{prefix}.top`` - system topology that defines shared atom types, includes the ITP files, and
|
|
565
565
|
lists molecule counts matching the GRO file.
|
|
566
566
|
|
|
567
567
|
Parameters
|
|
@@ -579,16 +579,20 @@ class SimCell:
|
|
|
579
579
|
RuntimeError
|
|
580
580
|
If called before :meth:`build`.
|
|
581
581
|
|
|
582
|
+
ValueError
|
|
583
|
+
If parameters are incomplete or unsupported, species names collide
|
|
584
|
+
with different definitions, or assembled residues do not match.
|
|
585
|
+
|
|
582
586
|
.. warning::
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
adjustments. Use with care and verify the output against a
|
|
587
|
-
reference.
|
|
587
|
+
Experimental export for harmonic bonds/angles, four-term OPLS
|
|
588
|
+
torsions, CVFF impropers, geometric LJ mixing and 0.5 LJ/Coulomb
|
|
589
|
+
1-4 scaling. Constraints and simulation settings are not generated.
|
|
588
590
|
"""
|
|
589
591
|
if self._universe is None:
|
|
590
592
|
raise RuntimeError("Call build() before write_gromacs().")
|
|
591
593
|
|
|
594
|
+
species = _prepare_species(self._all_species, self._universe)
|
|
595
|
+
|
|
592
596
|
import os
|
|
593
597
|
os.makedirs(outdir, exist_ok=True)
|
|
594
598
|
|
|
@@ -597,17 +601,13 @@ class SimCell:
|
|
|
597
601
|
"write_gromacs is experimental -- verify output before production use."
|
|
598
602
|
)
|
|
599
603
|
|
|
600
|
-
# -- one ITP per unique species (keyed by resname) -----------------
|
|
601
604
|
itp_basenames = []
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
itp_basenames.append(itp_name) # basename only for #include
|
|
609
|
-
written.add(sp.resname)
|
|
610
|
-
logger.info(" >> Species topology: %s", itp_path)
|
|
605
|
+
for sp in species:
|
|
606
|
+
itp_name = f"{sp.resname}.itp"
|
|
607
|
+
itp_path = os.path.join(outdir, itp_name)
|
|
608
|
+
write_gromacs_itp(sp, filename=itp_path, include_atomtypes=False)
|
|
609
|
+
itp_basenames.append(itp_name)
|
|
610
|
+
logger.info(" >> Species topology: %s", itp_path)
|
|
611
611
|
|
|
612
612
|
# -- coordinates (.gro) --------------------------------------------
|
|
613
613
|
gro_file = os.path.join(outdir, f"{prefix}.gro")
|
|
@@ -617,7 +617,7 @@ class SimCell:
|
|
|
617
617
|
# -- system topology (.top) ----------------------------------------
|
|
618
618
|
top_file = os.path.join(outdir, f"{prefix}.top")
|
|
619
619
|
write_gromacs_top(self._universe, itp_basenames,
|
|
620
|
-
filename=top_file, system_name=system_name)
|
|
620
|
+
filename=top_file, system_name=system_name, species=species)
|
|
621
621
|
logger.info(" >> System topology: %s", top_file)
|
|
622
622
|
|
|
623
623
|
def to_ase(self) -> ase.Atoms:
|
|
@@ -14,6 +14,8 @@ from mdinterface.build.polymerize import build_polymer
|
|
|
14
14
|
from mdinterface.build.snippets import make_snippet, remap_snippet_topology
|
|
15
15
|
|
|
16
16
|
# import random
|
|
17
|
+
from mdinterface.utils.auxiliary import _clean_charge
|
|
18
|
+
|
|
17
19
|
import numpy as np
|
|
18
20
|
import logging
|
|
19
21
|
|
|
@@ -238,7 +240,7 @@ class Polymer(Specie):
|
|
|
238
240
|
|
|
239
241
|
# get charges and connection elements
|
|
240
242
|
charges = self.charges
|
|
241
|
-
initial_charge =
|
|
243
|
+
initial_charge = _clean_charge(charges.sum())
|
|
242
244
|
pairs = self.junction_bonds
|
|
243
245
|
|
|
244
246
|
|
|
@@ -253,12 +255,12 @@ class Polymer(Specie):
|
|
|
253
255
|
if not np.isfinite(charges).all():
|
|
254
256
|
raise ValueError("Junction refinement returned nonfinite partial charges.")
|
|
255
257
|
target = int(self.atoms.arrays["nominal_charge"].sum())
|
|
256
|
-
refined_charge =
|
|
257
|
-
residual = refined_charge - target
|
|
258
|
+
refined_charge = _clean_charge(charges.sum())
|
|
259
|
+
residual = _clean_charge(refined_charge - target)
|
|
258
260
|
correction = 0.0
|
|
259
261
|
logger.info("Polymer charge after junction refinement: %.8f e; formal: %d e; residual: %+.8f e",
|
|
260
|
-
|
|
261
|
-
if charge_correction == "uniform":
|
|
262
|
+
refined_charge, target, residual)
|
|
263
|
+
if charge_correction == "uniform" and residual != 0:
|
|
262
264
|
correction = -residual / len(charges)
|
|
263
265
|
logger.info("Applying uniform partial-charge correction: %+.8f e per atom", correction)
|
|
264
266
|
charges += correction
|
|
@@ -270,6 +272,6 @@ class Polymer(Specie):
|
|
|
270
272
|
"refined_charge": refined_charge,
|
|
271
273
|
"residual": residual,
|
|
272
274
|
"correction_per_atom": correction,
|
|
273
|
-
"final_charge":
|
|
275
|
+
"final_charge": _clean_charge(charges.sum()),
|
|
274
276
|
"junctions": len(pairs),
|
|
275
277
|
}
|
|
@@ -955,7 +955,7 @@ class Specie(object):
|
|
|
955
955
|
|
|
956
956
|
return
|
|
957
957
|
|
|
958
|
-
def write_gromacs_itp(self, filename=None):
|
|
958
|
+
def write_gromacs_itp(self, filename=None, *, include_atomtypes=True):
|
|
959
959
|
"""
|
|
960
960
|
Write a GROMACS include topology (.itp) file for this species.
|
|
961
961
|
|
|
@@ -966,12 +966,16 @@ class Specie(object):
|
|
|
966
966
|
----------
|
|
967
967
|
filename : str, optional
|
|
968
968
|
Output filename. Defaults to ``{resname}.itp``.
|
|
969
|
+
include_atomtypes : bool, default True
|
|
970
|
+
Include atom types in this file. For multi-species exports, use
|
|
971
|
+
False and supply ``species`` to ``write_gromacs_top`` to define
|
|
972
|
+
all atom types before all molecule definitions.
|
|
969
973
|
"""
|
|
970
974
|
logger.warning(
|
|
971
975
|
"write_gromacs_itp is experimental -- verify output before production use."
|
|
972
976
|
)
|
|
973
977
|
from mdinterface.io.gromacswriter import write_gromacs_itp
|
|
974
|
-
write_gromacs_itp(self, filename=filename)
|
|
978
|
+
write_gromacs_itp(self, filename=filename, include_atomtypes=include_atomtypes)
|
|
975
979
|
|
|
976
980
|
def validate_force_field(self):
|
|
977
981
|
"""Raise if molecular parameters required for classical export are missing.
|
|
@@ -13,6 +13,8 @@ from mdinterface.io.read import read_lammps_data_file
|
|
|
13
13
|
import logging
|
|
14
14
|
import math
|
|
15
15
|
import copy
|
|
16
|
+
from mdinterface.utils.auxiliary import _clean_charge
|
|
17
|
+
|
|
16
18
|
import numpy as np
|
|
17
19
|
import os
|
|
18
20
|
import ase
|
|
@@ -398,7 +400,7 @@ def refine_large_specie_topology(specie, snippet_radius=12, cap_element="H",
|
|
|
398
400
|
if cap_element not in {"H", "F", "Cl", "Br", "I"}:
|
|
399
401
|
raise ValueError("cap_element must be a neutral monovalent element.")
|
|
400
402
|
staged, attributes = specie._parameterization_copy()
|
|
401
|
-
initial =
|
|
403
|
+
initial = _clean_charge(staged.charges.sum())
|
|
402
404
|
target = staged._resolve_charge(None)
|
|
403
405
|
if len(staged.atoms) <= segment_size:
|
|
404
406
|
result, atom_types, bonds, angles, dihedrals, impropers = run_ligpargen(staged.atoms, charge=target)
|
|
@@ -412,13 +414,13 @@ def refine_large_specie_topology(specie, snippet_radius=12, cap_element="H",
|
|
|
412
414
|
charges = staged.charges
|
|
413
415
|
if not np.isfinite(charges).all():
|
|
414
416
|
raise ValueError("Parameterization returned nonfinite partial charges.")
|
|
415
|
-
refined =
|
|
416
|
-
residual = refined - target
|
|
417
|
-
correction = -residual / len(charges) if charge_correction == "uniform" else 0.0
|
|
417
|
+
refined = _clean_charge(charges.sum())
|
|
418
|
+
residual = _clean_charge(refined - target)
|
|
419
|
+
correction = -residual / len(charges) if charge_correction == "uniform" and residual != 0 else 0.0
|
|
418
420
|
staged.atoms.set_initial_charges(charges + correction)
|
|
419
421
|
report = dict(formal_charge=target, initial_charge=initial, refined_charge=refined,
|
|
420
422
|
residual=residual, correction_per_atom=correction,
|
|
421
|
-
final_charge=
|
|
423
|
+
final_charge=_clean_charge(staged.charges.sum()), junctions=junctions)
|
|
422
424
|
staged.validate_force_field()
|
|
423
425
|
specie._apply_parameterization(staged, attributes)
|
|
424
426
|
logger.info("Parameterization charge audit: %s", report)
|
|
@@ -19,7 +19,10 @@ LAMMPS cvff improper dihedrals are mapped to ``funct=4``.
|
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
21
|
import logging
|
|
22
|
-
import
|
|
22
|
+
from itertools import groupby
|
|
23
|
+
|
|
24
|
+
import networkx as nx
|
|
25
|
+
import numpy as np
|
|
23
26
|
|
|
24
27
|
logger = logging.getLogger(__name__)
|
|
25
28
|
|
|
@@ -65,15 +68,92 @@ _DIHS_HDR = "; {:>5} {:>5} {:>5} {:>5} {:>5} {:>10} {:>14} {:>3}\n"
|
|
|
65
68
|
_DIHS_FMT = " {:>5d} {:>5d} {:>5d} {:>5d} {:>5d} {:>10.3f} {:>14.6f} {:>3d}\n"
|
|
66
69
|
|
|
67
70
|
|
|
68
|
-
def
|
|
71
|
+
def _validate_specie(specie):
|
|
72
|
+
specie.validate_force_field()
|
|
73
|
+
if not len(specie.atoms):
|
|
74
|
+
raise ValueError("Cannot export an empty GROMACS molecule.")
|
|
75
|
+
masses = specie.atoms.get_masses()
|
|
76
|
+
if not np.isfinite(masses).all() or (masses <= 0).any():
|
|
77
|
+
raise ValueError("GROMACS atom masses must be finite and positive.")
|
|
78
|
+
for tid in set(specie.dihedrals[1]):
|
|
79
|
+
if specie._dtype[tid].values[4] is not None:
|
|
80
|
+
raise ValueError("GROMACS export requires four-term OPLS dihedrals; A5 is unsupported.")
|
|
81
|
+
for tid in set(specie.impropers[1]):
|
|
82
|
+
_, sign, multiplicity = specie._itype[tid].values
|
|
83
|
+
if sign not in (-1, 1) or multiplicity not in range(7):
|
|
84
|
+
raise ValueError("GROMACS export requires CVFF impropers with d = +/-1 and integer n = 0..6.")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _atomtype_records(species):
|
|
88
|
+
records = {}
|
|
89
|
+
for specie in species:
|
|
90
|
+
labels, indices = specie.get_atom_types(return_index=True)
|
|
91
|
+
for label, index, atom in zip(labels, indices, specie.atoms):
|
|
92
|
+
parameter = specie._stype[index]
|
|
93
|
+
record = (atom.number, atom.mass, parameter.sig, parameter.eps)
|
|
94
|
+
if label in records and records[label] != record:
|
|
95
|
+
raise ValueError(f"Conflicting GROMACS atom type {label!r}.")
|
|
96
|
+
records[label] = record
|
|
97
|
+
return records
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _write_atomtypes(stream, records):
|
|
101
|
+
nw = max(4, max(map(len, records)))
|
|
102
|
+
stream.write("[ atomtypes ]\n")
|
|
103
|
+
stream.write(_at_hdr(nw).format(
|
|
104
|
+
"name", "atnum", "mass", "charge", "ptype", "sigma(nm)", "eps(kJ/mol)"))
|
|
105
|
+
for label, (number, mass, sigma, epsilon) in records.items():
|
|
106
|
+
stream.write(_at_fmt(nw).format(
|
|
107
|
+
label, number, mass, 0.0, "A", sigma * _ANG_TO_NM, epsilon * _KCAL_TO_KJ))
|
|
108
|
+
stream.write("\n")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _species_signature(specie):
|
|
112
|
+
interactions = []
|
|
113
|
+
for name, key in (("bonds", "_btype"), ("angles", "_atype"),
|
|
114
|
+
("dihedrals", "_dtype"), ("impropers", "_itype")):
|
|
115
|
+
indices, types = getattr(specie, name)
|
|
116
|
+
parameters = getattr(specie, key)
|
|
117
|
+
interactions.append(tuple((tuple(ids), tuple(parameters[tid].values))
|
|
118
|
+
for ids, tid in zip(indices, types)))
|
|
119
|
+
return (tuple(specie.get_atom_types()), tuple(specie._sids),
|
|
120
|
+
tuple(specie.atoms.numbers), tuple(specie.atoms.get_masses()),
|
|
121
|
+
tuple(specie.charges), tuple(_atomtype_records([specie]).items()),
|
|
122
|
+
tuple(interactions))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _prepare_species(species, universe):
|
|
126
|
+
unique = {}
|
|
127
|
+
signatures = {}
|
|
128
|
+
for specie in species:
|
|
129
|
+
_validate_specie(specie)
|
|
130
|
+
signature = _species_signature(specie)
|
|
131
|
+
if specie.resname in signatures and signatures[specie.resname] != signature:
|
|
132
|
+
raise ValueError(f"Conflicting GROMACS molecule definitions for {specie.resname!r}; use distinct species names.")
|
|
133
|
+
unique[specie.resname] = specie
|
|
134
|
+
signatures[specie.resname] = signature
|
|
135
|
+
_atomtype_records(unique.values())
|
|
136
|
+
for residue in universe.residues:
|
|
137
|
+
if residue.resname not in unique:
|
|
138
|
+
raise ValueError(f"No GROMACS molecule definition for {residue.resname!r}.")
|
|
139
|
+
specie = unique[residue.resname]
|
|
140
|
+
atoms = residue.atoms
|
|
141
|
+
if (len(atoms) != len(specie.atoms)
|
|
142
|
+
or not np.array_equal(atoms.types, specie.get_atom_types())
|
|
143
|
+
or not np.allclose(atoms.charges, specie.charges, rtol=0, atol=1e-8)
|
|
144
|
+
or not np.allclose(atoms.masses, specie.atoms.get_masses(), rtol=0, atol=1e-8)):
|
|
145
|
+
raise ValueError(f"Assembled residue {residue.resname!r} does not match its GROMACS molecule definition.")
|
|
146
|
+
return list(unique.values())
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def write_gromacs_itp(specie, filename=None, *, include_atomtypes=True):
|
|
69
150
|
"""
|
|
70
151
|
Write a GROMACS include topology (.itp) file for a Specie.
|
|
71
152
|
|
|
72
153
|
.. warning::
|
|
73
|
-
GROMACS output is
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
Use with care and verify the output against a reference.
|
|
154
|
+
GROMACS output is experimental and assumes harmonic bonds and angles,
|
|
155
|
+
four-term OPLS proper torsions, CVFF impropers, geometric LJ mixing,
|
|
156
|
+
and 0.5 LJ/Coulomb 1-4 scaling. Constraints are not generated.
|
|
77
157
|
|
|
78
158
|
Parameters
|
|
79
159
|
----------
|
|
@@ -81,14 +161,24 @@ def write_gromacs_itp(specie, filename=None):
|
|
|
81
161
|
The molecular species to write.
|
|
82
162
|
filename : str, optional
|
|
83
163
|
Output filename. Defaults to ``{resname}.itp``.
|
|
164
|
+
include_atomtypes : bool, default True
|
|
165
|
+
Include atom-type definitions. For multi-species systems, set False
|
|
166
|
+
and pass the species to :func:`write_gromacs_top` so all atom types
|
|
167
|
+
precede all molecule definitions.
|
|
168
|
+
|
|
169
|
+
Raises
|
|
170
|
+
------
|
|
171
|
+
ValueError
|
|
172
|
+
If parameters are incomplete, conflicting, or unsupported.
|
|
84
173
|
"""
|
|
174
|
+
_validate_specie(specie)
|
|
175
|
+
records = _atomtype_records([specie])
|
|
85
176
|
if filename is None:
|
|
86
177
|
filename = f"{specie.resname}.itp"
|
|
87
178
|
|
|
88
179
|
masses = specie.atoms.get_masses()
|
|
89
180
|
charges = specie.atoms.get_initial_charges()
|
|
90
|
-
|
|
91
|
-
atom_types, type_indexes = specie.get_atom_types(return_index=True)
|
|
181
|
+
atom_types = specie.get_atom_types()
|
|
92
182
|
|
|
93
183
|
bond_idxs, bond_tids = specie.bonds
|
|
94
184
|
angle_idxs, angle_tids = specie.angles
|
|
@@ -98,8 +188,6 @@ def write_gromacs_itp(specie, filename=None):
|
|
|
98
188
|
# column width for name/type: fit the longest label, at least "name" (4)
|
|
99
189
|
nw = max(max(len(t) for t in atom_types), 4)
|
|
100
190
|
|
|
101
|
-
AT_HDR = _at_hdr(nw)
|
|
102
|
-
AT_FMT = _at_fmt(nw)
|
|
103
191
|
ATOMS_HDR = _atoms_hdr(nw)
|
|
104
192
|
ATOMS_FMT = _atoms_fmt(nw)
|
|
105
193
|
|
|
@@ -107,23 +195,8 @@ def write_gromacs_itp(specie, filename=None):
|
|
|
107
195
|
f.write("; GROMACS ITP file generated by mdinterface\n")
|
|
108
196
|
f.write(f"; Molecule: {specie.resname}\n\n")
|
|
109
197
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
# ------------------------------------------------------------------
|
|
113
|
-
f.write("[ atomtypes ]\n")
|
|
114
|
-
f.write(AT_HDR.format(
|
|
115
|
-
"name", "atnum", "mass", "charge", "ptype", "sigma(nm)", "eps(kJ/mol)"))
|
|
116
|
-
seen = set()
|
|
117
|
-
for atype, atnum, mass, tidx in zip(
|
|
118
|
-
atom_types, atnums, masses, type_indexes):
|
|
119
|
-
if atype in seen:
|
|
120
|
-
continue
|
|
121
|
-
seen.add(atype)
|
|
122
|
-
stype = specie._stype[tidx]
|
|
123
|
-
sig = (stype.sig * _ANG_TO_NM) if stype.sig is not None else 0.0
|
|
124
|
-
eps = (stype.eps * _KCAL_TO_KJ) if stype.eps is not None else 0.0
|
|
125
|
-
f.write(AT_FMT.format(atype, atnum, mass, 0.0, "A", sig, eps))
|
|
126
|
-
f.write("\n")
|
|
198
|
+
if include_atomtypes:
|
|
199
|
+
_write_atomtypes(f, records)
|
|
127
200
|
|
|
128
201
|
# ------------------------------------------------------------------
|
|
129
202
|
# [ moleculetype ]
|
|
@@ -153,12 +226,22 @@ def write_gromacs_itp(specie, filename=None):
|
|
|
153
226
|
f.write(_BONDS_HDR.format("ai", "aj", "funct", "b0(nm)", "kb(kJ/mol/nm2)"))
|
|
154
227
|
for (ai, aj), tid in zip(bond_idxs, bond_tids):
|
|
155
228
|
bond = specie._btype[tid]
|
|
156
|
-
b0 =
|
|
229
|
+
b0 = bond.r0 * _ANG_TO_NM
|
|
157
230
|
# LAMMPS: E = K*(r-r0)^2 | GROMACS: V = (kb/2)*(r-r0)^2 => kb = 2*K
|
|
158
|
-
kb =
|
|
231
|
+
kb = 2 * bond.kr * _KCAL_TO_KJ / _ANG_TO_NM**2
|
|
159
232
|
f.write(_BONDS_FMT.format(ai+1, aj+1, 1, b0, kb))
|
|
160
233
|
f.write("\n")
|
|
161
234
|
|
|
235
|
+
graph = nx.Graph()
|
|
236
|
+
graph.add_edges_from(bond_idxs)
|
|
237
|
+
pairs = sorted((a, b) for a, distances in nx.all_pairs_shortest_path_length(graph, cutoff=3)
|
|
238
|
+
for b, distance in distances.items() if a < b and distance == 3)
|
|
239
|
+
if pairs:
|
|
240
|
+
f.write("[ pairs ]\n; ai aj funct\n")
|
|
241
|
+
for ai, aj in pairs:
|
|
242
|
+
f.write(f" {ai + 1} {aj + 1} 1\n")
|
|
243
|
+
f.write("\n")
|
|
244
|
+
|
|
162
245
|
# ------------------------------------------------------------------
|
|
163
246
|
# [ angles ]
|
|
164
247
|
# ------------------------------------------------------------------
|
|
@@ -167,9 +250,9 @@ def write_gromacs_itp(specie, filename=None):
|
|
|
167
250
|
f.write(_ANGS_HDR.format("ai", "aj", "ak", "funct", "th0(deg)", "cth(kJ/mol/rad2)"))
|
|
168
251
|
for (ai, aj, ak), tid in zip(angle_idxs, angle_tids):
|
|
169
252
|
angle = specie._atype[tid]
|
|
170
|
-
th0 = angle.theta0
|
|
253
|
+
th0 = angle.theta0
|
|
171
254
|
# LAMMPS: E = K*(θ-θ0)^2 | GROMACS: V = (kt/2)*(θ-θ0)^2 => kt = 2*K
|
|
172
|
-
cth =
|
|
255
|
+
cth = 2 * angle.kr * _KCAL_TO_KJ
|
|
173
256
|
f.write(_ANGS_FMT.format(ai+1, aj+1, ak+1, 1, th0, cth))
|
|
174
257
|
f.write("\n")
|
|
175
258
|
|
|
@@ -195,10 +278,10 @@ def write_gromacs_itp(specie, filename=None):
|
|
|
195
278
|
vals = dih.values # [K1, K2, K3, K4, K5?]
|
|
196
279
|
for vi, n, phi0 in opls_map:
|
|
197
280
|
K = vals[vi]
|
|
198
|
-
if K
|
|
281
|
+
if K == 0.0:
|
|
199
282
|
continue
|
|
200
283
|
kphi = K / 2.0 * _KCAL_TO_KJ
|
|
201
|
-
f.write(_DIHS_FMT.format(ai+1, aj+1, ak+1, al+1, 9, phi0, kphi, n))
|
|
284
|
+
f.write(_DIHS_FMT.format(ai+1, aj+1, ak+1, al+1, 9, phi0, kphi, int(n)))
|
|
202
285
|
f.write("\n")
|
|
203
286
|
|
|
204
287
|
# ------------------------------------------------------------------
|
|
@@ -217,14 +300,14 @@ def write_gromacs_itp(specie, filename=None):
|
|
|
217
300
|
imp = specie._itype[tid]
|
|
218
301
|
K, d, n = imp.values
|
|
219
302
|
phi0 = 0.0 if d == 1 else 180.0
|
|
220
|
-
kphi =
|
|
221
|
-
f.write(_DIHS_FMT.format(ai+1, aj+1, ak+1, al+1, 4, phi0, kphi, n))
|
|
303
|
+
kphi = K * _KCAL_TO_KJ
|
|
304
|
+
f.write(_DIHS_FMT.format(ai+1, aj+1, ak+1, al+1, 4, phi0, kphi, int(n)))
|
|
222
305
|
f.write("\n")
|
|
223
306
|
|
|
224
307
|
|
|
225
308
|
|
|
226
309
|
def write_gromacs_top(universe, itp_files, filename="system.top",
|
|
227
|
-
system_name="MD System"):
|
|
310
|
+
system_name="MD System", *, species=None):
|
|
228
311
|
"""
|
|
229
312
|
Write a GROMACS system topology (.top) file.
|
|
230
313
|
|
|
@@ -239,13 +322,16 @@ def write_gromacs_top(universe, itp_files, filename="system.top",
|
|
|
239
322
|
Output filename. Default ``"system.top"``.
|
|
240
323
|
system_name : str, optional
|
|
241
324
|
Title written in the ``[ system ]`` section.
|
|
325
|
+
species : sequence of Specie, optional
|
|
326
|
+
Species whose atom types are written before the molecule includes.
|
|
327
|
+
These ITP files must be written with ``include_atomtypes=False``.
|
|
328
|
+
Also validates residue identity against the assembled universe.
|
|
242
329
|
"""
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
for
|
|
247
|
-
|
|
248
|
-
seen[name] = seen.get(name, 0) + 1
|
|
330
|
+
records = None
|
|
331
|
+
if species is not None:
|
|
332
|
+
records = _atomtype_records(_prepare_species(species, universe))
|
|
333
|
+
runs = [(name, sum(1 for _ in group))
|
|
334
|
+
for name, group in groupby(universe.residues.resnames)]
|
|
249
335
|
|
|
250
336
|
with open(filename, "w") as f:
|
|
251
337
|
f.write("; GROMACS topology file generated by mdinterface\n\n")
|
|
@@ -255,6 +341,9 @@ def write_gromacs_top(universe, itp_files, filename="system.top",
|
|
|
255
341
|
f.write("; nbfunc comb-rule gen-pairs fudgeLJ fudgeQQ\n")
|
|
256
342
|
f.write(" 1 3 yes 0.5 0.5\n\n")
|
|
257
343
|
|
|
344
|
+
if records:
|
|
345
|
+
_write_atomtypes(f, records)
|
|
346
|
+
|
|
258
347
|
for itp in itp_files:
|
|
259
348
|
f.write(f'#include "{itp}"\n')
|
|
260
349
|
f.write("\n")
|
|
@@ -264,6 +353,6 @@ def write_gromacs_top(universe, itp_files, filename="system.top",
|
|
|
264
353
|
|
|
265
354
|
f.write("[ molecules ]\n")
|
|
266
355
|
f.write("; {:<14} {:>6}\n".format("molecule", "nmols"))
|
|
267
|
-
for resname, count in
|
|
356
|
+
for resname, count in runs:
|
|
268
357
|
f.write(f" {resname:<14s} {count:>6d}\n")
|
|
269
358
|
|
|
@@ -192,3 +192,9 @@ def round_list_to_sum(lst, target_sum, decimals=3):
|
|
|
192
192
|
difference -= adjustment
|
|
193
193
|
|
|
194
194
|
return rounded_list
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _clean_charge(value):
|
|
198
|
+
"""Normalize charge-report roundoff within 1e-12 e without rounding charges."""
|
|
199
|
+
value = float(value)
|
|
200
|
+
return 0.0 if abs(value) <= 1e-12 else value
|
|
@@ -107,6 +107,7 @@ tests/test_chemistry.py
|
|
|
107
107
|
tests/test_compartment.py
|
|
108
108
|
tests/test_database.py
|
|
109
109
|
tests/test_distribution_contents.py
|
|
110
|
+
tests/test_gromacswriter.py
|
|
110
111
|
tests/test_imports.py
|
|
111
112
|
tests/test_ligpargen.py
|
|
112
113
|
tests/test_parameterization.py
|
|
@@ -114,4 +115,8 @@ tests/test_regions.py
|
|
|
114
115
|
tests/test_solvent.py
|
|
115
116
|
tests/test_specie.py
|
|
116
117
|
tests/test_structure_metadata.py
|
|
117
|
-
tests/test_topology.py
|
|
118
|
+
tests/test_topology.py
|
|
119
|
+
tests/data/gromacs/README.md
|
|
120
|
+
tests/data/gromacs/benzene.json
|
|
121
|
+
tests/data/gromacs/ethanol.json
|
|
122
|
+
tests/data/gromacs/methane.json
|