Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
### Coming in the next version:
* high-symmetry determination bugs (#21)
* headless fallback build by @JamesOBrien2 (#44)
* ORCA / Gaussian plugins for `vmol2`: prompt / `view:` selectors for frequencies, optimization, single point, scan, and IRC trajectories; frequency view builds a selectable mode-displacement trajectory (`mode:` / `scale:` / `npoints:`)

**Full Changelog**: https://github.com/briling/v/compare/v2.0...v3.0rc7

8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ A simple X11 molecular viewer.
- [Priroda](http://rad.chem.msu.ru/~laikov) input and output files
- [`.xyz`](https://en.wikipedia.org/wiki/XYZ_file_format) files
- [extended `.xyz`](https://github.com/libAtoms/extxyz) files (currently the extra columns are ignored)
- various quantum-chemical outputs with [cclib](https://github.com/cclib/cclib), see the Python wrapper [page](python/README.md).
- various quantum-chemical outputs with [cclib](https://github.com/cclib/cclib), see the Python wrapper [page](python/README.md)
(`vmol2`; ORCA and Gaussian plugins for frequencies, optimization, single point, scan, and IRC)

## Python package (wrapper / API) available

See python package page [here](python/README.md).

Provides wrapper scripts with a simple installation and
allows to open unsupported file formats with `cclib`.
allows to open unsupported file formats with `cclib` (`vmol2`).
For ORCA / Gaussian multi-job logs, `vmol2` can prompt (or take `view:` / `vib:`)
for frequencies, optimization, single point, IRC, or scan
(see [python/README.md](python/README.md)).

## Download
```bash
Expand Down
31 changes: 25 additions & 6 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,33 @@ vmol ../mol/MOL_3525.xyz cell:8.93,0.0,0.0,4.2,8.9,0.0,0.48,2.32,10
python -m vmol../mol/periodic.in bonds:0
```

Unsupported quantum-chemical outputs, e.g., Orca:
Unsupported quantum-chemical outputs, e.g., ORCA or Gaussian:
```bash
vmol2 ../mol/CEHZOF_1_SPE.out
python -m vmol.vmol2 ../mol/AJALIH_5_SPE.out
vmol2 mol/Cat1_TSOA_CfA_Cf0.log
```
or Gaussian:
ORCA and Gaussian logs use dedicated plugins. When a file has several of
frequencies, optimization, trailing single point (ORCA), scan, or IRC,
`vmol2` prompts on a TTY (each choice opens in its own `v` session), or accepts:
```bash
vmol2 mol/Cat1_TSOA_CfA_Cf0.log vib:0 # geometry optimization
vmol2 mol/Cat1_TSOA_CfA_Cf0.log # normal modes (default for files containing both)
vmol2 job.out view:freq # pick a normal mode, then show a displacement path
vmol2 job.out view:opt # geometry optimization frames
vmol2 job.out view:sp # final single-point geometry / energy (ORCA multi-job)
vmol2 job.out view:irc # IRC trajectory
vmol2 job.out view:scan # relaxed surface scan
vmol2 job.out vib:0 # geometry path (opt, else SP, else IRC, else scan)
vmol2 job.out vib:1 # frequencies (same as view:freq)
```
Choosing frequencies (TTY) lists modes with their cm⁻¹ values, then asks for a
mode number, displacement magnitude (Å), and number of frames. Non-interactive:
```bash
vmol2 job.out view:freq mode:1 scale:0.5 npoints:21
```
ORCA IRC frames come from `basename_IRC_Full_trj.xyz` (or the path in the log's
`Storing full IRC trajectory in ....` line). Gaussian IRC frames are read from
the `.log` itself when an IRC job is detected. Other packages still use the
generic cclib path.

### Library

Expand Down Expand Up @@ -154,8 +171,10 @@ out = vmol.capture(mols=mols)
```

File formats which are not supported natively can be read with `cclib` and passed
(see [example](https://github.com/briling/v/tree/master/python/examples/ex_cclib.py)
and [vmol2 source](https://github.com/briling/v/tree/master/python/vmol/vmol2.py)).
(see [example](https://github.com/briling/v/tree/master/python/examples/ex_cclib.py),
[vmol2](https://github.com/briling/v/tree/master/python/vmol/vmol2.py), and the
ORCA / Gaussian plugins under
[python/vmol/plugins](https://github.com/briling/v/tree/master/python/vmol/plugins)).

One can also pass normal modes data with
```python
Expand Down
2 changes: 1 addition & 1 deletion python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ Repository = "https://github.com/briling/v.git"
"Bug Tracker" = "https://github.com/briling/v/issues"

[tool.setuptools]
packages = ["vmol"]
packages = ["vmol", "vmol.plugins"]
2 changes: 2 additions & 0 deletions python/ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,5 @@ ignore = [
[lint.per-file-ignores]
"vmol/__main__.py" = ["D"]
"examples/ex*.py" = ["D"]
"tests/*.py" = ["SLF", "PLC2701", "D"]
"vmol/plugins/vibtraj.py" = ["PLC2701"]
20 changes: 20 additions & 0 deletions python/tests/_plugin_fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Shared fixtures for ORCA / Gaussian plugin tests."""

from __future__ import annotations

from types import SimpleNamespace

import numpy as np


def coords(*frames):
return np.asarray(frames, dtype=float)


def base_data(nframes=1):
frames = [[[0.0, 0.0, 0.0], [0.0, 0.0, 0.74 + 0.01 * i]] for i in range(nframes)]
return SimpleNamespace(
atomnos=np.array([1, 1]),
atomcoords=coords(*frames),
scfenergies=np.array([-27.2 - 0.01 * i for i in range(nframes)]),
)
112 changes: 112 additions & 0 deletions python/tests/test_gaussian_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Unit tests for the Gaussian vmol2 plugin (no viewer / X11)."""

from __future__ import annotations

import os
import tempfile
import unittest
from pathlib import Path

import numpy as np

from _plugin_fixtures import base_data
from vmol import vmol2
from vmol.plugins import plugin_for
from vmol.plugins.gaussian import GaussianPlugin, _log_has_irc


class TestGaussianInventory(unittest.TestCase):
"""Inventory detection for Gaussian frequencies / optimization / IRC."""

def setUp(self):
"""Create a temporary directory for fake log paths."""
self.tmpdir = tempfile.TemporaryDirectory()
self.addCleanup(self.tmpdir.cleanup)
self.log_path = os.path.join(self.tmpdir.name, "job.log")

def _write_log(self, text: str) -> str:
Path(self.log_path).write_text(text, encoding="utf-8")
return self.log_path

def test_opt_plus_freq(self):
"""Opt+freq Link1 jobs expose both datasets."""
data = base_data(4)
data.geovalues = np.ones((4, 4))
data.optdone = True
data.vibfreqs = np.array([100.0, 200.0])
data.vibdisps = np.zeros((2, 2, 3))
data.vibdisps[:, 0, 0] = 1.0
self._write_log("Entering Gaussian System\n")

datasets = GaussianPlugin.inventory(self.log_path, data)
self.assertEqual([d.key for d in datasets], ["freq", "opt"])
self.assertEqual(len(datasets[1].mols), 4)
np.testing.assert_allclose(datasets[0].mols[0]["r"], data.atomcoords[3])

def test_irc_log_labeled_as_irc(self):
"""IRC marker lines label the trajectory as irc, not opt."""
data = base_data(5)
data.geovalues = np.ones((5, 4))
data.optdone = True
self._write_log(
"Entering Gaussian System\nIRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC\n",
)
datasets = GaussianPlugin.inventory(self.log_path, data)
self.assertEqual([d.key for d in datasets], ["irc"])
self.assertEqual(len(datasets[0].mols), 5)

def test_single_point_geometry(self):
"""A lone single-point job yields one geom dataset."""
data = base_data(1)
self._write_log("Entering Gaussian System\n")
datasets = GaussianPlugin.inventory(self.log_path, data)
self.assertEqual([d.key for d in datasets], ["geom"])

def test_log_has_irc_route(self):
"""Route lines containing irc are detected."""
path = self._write_log("#p b3lyp/6-31g(d) irc=(calcfc,maxpoints=20)\n")
self.assertTrue(_log_has_irc(path))
path = self._write_log("#p b3lyp/6-31g(d) opt freq\n")
self.assertFalse(_log_has_irc(path))


class TestGaussianSampleFile(unittest.TestCase):
"""Integration check against the repo Gaussian opt+freq sample."""

@classmethod
def setUpClass(cls):
"""Resolve the sample Gaussian logfile paths."""
mol = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "mol"),
)
cls.path = os.path.join(mol, "Cat1_TSOA_CfA_Cf0.log")
cls.spc = os.path.join(mol, "Cat1_TSOA_CfA_Cf0_SPC.log")

def test_parse_and_inventory_opt_freq(self):
"""Sample opt+freq log exposes freq and opt selectors."""
if not os.path.isfile(self.path):
self.skipTest("sample Gaussian logfile not present")

parser, data = GaussianPlugin.parse(self.path)
self.assertEqual(parser.logname, "Gaussian")
self.assertIs(plugin_for(parser), GaussianPlugin)
datasets = GaussianPlugin.inventory(self.path, data)
self.assertEqual([d.key for d in datasets], ["freq", "opt"])
by_key = {d.key: d for d in datasets}
self.assertEqual(len(by_key["freq"].vib["freq"]), 123)
self.assertEqual(len(by_key["opt"].mols), 10)
chosen = vmol2._choose_datasets(datasets, ["opt"], None)
self.assertEqual(chosen[0].key, "opt")

def test_parse_spc(self):
"""Separate single-point log is a single geometry."""
if not os.path.isfile(self.spc):
self.skipTest("sample Gaussian SPC logfile not present")
_parser, data = GaussianPlugin.parse(self.spc)
datasets = GaussianPlugin.inventory(self.spc, data)
self.assertEqual([d.key for d in datasets], ["geom"])
self.assertEqual(len(datasets[0].mols), 1)


if __name__ == "__main__":
unittest.main()
Loading
Loading