Skip to content
Merged
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Changelog
* Stiffen the intraghost angle to 180 degrees for dual junctions with two ghost branches instead of removing it. With both ghosts confined to the normal of the physical plane, removal left the same-side arrangement degenerate at the end state, giving large free energy errors for e.g. methoxy and hydroxyl perturbations when the input geometry had both ghosts on the same side [#17](https://github.com/OpenBioSim/ghostly/pull/17).
* Fixed ``--stiffen-rotamers`` placing the minimum of every stiffened dihedral at 0 degrees (cis), which eclipsed the ghosts with the anchor and stacked multiple ghosts on the same bridge. The minimum is now the dihedral angle measured in the end state where the ghosts are physical [#19](https://github.com/OpenBioSim/ghostly/pull/19).
* Added ``--single-well-anchors`` and ``--k-single-well`` to replace surviving anchor dihedrals with a single-well cosine potential whose minimum is the geometry of the end state where the ghost is physical. Multi-well anchors, e.g. the n=2 term of a dummy hydrogen on a carbonyl oxygen, can trap ghosts in an alternative well at intermediate lambda, giving hysteresis between independently equilibrated windows. Anchors with zero force constants or an ill-defined reference dihedral are left unchanged. (Default: False, 5 kcal/mol) [#19](https://github.com/OpenBioSim/ghostly/pull/19).
* Record the options used for each modification in a ``ghostly_options`` property on each modified molecule, and raise an error when modifying a system that has already been modified, unless ``--force`` is used. Forced modifications are applied on top of the existing ones [#20](https://github.com/OpenBioSim/ghostly/pull/20).

[2026.2.0](https://github.com/openbiosim/ghostly/compare/2026.1.0...2026.2.0) - Sep 2026
----------------------------------------------------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,8 @@ modifications that were made.
> When a stream file is used as input, the `--mapping` option is ignored. and
> the output will always be a stream file, regardless of the `--output-format` option.

Modified molecules in the stream file record the options used in a
`ghostly_options` property, and ghostly will refuse to modify them again. To
apply new modifications on top of the existing ones, use the `--force` option.

Please run `ghostly --help` for more details of other configuration options.
12 changes: 12 additions & 0 deletions src/ghostly/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,17 @@ def run():
required=False,
)

parser.add_argument(
"--force",
action=argparse.BooleanOptionalAction,
help="""
Modify a system that has already been modified by ghostly. The new
modifications are applied on top of the existing ones.
""",
default=False,
required=False,
)

parser.add_argument(
"--output-prefix",
type=str,
Expand Down Expand Up @@ -426,6 +437,7 @@ def run():
stiffen_ring_bridges=args.stiffen_ring_bridges,
stiffen_sp2_bridges=args.stiffen_sp2_bridges,
linearise_ring_break=args.linearise_ring_break,
force=args.force,
)
except Exception as e:
logger.error(
Expand Down
47 changes: 46 additions & 1 deletion src/ghostly/_ghostly.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
except Exception:
from loguru import logger as _logger

import json as _json
import sys as _sys

try:
Expand All @@ -59,6 +60,7 @@ def modify(
stiffen_ring_bridges=False,
stiffen_sp2_bridges=False,
linearise_ring_break=False,
force=False,
):
"""
Apply modifications to ghost atom bonded terms to avoid non-physical
Expand Down Expand Up @@ -175,11 +177,19 @@ def modify(
as they are degenerate at 180°. Disabled by default as it is
experimental; enable for ring-breaking or chain-expansion perturbations.

force : bool, optional
Whether to modify a system that has already been modified by ghostly.
The new modifications are applied on top of the existing ones, which
are not undone.

Returns
-------

system : sire.system.System
The updated system.
The updated system. Each perturbable molecule has a ``ghostly_options``
property holding a JSON list of the options used for each
modification, in the order applied. Systems with this property can
only be modified again with ``force=True``.

modifications : dict
A dictionary containing details of the modifications made.
Expand Down Expand Up @@ -210,6 +220,30 @@ def modify(
except KeyError:
raise KeyError("No perturbable molecules in the system")

if not force:
for mol in pert_mols:
if mol.has_property("ghostly_options"):
raise ValueError(
"The system has already been modified by ghostly. "
"Use 'force=True', or '--force' from the command line, "
"to modify it again."
)

options = {
"k_hard": k_hard,
"k_soft": k_soft,
"optimise_angles": optimise_angles,
"num_optimise": num_optimise,
"soften_anchors": soften_anchors,
"single_well_anchors": single_well_anchors,
"k_single_well": k_single_well,
"stiffen_rotamers": stiffen_rotamers,
"k_rotamer": k_rotamer,
"stiffen_ring_bridges": stiffen_ring_bridges,
"stiffen_sp2_bridges": stiffen_sp2_bridges,
"linearise_ring_break": linearise_ring_break,
}

# Create the modifications dictionary.
modifications = {}

Expand Down Expand Up @@ -579,6 +613,17 @@ def modify(
k_rotamer=k_rotamer,
)

history = []
if mol.has_property("ghostly_options"):
history = _json.loads(str(mol.property("ghostly_options")))
history.append(options)
mol = (
mol.edit()
.set_property("ghostly_options", _json.dumps(history))
.molecule()
.commit()
)

# Update the molecule in the system.
system.update(mol)

Expand Down
24 changes: 24 additions & 0 deletions tests/test_ghostly.py
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,30 @@ def test_soften_anchors():
assert not anchors & set(remaining)


def test_modified_flag():
"""
Test that modified molecules record the options used, and that they can
only be modified again when forced.
"""

import json

import pytest

mols = sr.load_test_files("hex2prp.s3")

modified_mols, _ = modify(mols, k_soft=3)
history = json.loads(str(modified_mols[0].property("ghostly_options")))
assert [options["k_soft"] for options in history] == [3]

with pytest.raises(ValueError, match="already been modified"):
modify(modified_mols)

forced_mols, _ = modify(modified_mols, k_soft=4, force=True)
history = json.loads(str(forced_mols[0].property("ghostly_options")))
assert [options["k_soft"] for options in history] == [3, 4]


def test_single_well_overrides_soften_anchors():
"""
Test that single-well and stiffened rotamer terms replace softened
Expand Down
Loading