@@ -362,14 +362,27 @@ def __init__(self, system, config):
362362 # Create alchemical ions.
363363 ion_indices = []
364364 if charge_diff != 0 :
365- self ._system , coalchemical_restraints , ion_indices = (
365+ # On restart, reuse the exact molecule(s) chosen as alchemical ions
366+ # in the original run, rather than re-running the "furthest waters"
367+ # search. This makes ion selection independent of GCMC state (or
368+ # anything else that might change between runs), since the search
369+ # is otherwise only reproducible by assumption, not by construction.
370+ mol_indices = None
371+ if self ._config .restart :
372+ mol_indices = self ._load_alchemical_ion_indices ()
373+
374+ self ._system , coalchemical_restraints , ion_indices , ion_mol_indices = (
366375 self ._create_alchemical_ions (
367376 self ._system ,
368377 charge_diff ,
369378 restraint_distance = self ._config .coalchemical_restraint_dist ,
379+ mol_indices = mol_indices ,
370380 )
371381 )
372382
383+ # Keep the stored indices in sync for any future restart.
384+ self ._save_alchemical_ion_indices (ion_mol_indices )
385+
373386 # Add the coalchemical restraints to the extra args.
374387 if coalchemical_restraints is not None :
375388 self ._config ._extra_args ["coalchemical_restraints" ] = (
@@ -1045,8 +1058,49 @@ def _get_charge_difference(system):
10451058
10461059 return perturbed - reference
10471060
1061+ def _save_alchemical_ion_indices (self , mol_indices ):
1062+ """
1063+ Persist the absolute molecule index of each alchemical ion to a small,
1064+ dedicated file in the output directory, independent of the per-window
1065+ (regular runner) or shared (repex) checkpoint formats. This allows a
1066+ restart to reuse the exact same ion(s) chosen in the original run.
1067+
1068+ Parameters
1069+ ----------
1070+
1071+ mol_indices: [int]
1072+ The absolute molecule index of each alchemical ion.
1073+ """
1074+ import numpy as _np
1075+
1076+ path = self ._config .output_directory / "alchemical_ions.npz"
1077+ _np .savez (path , mol_indices = _np .array (mol_indices , dtype = int ))
1078+
1079+ def _load_alchemical_ion_indices (self ):
1080+ """
1081+ Load the absolute molecule index of each alchemical ion previously
1082+ stored by `_save_alchemical_ion_indices`, if present.
1083+
1084+ Returns
1085+ -------
1086+
1087+ mol_indices: [int], None
1088+ The absolute molecule index of each alchemical ion, or None if no
1089+ stored indices are available (e.g. a fresh run, or a restart from
1090+ an output directory that predates this feature).
1091+ """
1092+ import numpy as _np
1093+
1094+ path = self ._config .output_directory / "alchemical_ions.npz"
1095+ try :
1096+ return _np .load (path )["mol_indices" ].tolist ()
1097+ except Exception :
1098+ return None
1099+
10481100 @staticmethod
1049- def _create_alchemical_ions (system , charge_diff , restraint_distance = None ):
1101+ def _create_alchemical_ions (
1102+ system , charge_diff , restraint_distance = None , mol_indices = None
1103+ ):
10501104 """
10511105 Internal function to create alchemical ions to maintain a constant charge.
10521106
@@ -1059,6 +1113,14 @@ def _create_alchemical_ions(system, charge_diff, restraint_distance=None):
10591113 charge_diff: int
10601114 The charge difference between perturbed and reference states.
10611115
1116+ mol_indices: [int]
1117+ The absolute molecule index (position in `system.molecules()`) of
1118+ each water to convert into an alchemical ion. If provided, these
1119+ molecules are converted directly, bypassing the "furthest waters"
1120+ search. Used on restart to reproduce the exact same ion(s) chosen
1121+ in the original run, independent of any GCMC state or changes to
1122+ the search heuristic. Must have the same length as `abs(charge_diff)`.
1123+
10621124 Returns
10631125 -------
10641126
@@ -1073,6 +1135,11 @@ def _create_alchemical_ions(system, charge_diff, restraint_distance=None):
10731135 The perturbable-molecule index of each alchemical ion that was
10741136 added, suitable for use with
10751137 `LambdaSchedule.set_molecule_schedule <sire.cas.LambdaSchedule>`.
1138+
1139+ ion_mol_indices: [int]
1140+ The absolute molecule index (position in `system.molecules()`,
1141+ prior to any conversion) of each alchemical ion that was added.
1142+ Suitable for passing back in as `mol_indices` on a restart.
10761143 """
10771144
10781145 from sire .legacy .IO import createChlorineIon as _createChlorineIon
@@ -1116,12 +1183,28 @@ def _create_alchemical_ions(system, charge_diff, restraint_distance=None):
11161183 f"{ len (system ['water' ].molecules ())} available."
11171184 )
11181185
1119- # Reference coordinates.
1120- coords = system .molecules ("property is_perturbable" ).coordinates ()
1121- coord_string = f"{ coords [0 ].value ()} , { coords [1 ].value ()} , { coords [2 ].value ()} "
1186+ if mol_indices is not None :
1187+ if len (mol_indices ) != num_waters :
1188+ raise ValueError (
1189+ f"Number of stored alchemical-ion molecule indices "
1190+ f"({ len (mol_indices )} ) does not match the current charge "
1191+ f"difference ({ num_waters } waters required)."
1192+ )
1193+
1194+ # Reuse the exact molecules chosen in the original run.
1195+ all_mols = system .molecules ()
1196+ waters = [all_mols [idx ] for idx in mol_indices ]
1197+ else :
1198+ # Reference coordinates.
1199+ coords = system .molecules ("property is_perturbable" ).coordinates ()
1200+ coord_string = (
1201+ f"{ coords [0 ].value ()} , { coords [1 ].value ()} , { coords [2 ].value ()} "
1202+ )
11221203
1123- # Find the furthest N waters from the perturbable molecule.
1124- waters = system [f"furthest { num_waters } waters from { coord_string } " ].molecules ()
1204+ # Find the furthest N waters from the perturbable molecule.
1205+ waters = system [
1206+ f"furthest { num_waters } waters from { coord_string } "
1207+ ].molecules ()
11251208
11261209 # Determine the water model.
11271210 if waters [0 ].num_atoms () == 3 :
@@ -1141,6 +1224,10 @@ def _create_alchemical_ions(system, charge_diff, restraint_distance=None):
11411224 # Store the molecule numbers of the alchemical ions.
11421225 ion_numbers = []
11431226
1227+ # Store the absolute molecule index of each alchemical ion (prior to
1228+ # conversion), for persisting across restarts.
1229+ ion_mol_indices = []
1230+
11441231 # Create the ions.
11451232 for water in waters :
11461233 # Flag to indicate whether we need to reverse the alchemical ion
@@ -1261,6 +1348,7 @@ def _create_alchemical_ions(system, charge_diff, restraint_distance=None):
12611348
12621349 # Get the index of the perturbed water.
12631350 index = numbers .index (water .number ())
1351+ ion_mol_indices .append (index )
12641352
12651353 # Log that we are adding an alchemical ion.
12661354 if is_reverse :
@@ -1283,7 +1371,7 @@ def _create_alchemical_ions(system, charge_diff, restraint_distance=None):
12831371 perturbable_mols = system .molecules ()["perturbable" ].molecules ()
12841372 ion_indices = [perturbable_mols .find (system [number ]) for number in ion_numbers ]
12851373
1286- return system , restraints , ion_indices
1374+ return system , restraints , ion_indices , ion_mol_indices
12871375
12881376 @staticmethod
12891377 def _create_filenames (lambda_array , lambda_value , output_directory , restart = False ):
0 commit comments