Skip to content

Commit 9ab7e85

Browse files
authored
Merge pull request #20 from OpenBioSim/feature_modified_flag
2 parents dc4d7d7 + 85f844d commit 9ab7e85

5 files changed

Lines changed: 87 additions & 1 deletion

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Changelog
88
* 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).
99
* 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).
1010
* 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).
11+
* 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).
1112

1213
[2026.2.0](https://github.com/openbiosim/ghostly/compare/2026.1.0...2026.2.0) - Sep 2026
1314
----------------------------------------------------------------------------------------

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,8 @@ modifications that were made.
170170
> When a stream file is used as input, the `--mapping` option is ignored. and
171171
> the output will always be a stream file, regardless of the `--output-format` option.
172172
173+
Modified molecules in the stream file record the options used in a
174+
`ghostly_options` property, and ghostly will refuse to modify them again. To
175+
apply new modifications on top of the existing ones, use the `--force` option.
176+
173177
Please run `ghostly --help` for more details of other configuration options.

‎src/ghostly/_cli.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,17 @@ def run():
245245
required=False,
246246
)
247247

248+
parser.add_argument(
249+
"--force",
250+
action=argparse.BooleanOptionalAction,
251+
help="""
252+
Modify a system that has already been modified by ghostly. The new
253+
modifications are applied on top of the existing ones.
254+
""",
255+
default=False,
256+
required=False,
257+
)
258+
248259
parser.add_argument(
249260
"--output-prefix",
250261
type=str,
@@ -426,6 +437,7 @@ def run():
426437
stiffen_ring_bridges=args.stiffen_ring_bridges,
427438
stiffen_sp2_bridges=args.stiffen_sp2_bridges,
428439
linearise_ring_break=args.linearise_ring_break,
440+
force=args.force,
429441
)
430442
except Exception as e:
431443
logger.error(

‎src/ghostly/_ghostly.py‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
except Exception:
3535
from loguru import logger as _logger
3636

37+
import json as _json
3738
import sys as _sys
3839

3940
try:
@@ -59,6 +60,7 @@ def modify(
5960
stiffen_ring_bridges=False,
6061
stiffen_sp2_bridges=False,
6162
linearise_ring_break=False,
63+
force=False,
6264
):
6365
"""
6466
Apply modifications to ghost atom bonded terms to avoid non-physical
@@ -175,11 +177,19 @@ def modify(
175177
as they are degenerate at 180°. Disabled by default as it is
176178
experimental; enable for ring-breaking or chain-expansion perturbations.
177179
180+
force : bool, optional
181+
Whether to modify a system that has already been modified by ghostly.
182+
The new modifications are applied on top of the existing ones, which
183+
are not undone.
184+
178185
Returns
179186
-------
180187
181188
system : sire.system.System
182-
The updated system.
189+
The updated system. Each perturbable molecule has a ``ghostly_options``
190+
property holding a JSON list of the options used for each
191+
modification, in the order applied. Systems with this property can
192+
only be modified again with ``force=True``.
183193
184194
modifications : dict
185195
A dictionary containing details of the modifications made.
@@ -210,6 +220,30 @@ def modify(
210220
except KeyError:
211221
raise KeyError("No perturbable molecules in the system")
212222

223+
if not force:
224+
for mol in pert_mols:
225+
if mol.has_property("ghostly_options"):
226+
raise ValueError(
227+
"The system has already been modified by ghostly. "
228+
"Use 'force=True', or '--force' from the command line, "
229+
"to modify it again."
230+
)
231+
232+
options = {
233+
"k_hard": k_hard,
234+
"k_soft": k_soft,
235+
"optimise_angles": optimise_angles,
236+
"num_optimise": num_optimise,
237+
"soften_anchors": soften_anchors,
238+
"single_well_anchors": single_well_anchors,
239+
"k_single_well": k_single_well,
240+
"stiffen_rotamers": stiffen_rotamers,
241+
"k_rotamer": k_rotamer,
242+
"stiffen_ring_bridges": stiffen_ring_bridges,
243+
"stiffen_sp2_bridges": stiffen_sp2_bridges,
244+
"linearise_ring_break": linearise_ring_break,
245+
}
246+
213247
# Create the modifications dictionary.
214248
modifications = {}
215249

@@ -579,6 +613,17 @@ def modify(
579613
k_rotamer=k_rotamer,
580614
)
581615

616+
history = []
617+
if mol.has_property("ghostly_options"):
618+
history = _json.loads(str(mol.property("ghostly_options")))
619+
history.append(options)
620+
mol = (
621+
mol.edit()
622+
.set_property("ghostly_options", _json.dumps(history))
623+
.molecule()
624+
.commit()
625+
)
626+
582627
# Update the molecule in the system.
583628
system.update(mol)
584629

‎tests/test_ghostly.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,30 @@ def test_soften_anchors():
836836
assert not anchors & set(remaining)
837837

838838

839+
def test_modified_flag():
840+
"""
841+
Test that modified molecules record the options used, and that they can
842+
only be modified again when forced.
843+
"""
844+
845+
import json
846+
847+
import pytest
848+
849+
mols = sr.load_test_files("hex2prp.s3")
850+
851+
modified_mols, _ = modify(mols, k_soft=3)
852+
history = json.loads(str(modified_mols[0].property("ghostly_options")))
853+
assert [options["k_soft"] for options in history] == [3]
854+
855+
with pytest.raises(ValueError, match="already been modified"):
856+
modify(modified_mols)
857+
858+
forced_mols, _ = modify(modified_mols, k_soft=4, force=True)
859+
history = json.loads(str(forced_mols[0].property("ghostly_options")))
860+
assert [options["k_soft"] for options in history] == [3, 4]
861+
862+
839863
def test_single_well_overrides_soften_anchors():
840864
"""
841865
Test that single-well and stiffened rotamer terms replace softened

0 commit comments

Comments
 (0)