MultiOptPy 1.20.5__py3-none-any.whl → 1.20.6__py3-none-any.whl
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.
- multioptpy/MD/thermostat.py +236 -123
- multioptpy/ModelHessian/fischerd3.py +240 -295
- multioptpy/Optimizer/rsirfo.py +112 -4
- multioptpy/Optimizer/rsprfo.py +1005 -698
- multioptpy/entrypoints.py +406 -16
- multioptpy/moleculardynamics.py +21 -13
- {multioptpy-1.20.5.dist-info → multioptpy-1.20.6.dist-info}/METADATA +9 -9
- {multioptpy-1.20.5.dist-info → multioptpy-1.20.6.dist-info}/RECORD +12 -12
- {multioptpy-1.20.5.dist-info → multioptpy-1.20.6.dist-info}/WHEEL +1 -1
- {multioptpy-1.20.5.dist-info → multioptpy-1.20.6.dist-info}/entry_points.txt +0 -0
- {multioptpy-1.20.5.dist-info → multioptpy-1.20.6.dist-info}/licenses/LICENSE +0 -0
- {multioptpy-1.20.5.dist-info → multioptpy-1.20.6.dist-info}/top_level.txt +0 -0
multioptpy/entrypoints.py
CHANGED
|
@@ -12,6 +12,7 @@ import itertools
|
|
|
12
12
|
import multioptpy
|
|
13
13
|
from multioptpy.Parameters.unit_values import UnitValueLib
|
|
14
14
|
from multioptpy.Utils import calc_tools
|
|
15
|
+
from multioptpy.Utils.bond_connectivity import BondConnectivity
|
|
15
16
|
from multioptpy.Wrapper.autots import AutoTSWorkflow
|
|
16
17
|
from multioptpy.Wrapper.autots import AutoTSWorkflow_v2
|
|
17
18
|
|
|
@@ -534,8 +535,10 @@ def run_autots():
|
|
|
534
535
|
|
|
535
536
|
def run_confsearch():
|
|
536
537
|
""" Entry point for the conformation search script (conformation_search.py). """
|
|
537
|
-
|
|
538
|
+
|
|
539
|
+
# Unit conversion constants
|
|
538
540
|
bohr2ang = 0.529177210903
|
|
541
|
+
ang2bohr = 1.0 / bohr2ang
|
|
539
542
|
|
|
540
543
|
#Example: python conformation_search.py s8_for_confomation_search_test.xyz -xtb GFN2-xTB -ns 2000
|
|
541
544
|
|
|
@@ -551,6 +554,38 @@ def run_confsearch():
|
|
|
551
554
|
|
|
552
555
|
return boltzmann_distribution
|
|
553
556
|
|
|
557
|
+
def calc_penalized_boltzmann_distribution(energy_list, visit_counts, temperature=298.15, alpha=0.5):
|
|
558
|
+
"""
|
|
559
|
+
Calculate the Boltzmann distribution with frequency penalty (Tabu Search / Metadynamics approach).
|
|
560
|
+
|
|
561
|
+
P_i^{select} ∝ P_i^{Boltzmann} × exp(-α × N_i)
|
|
562
|
+
|
|
563
|
+
where:
|
|
564
|
+
- P_i^{Boltzmann}: Standard Boltzmann probability
|
|
565
|
+
- N_i: Number of times conformer i has been selected as starting point
|
|
566
|
+
- α: Penalty coefficient (larger values = stronger penalty)
|
|
567
|
+
|
|
568
|
+
This prevents the search from staying in a specific basin and encourages
|
|
569
|
+
broader exploration of conformational space.
|
|
570
|
+
"""
|
|
571
|
+
# Calculate standard Boltzmann distribution
|
|
572
|
+
energy_list = np.array(energy_list)
|
|
573
|
+
energy_list = energy_list - min(energy_list)
|
|
574
|
+
energy_list = energy_list * 627.509 # Convert to kcal/mol
|
|
575
|
+
boltzmann_weights = np.exp(-energy_list / (0.0019872041 * temperature))
|
|
576
|
+
|
|
577
|
+
# Apply frequency penalty: exp(-α × N_i)
|
|
578
|
+
visit_counts = np.array(visit_counts)
|
|
579
|
+
frequency_penalty = np.exp(-alpha * visit_counts)
|
|
580
|
+
|
|
581
|
+
# Combined probability
|
|
582
|
+
penalized_weights = boltzmann_weights * frequency_penalty
|
|
583
|
+
|
|
584
|
+
# Normalize
|
|
585
|
+
penalized_distribution = penalized_weights / np.sum(penalized_weights)
|
|
586
|
+
|
|
587
|
+
return penalized_distribution
|
|
588
|
+
|
|
554
589
|
def get_index_from_distribution(probabilities):
|
|
555
590
|
if not abs(sum(probabilities) - 1.0) < 1e-8:
|
|
556
591
|
raise ValueError("the sum of probabilities is not 1.0")
|
|
@@ -568,7 +603,6 @@ def run_confsearch():
|
|
|
568
603
|
return i
|
|
569
604
|
|
|
570
605
|
|
|
571
|
-
|
|
572
606
|
def calc_distance_matrix(geom_num_list):
|
|
573
607
|
natoms = len(geom_num_list)
|
|
574
608
|
combination_natoms = int(natoms * (natoms - 1) / 2)
|
|
@@ -582,8 +616,8 @@ def run_confsearch():
|
|
|
582
616
|
return distance_matrix
|
|
583
617
|
|
|
584
618
|
def sort_distance_matrix(distance_matrix):
|
|
585
|
-
|
|
586
|
-
return
|
|
619
|
+
sorted_dist_matrix = np.sort(distance_matrix)
|
|
620
|
+
return sorted_dist_matrix
|
|
587
621
|
|
|
588
622
|
def check_identical(geom_num_list_1, geom_num_list_2, threshold=1e-3):
|
|
589
623
|
distance_matrix_1 = calc_distance_matrix(geom_num_list_1)
|
|
@@ -615,6 +649,81 @@ def run_confsearch():
|
|
|
615
649
|
|
|
616
650
|
return geom_num_list, element_list
|
|
617
651
|
|
|
652
|
+
def get_bond_connectivity_table(geom_num_list, element_list):
|
|
653
|
+
"""
|
|
654
|
+
Calculate bond connectivity table from geometry and element list.
|
|
655
|
+
The geometry should be in Angstrom units.
|
|
656
|
+
Returns a sorted list of bond pairs for comparison.
|
|
657
|
+
"""
|
|
658
|
+
BC = BondConnectivity()
|
|
659
|
+
# Convert Angstrom to Bohr for BondConnectivity class
|
|
660
|
+
geom_bohr = geom_num_list * ang2bohr
|
|
661
|
+
bond_connect_mat = BC.bond_connect_matrix(element_list, geom_bohr)
|
|
662
|
+
bond_table = BC.bond_connect_table(bond_connect_mat)
|
|
663
|
+
# Sort the bond table for consistent comparison
|
|
664
|
+
sorted_bond_table = sorted([tuple(sorted(bond)) for bond in bond_table])
|
|
665
|
+
return sorted_bond_table
|
|
666
|
+
|
|
667
|
+
def check_bond_connectivity_preserved(init_bond_table, conformer_bond_table):
|
|
668
|
+
"""
|
|
669
|
+
Check if the bond connectivity is preserved between initial structure and conformer.
|
|
670
|
+
Returns a tuple: (is_preserved, added_bonds, removed_bonds)
|
|
671
|
+
"""
|
|
672
|
+
if init_bond_table == conformer_bond_table:
|
|
673
|
+
print("Bond connectivity is preserved.")
|
|
674
|
+
return True, [], []
|
|
675
|
+
else:
|
|
676
|
+
# Find added and removed bonds
|
|
677
|
+
init_set = set(init_bond_table)
|
|
678
|
+
conformer_set = set(conformer_bond_table)
|
|
679
|
+
|
|
680
|
+
added_bonds = list(conformer_set - init_set)
|
|
681
|
+
removed_bonds = list(init_set - conformer_set)
|
|
682
|
+
|
|
683
|
+
if added_bonds:
|
|
684
|
+
print(f"New bonds formed: {added_bonds}")
|
|
685
|
+
if removed_bonds:
|
|
686
|
+
print(f"Bonds broken: {removed_bonds}")
|
|
687
|
+
|
|
688
|
+
print("Bond connectivity is NOT preserved. This conformer will be excluded.")
|
|
689
|
+
return False, added_bonds, removed_bonds
|
|
690
|
+
|
|
691
|
+
def save_rejected_conformer(conformer, element_list, energy, trial_num,
|
|
692
|
+
rejected_folder, init_INPUT, added_bonds, removed_bonds,
|
|
693
|
+
rejected_count):
|
|
694
|
+
"""
|
|
695
|
+
Save a conformer that was rejected due to bond connectivity change.
|
|
696
|
+
"""
|
|
697
|
+
no_ext_init_INPUT = os.path.splitext(os.path.basename(init_INPUT))[0]
|
|
698
|
+
|
|
699
|
+
# Save xyz file
|
|
700
|
+
rejected_file_name = f"{rejected_folder}/{no_ext_init_INPUT}_REJECTED{rejected_count}.xyz"
|
|
701
|
+
with open(rejected_file_name, 'w') as f:
|
|
702
|
+
f.write(str(len(conformer))+"\n")
|
|
703
|
+
f.write(f"Rejected_Trial{trial_num}_Energy{energy:.8f}\n")
|
|
704
|
+
for i in range(len(conformer)):
|
|
705
|
+
f.write(f"{element_list[i]} {conformer[i][0]} {conformer[i][1]} {conformer[i][2]}\n")
|
|
706
|
+
|
|
707
|
+
# Save bond change information
|
|
708
|
+
info_file_name = f"{rejected_folder}/{no_ext_init_INPUT}_REJECTED{rejected_count}_info.txt"
|
|
709
|
+
with open(info_file_name, 'w') as f:
|
|
710
|
+
f.write(f"Rejected Conformer Information\n")
|
|
711
|
+
f.write(f"="*50 + "\n")
|
|
712
|
+
f.write(f"Trial number: {trial_num}\n")
|
|
713
|
+
f.write(f"Energy: {energy}\n")
|
|
714
|
+
f.write(f"XYZ file: {no_ext_init_INPUT}_REJECTED{rejected_count}.xyz\n")
|
|
715
|
+
f.write(f"\nBond Connectivity Changes:\n")
|
|
716
|
+
if added_bonds:
|
|
717
|
+
f.write(f" New bonds formed:\n")
|
|
718
|
+
for bond in added_bonds:
|
|
719
|
+
f.write(f" {bond[0]+1} - {bond[1]+1}\n")
|
|
720
|
+
if removed_bonds:
|
|
721
|
+
f.write(f" Bonds broken:\n")
|
|
722
|
+
for bond in removed_bonds:
|
|
723
|
+
f.write(f" {bond[0]+1} - {bond[1]+1}\n")
|
|
724
|
+
|
|
725
|
+
return rejected_file_name
|
|
726
|
+
|
|
618
727
|
def conformation_search(parser):
|
|
619
728
|
parser.add_argument("-bf", "--base_force", type=float, default=100.0, help='bias force to search conformations (default: 100.0 kJ)')
|
|
620
729
|
parser.add_argument("-ms", "--max_samples", type=int, default=50, help='the number of trial of calculation (default: 50)')
|
|
@@ -623,6 +732,9 @@ def run_confsearch():
|
|
|
623
732
|
parser.add_argument("-tgta", "--target_atoms", nargs="*", type=str, help='the atom to add bias force to perform conformational search (ex.) 1,2,3 or 1-3', default=None)
|
|
624
733
|
parser.add_argument("-st", "--sampling_temperature", type=float, help='set temperature to select conformer using Boltzmann distribution (default) 298.15 (K)', default=298.15)
|
|
625
734
|
parser.add_argument("-nost", "--no_stochastic", action="store_true", help='no switching EQ structure during conformation sampling')
|
|
735
|
+
parser.add_argument("-pbc", "--preserve_bond_connectivity", action="store_true", help='exclude conformers with different bond connectivity from the initial optimized structure')
|
|
736
|
+
parser.add_argument("-tabu", "--tabu_search", action="store_true", help='enable Tabu Search / Metadynamics-like frequency penalty to avoid revisiting the same conformers')
|
|
737
|
+
parser.add_argument("-alpha", "--tabu_alpha", type=float, default=0.5, help='penalty coefficient for Tabu Search (default: 0.5). Larger values = stronger penalty for frequently visited conformers')
|
|
626
738
|
return parser
|
|
627
739
|
|
|
628
740
|
def return_pair_idx(i, j):
|
|
@@ -678,6 +790,75 @@ def run_confsearch():
|
|
|
678
790
|
|
|
679
791
|
return energy_list
|
|
680
792
|
|
|
793
|
+
def read_visit_counts_file(file_name):
|
|
794
|
+
"""
|
|
795
|
+
Read visit counts file and return visit counts list.
|
|
796
|
+
Format:
|
|
797
|
+
0
|
|
798
|
+
2
|
|
799
|
+
1
|
|
800
|
+
....
|
|
801
|
+
"""
|
|
802
|
+
with open(file_name, 'r') as f:
|
|
803
|
+
data = f.read().splitlines()
|
|
804
|
+
|
|
805
|
+
visit_counts = []
|
|
806
|
+
|
|
807
|
+
for i in range(len(data)):
|
|
808
|
+
splitted_data = data[i].split()
|
|
809
|
+
if len(splitted_data) == 0:
|
|
810
|
+
continue
|
|
811
|
+
visit_counts.append(int(splitted_data[0]))
|
|
812
|
+
|
|
813
|
+
return visit_counts
|
|
814
|
+
|
|
815
|
+
def read_rejected_count_file(file_name):
|
|
816
|
+
"""
|
|
817
|
+
Read rejected count file and return the count.
|
|
818
|
+
"""
|
|
819
|
+
if not os.path.exists(file_name):
|
|
820
|
+
return 0
|
|
821
|
+
with open(file_name, 'r') as f:
|
|
822
|
+
data = f.read().strip()
|
|
823
|
+
if data:
|
|
824
|
+
return int(data)
|
|
825
|
+
return 0
|
|
826
|
+
|
|
827
|
+
def read_bond_table_file(file_name):
|
|
828
|
+
"""
|
|
829
|
+
Read bond table from file.
|
|
830
|
+
Format:
|
|
831
|
+
1 2
|
|
832
|
+
1 3
|
|
833
|
+
2 4
|
|
834
|
+
...
|
|
835
|
+
"""
|
|
836
|
+
if not os.path.exists(file_name):
|
|
837
|
+
return None
|
|
838
|
+
|
|
839
|
+
bond_table = []
|
|
840
|
+
with open(file_name, 'r') as f:
|
|
841
|
+
for line in f:
|
|
842
|
+
line = line.strip()
|
|
843
|
+
if line and not line.startswith('#'):
|
|
844
|
+
parts = line.split()
|
|
845
|
+
if len(parts) >= 2:
|
|
846
|
+
# Convert from 1-indexed (file) to 0-indexed (internal)
|
|
847
|
+
bond_table.append((int(parts[0])-1, int(parts[1])-1))
|
|
848
|
+
|
|
849
|
+
return sorted(bond_table)
|
|
850
|
+
|
|
851
|
+
def save_bond_table_file(bond_table, file_name):
|
|
852
|
+
"""
|
|
853
|
+
Save bond table to file.
|
|
854
|
+
"""
|
|
855
|
+
with open(file_name, 'w') as f:
|
|
856
|
+
f.write("# Reference Bond Connectivity Table (1-indexed)\n")
|
|
857
|
+
f.write("# Obtained from initial optimized structure (EQ0)\n")
|
|
858
|
+
for bond in bond_table:
|
|
859
|
+
# Convert from 0-indexed (internal) to 1-indexed (file)
|
|
860
|
+
f.write(f"{bond[0]+1} {bond[1]+1}\n")
|
|
861
|
+
|
|
681
862
|
def make_tgt_atom_pair(geom_num_list, element_list, target_atoms):
|
|
682
863
|
norm_dist_min = 1.0
|
|
683
864
|
norm_dist_max = 8.0
|
|
@@ -726,27 +907,92 @@ def run_confsearch():
|
|
|
726
907
|
idx = get_index_from_distribution(boltzmann_distribution)
|
|
727
908
|
return idx
|
|
728
909
|
|
|
910
|
+
def switch_conformer_with_tabu(energy_list, visit_counts, temperature=298.15, alpha=0.5):
|
|
911
|
+
"""
|
|
912
|
+
Select conformer using Boltzmann distribution with frequency penalty.
|
|
913
|
+
This implements a Tabu Search / Metadynamics-like approach.
|
|
914
|
+
"""
|
|
915
|
+
penalized_distribution = calc_penalized_boltzmann_distribution(
|
|
916
|
+
energy_list, visit_counts, temperature, alpha
|
|
917
|
+
)
|
|
918
|
+
idx = get_index_from_distribution(penalized_distribution)
|
|
919
|
+
return idx, penalized_distribution
|
|
729
920
|
|
|
921
|
+
# ========== Main execution starts here ==========
|
|
922
|
+
|
|
730
923
|
parser = multioptpy.interface.init_parser()
|
|
731
924
|
parser = conformation_search(parser)
|
|
732
925
|
|
|
733
926
|
args = multioptpy.interface.optimizeparser(parser)
|
|
734
927
|
no_stochastic = args.no_stochastic
|
|
928
|
+
preserve_bond_connectivity = args.preserve_bond_connectivity
|
|
929
|
+
use_tabu_search = args.tabu_search
|
|
930
|
+
tabu_alpha = args.tabu_alpha
|
|
735
931
|
init_geom_num_list, init_element_list = read_xyz(args.INPUT)
|
|
736
932
|
sampling_temperature = args.sampling_temperature
|
|
737
|
-
|
|
933
|
+
date_str = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
934
|
+
folder_name = os.path.splitext(args.INPUT)[0]+"_"+str(int(args.base_force))+"KJ_CS_"+date_str
|
|
738
935
|
|
|
739
936
|
if not os.path.exists(folder_name):
|
|
740
937
|
os.makedirs(folder_name)
|
|
741
938
|
|
|
939
|
+
# Create folder for rejected conformers (bond connectivity changed)
|
|
940
|
+
rejected_folder = folder_name + "/REJECTED_CONFORMERS"
|
|
941
|
+
if preserve_bond_connectivity:
|
|
942
|
+
if not os.path.exists(rejected_folder):
|
|
943
|
+
os.makedirs(rejected_folder)
|
|
944
|
+
|
|
945
|
+
# Log Tabu Search settings
|
|
946
|
+
if use_tabu_search:
|
|
947
|
+
print(f"Tabu Search / Metadynamics-like frequency penalty is enabled (alpha = {tabu_alpha}).")
|
|
948
|
+
with open(folder_name+"/tabu_search_settings.log", "w") as f:
|
|
949
|
+
f.write("Tabu Search / Metadynamics-like Frequency Penalty Settings\n")
|
|
950
|
+
f.write("="*60 + "\n")
|
|
951
|
+
f.write(f"Alpha (penalty coefficient): {tabu_alpha}\n")
|
|
952
|
+
f.write(f"Formula: P_i^select ∝ P_i^Boltzmann × exp(-α × N_i)\n")
|
|
953
|
+
f.write(f" where N_i is the number of times conformer i has been selected.\n")
|
|
742
954
|
|
|
743
955
|
energy_list_file_path = folder_name+"/EQ_energy.dat"
|
|
956
|
+
visit_counts_file_path = folder_name+"/visit_counts.dat"
|
|
957
|
+
rejected_count_file_path = rejected_folder+"/rejected_count.dat"
|
|
958
|
+
rejected_energy_file_path = rejected_folder+"/rejected_energy.dat"
|
|
959
|
+
reference_bond_table_file_path = folder_name+"/reference_bond_table.dat"
|
|
960
|
+
|
|
744
961
|
if os.path.exists(energy_list_file_path):
|
|
745
962
|
energy_list = read_energy_file(energy_list_file_path)
|
|
746
|
-
|
|
747
963
|
else:
|
|
748
964
|
energy_list = []
|
|
749
965
|
|
|
966
|
+
# Initialize or load visit counts for Tabu Search
|
|
967
|
+
if use_tabu_search:
|
|
968
|
+
if os.path.exists(visit_counts_file_path):
|
|
969
|
+
visit_counts = read_visit_counts_file(visit_counts_file_path)
|
|
970
|
+
# Extend visit_counts if energy_list is longer (new conformers added)
|
|
971
|
+
while len(visit_counts) < len(energy_list):
|
|
972
|
+
visit_counts.append(0)
|
|
973
|
+
else:
|
|
974
|
+
visit_counts = [0] * len(energy_list)
|
|
975
|
+
else:
|
|
976
|
+
visit_counts = None
|
|
977
|
+
|
|
978
|
+
# Initialize or load rejected conformer count
|
|
979
|
+
if preserve_bond_connectivity:
|
|
980
|
+
bond_reject_count = read_rejected_count_file(rejected_count_file_path)
|
|
981
|
+
# Load rejected energies if exists
|
|
982
|
+
if os.path.exists(rejected_energy_file_path):
|
|
983
|
+
rejected_energy_list = read_energy_file(rejected_energy_file_path)
|
|
984
|
+
else:
|
|
985
|
+
rejected_energy_list = []
|
|
986
|
+
# Try to load reference bond table (from previous run)
|
|
987
|
+
init_bond_table = read_bond_table_file(reference_bond_table_file_path)
|
|
988
|
+
if init_bond_table is not None:
|
|
989
|
+
print("Loaded reference bond connectivity from previous run.")
|
|
990
|
+
print(f"Reference bond table: {init_bond_table}")
|
|
991
|
+
else:
|
|
992
|
+
bond_reject_count = 0
|
|
993
|
+
rejected_energy_list = []
|
|
994
|
+
init_bond_table = None
|
|
995
|
+
|
|
750
996
|
with open(folder_name+"/input.txt", "a") as f:
|
|
751
997
|
f.write(str(vars(args))+"\n")
|
|
752
998
|
|
|
@@ -770,26 +1016,73 @@ def run_confsearch():
|
|
|
770
1016
|
else:
|
|
771
1017
|
count = len(energy_list) - 1
|
|
772
1018
|
reason = ""
|
|
1019
|
+
|
|
773
1020
|
if len(energy_list) == 0:
|
|
774
1021
|
print("initial conformer.")
|
|
775
1022
|
bpa = multioptpy.optimization.Optimize(args)
|
|
776
1023
|
bpa.run()
|
|
777
1024
|
if not bpa.optimized_flag:
|
|
778
1025
|
print("Optimization is failed. Exit...")
|
|
779
|
-
exit()
|
|
1026
|
+
sys.exit(1)
|
|
780
1027
|
energy = bpa.final_energy
|
|
781
1028
|
init_conformer = bpa.final_geometry #Bohr
|
|
782
1029
|
init_conformer = init_conformer * bohr2ang #Angstrom
|
|
1030
|
+
|
|
1031
|
+
# Store bond connectivity from the initial OPTIMIZED structure
|
|
1032
|
+
if preserve_bond_connectivity:
|
|
1033
|
+
init_bond_table = get_bond_connectivity_table(init_conformer, init_element_list)
|
|
1034
|
+
print("Bond connectivity preservation is enabled.")
|
|
1035
|
+
print("Reference bond connectivity is determined from the initial OPTIMIZED structure (EQ0).")
|
|
1036
|
+
print(f"Reference bond table: {init_bond_table}")
|
|
1037
|
+
|
|
1038
|
+
# Save reference bond table to file for restart
|
|
1039
|
+
save_bond_table_file(init_bond_table, reference_bond_table_file_path)
|
|
1040
|
+
|
|
1041
|
+
# Write human-readable log
|
|
1042
|
+
with open(folder_name+"/reference_bond_connectivity.log", "w") as f:
|
|
1043
|
+
f.write("Reference Bond Connectivity Table\n")
|
|
1044
|
+
f.write("="*50 + "\n")
|
|
1045
|
+
f.write("Obtained from: Initial OPTIMIZED structure (EQ0)\n")
|
|
1046
|
+
f.write("Note: This is the structure after optimization of the input XYZ file.\n")
|
|
1047
|
+
f.write("\nBond List (1-indexed):\n")
|
|
1048
|
+
for bond in init_bond_table:
|
|
1049
|
+
f.write(f" {bond[0]+1} - {bond[1]+1}\n")
|
|
1050
|
+
f.write(f"\nTotal number of bonds: {len(init_bond_table)}\n")
|
|
1051
|
+
|
|
1052
|
+
# Create README for rejected folder
|
|
1053
|
+
with open(rejected_folder+"/README.txt", "w") as f:
|
|
1054
|
+
f.write("Rejected Conformers due to Bond Connectivity Change\n")
|
|
1055
|
+
f.write("="*55 + "\n\n")
|
|
1056
|
+
f.write("This folder contains conformers that were excluded from the\n")
|
|
1057
|
+
f.write("main conformer search results because their bond connectivity\n")
|
|
1058
|
+
f.write("differs from the initial OPTIMIZED structure (EQ0).\n\n")
|
|
1059
|
+
f.write("Reference structure: EQ0 (optimized from input XYZ)\n\n")
|
|
1060
|
+
f.write("Each rejected conformer has:\n")
|
|
1061
|
+
f.write(" - XYZ file: *_REJECTEDn.xyz\n")
|
|
1062
|
+
f.write(" - Info file: *_REJECTEDn_info.txt (contains bond change details)\n\n")
|
|
1063
|
+
f.write("These structures may represent:\n")
|
|
1064
|
+
f.write(" - Reaction products\n")
|
|
1065
|
+
f.write(" - Isomers with different connectivity\n")
|
|
1066
|
+
f.write(" - Structures with broken/formed bonds\n")
|
|
1067
|
+
|
|
783
1068
|
energy_list.append(energy)
|
|
784
1069
|
with open(energy_list_file_path, 'a') as f:
|
|
785
1070
|
f.write(str(energy)+"\n")
|
|
1071
|
+
|
|
1072
|
+
# Initialize visit count for the first conformer
|
|
1073
|
+
if use_tabu_search:
|
|
1074
|
+
visit_counts.append(0)
|
|
1075
|
+
with open(visit_counts_file_path, 'w') as f:
|
|
1076
|
+
for vc in visit_counts:
|
|
1077
|
+
f.write(str(vc)+"\n")
|
|
1078
|
+
|
|
786
1079
|
print("initial conformer.")
|
|
787
1080
|
print("Energy: ", energy)
|
|
788
1081
|
save_xyz_file(init_conformer, init_element_list, folder_name+"/"+init_INPUT, "EQ"+str(0))
|
|
789
1082
|
|
|
790
1083
|
if len(atom_pair_list) == 0:
|
|
791
1084
|
print("Cannot make atom_pair list. exit...")
|
|
792
|
-
exit()
|
|
1085
|
+
sys.exit(1)
|
|
793
1086
|
else:
|
|
794
1087
|
with open(folder_name+"/search_atom_pairs.log", 'a') as f:
|
|
795
1088
|
for atom_pair in atom_pair_list:
|
|
@@ -830,26 +1123,71 @@ def run_confsearch():
|
|
|
830
1123
|
energy = bpa.final_energy
|
|
831
1124
|
conformer = bpa.final_geometry #Bohr
|
|
832
1125
|
conformer = conformer * bohr2ang #Angstrom
|
|
1126
|
+
|
|
1127
|
+
# Check bond connectivity if preservation is enabled
|
|
1128
|
+
bond_preserved = True
|
|
1129
|
+
added_bonds = []
|
|
1130
|
+
removed_bonds = []
|
|
1131
|
+
if preserve_bond_connectivity:
|
|
1132
|
+
conformer_bond_table = get_bond_connectivity_table(conformer, init_element_list)
|
|
1133
|
+
bond_preserved, added_bonds, removed_bonds = check_bond_connectivity_preserved(init_bond_table, conformer_bond_table)
|
|
1134
|
+
if not bond_preserved:
|
|
1135
|
+
# Save rejected conformer to separate folder
|
|
1136
|
+
rejected_file = save_rejected_conformer(
|
|
1137
|
+
conformer, init_element_list, energy, i,
|
|
1138
|
+
rejected_folder, init_INPUT, added_bonds, removed_bonds,
|
|
1139
|
+
bond_reject_count
|
|
1140
|
+
)
|
|
1141
|
+
print(f"Rejected conformer saved to: {rejected_file}")
|
|
1142
|
+
|
|
1143
|
+
# Update rejected energy list
|
|
1144
|
+
rejected_energy_list.append(energy)
|
|
1145
|
+
with open(rejected_energy_file_path, 'a') as f:
|
|
1146
|
+
f.write(str(energy)+"\n")
|
|
1147
|
+
|
|
1148
|
+
# Update rejected count
|
|
1149
|
+
bond_reject_count += 1
|
|
1150
|
+
with open(rejected_count_file_path, 'w') as f:
|
|
1151
|
+
f.write(str(bond_reject_count))
|
|
1152
|
+
|
|
1153
|
+
# Log to summary file
|
|
1154
|
+
with open(rejected_folder+"/rejected_summary.log", "a") as f:
|
|
1155
|
+
f.write(f"REJECTED{bond_reject_count-1}: Trial {i}, Energy {energy:.8f}\n")
|
|
1156
|
+
if added_bonds:
|
|
1157
|
+
f.write(f" New bonds: {[(b[0]+1, b[1]+1) for b in added_bonds]}\n")
|
|
1158
|
+
if removed_bonds:
|
|
1159
|
+
f.write(f" Broken bonds: {[(b[0]+1, b[1]+1) for b in removed_bonds]}\n")
|
|
1160
|
+
|
|
833
1161
|
# Check identical
|
|
834
1162
|
bool_identical = is_identical(conformer, energy, energy_list, folder_name, init_INPUT)
|
|
835
1163
|
else:
|
|
836
1164
|
optimized_flag = False
|
|
837
1165
|
bool_identical = True
|
|
1166
|
+
bond_preserved = True # Not applicable when DC_check_flag is True
|
|
838
1167
|
|
|
839
1168
|
|
|
840
|
-
if bool_identical or not optimized_flag or DC_check_flag:
|
|
1169
|
+
if bool_identical or not optimized_flag or DC_check_flag or not bond_preserved:
|
|
841
1170
|
if not optimized_flag:
|
|
842
1171
|
print("Optimization is failed...")
|
|
843
1172
|
if DC_check_flag:
|
|
844
1173
|
print("DC is detected...")
|
|
1174
|
+
if not bond_preserved:
|
|
1175
|
+
print("Bond connectivity changed. Conformer saved to REJECTED_CONFORMERS folder.")
|
|
845
1176
|
|
|
846
1177
|
else:
|
|
847
1178
|
count += 1
|
|
848
1179
|
energy_list.append(energy)
|
|
849
1180
|
|
|
850
1181
|
with open(energy_list_file_path, 'w') as f:
|
|
851
|
-
for
|
|
852
|
-
f.write(str(
|
|
1182
|
+
for ene in energy_list:
|
|
1183
|
+
f.write(str(ene)+"\n")
|
|
1184
|
+
|
|
1185
|
+
# Add visit count for new conformer
|
|
1186
|
+
if use_tabu_search:
|
|
1187
|
+
visit_counts.append(0)
|
|
1188
|
+
with open(visit_counts_file_path, 'w') as f:
|
|
1189
|
+
for vc in visit_counts:
|
|
1190
|
+
f.write(str(vc)+"\n")
|
|
853
1191
|
|
|
854
1192
|
print("Find new conformer.")
|
|
855
1193
|
print("Energy: ", energy)
|
|
@@ -883,16 +1221,44 @@ def run_confsearch():
|
|
|
883
1221
|
if no_stochastic:
|
|
884
1222
|
idx = 0
|
|
885
1223
|
else:
|
|
886
|
-
if
|
|
887
|
-
|
|
1224
|
+
if use_tabu_search:
|
|
1225
|
+
# Use Tabu Search / Metadynamics-like frequency penalty
|
|
1226
|
+
if i % 5 == 0:
|
|
1227
|
+
idx, prob_dist = switch_conformer_with_tabu(
|
|
1228
|
+
energy_list, visit_counts, sampling_temperature*10, tabu_alpha
|
|
1229
|
+
)
|
|
1230
|
+
else:
|
|
1231
|
+
idx, prob_dist = switch_conformer_with_tabu(
|
|
1232
|
+
energy_list, visit_counts, sampling_temperature, tabu_alpha)
|
|
1233
|
+
|
|
1234
|
+
# Update visit count for selected conformer
|
|
1235
|
+
visit_counts[idx] += 1
|
|
1236
|
+
with open(visit_counts_file_path, 'w') as f:
|
|
1237
|
+
for vc in visit_counts:
|
|
1238
|
+
f.write(str(vc)+"\n")
|
|
1239
|
+
|
|
1240
|
+
# Log selection probabilities (periodically for debugging)
|
|
1241
|
+
if i % 10 == 0:
|
|
1242
|
+
with open(folder_name+"/tabu_selection_log.log", "a") as f:
|
|
1243
|
+
f.write(f"\nTrial {i}: Selection probabilities (with frequency penalty)\n")
|
|
1244
|
+
f.write(f" Visit counts: {visit_counts}\n")
|
|
1245
|
+
f.write(f" Probabilities: {[f'{p:.4f}' for p in prob_dist]}\n")
|
|
1246
|
+
f.write(f" Selected: EQ{idx}\n")
|
|
888
1247
|
else:
|
|
889
|
-
|
|
1248
|
+
# Standard Boltzmann selection
|
|
1249
|
+
if i % 5 == 0:
|
|
1250
|
+
idx = switch_conformer(energy_list, sampling_temperature*10)
|
|
1251
|
+
else:
|
|
1252
|
+
idx = switch_conformer(energy_list, sampling_temperature)
|
|
890
1253
|
|
|
891
1254
|
no_ext_init_INPUT = os.path.splitext(init_INPUT)[0]
|
|
892
1255
|
args.INPUT = folder_name + "/" + no_ext_init_INPUT + "_EQ" + str(idx) + ".xyz"
|
|
893
1256
|
print("Switch conformer: EQ"+str(idx))
|
|
894
1257
|
with open(folder_name+"/switch_conformer.log", 'a') as f:
|
|
895
|
-
|
|
1258
|
+
if use_tabu_search:
|
|
1259
|
+
f.write(f"Trial {i}: Switch conformer: EQ{idx} (visit_count={visit_counts[idx]})\n")
|
|
1260
|
+
else:
|
|
1261
|
+
f.write("Trial "+str(i)+": Switch conformer: EQ"+str(idx)+"\n")
|
|
896
1262
|
else:
|
|
897
1263
|
args.INPUT = init_INPUT
|
|
898
1264
|
|
|
@@ -900,6 +1266,7 @@ def run_confsearch():
|
|
|
900
1266
|
else:
|
|
901
1267
|
print("Max samples are reached. Exit....")
|
|
902
1268
|
reason = "Max samples are reached. Exit...."
|
|
1269
|
+
|
|
903
1270
|
energy_list_suumary_file_path = folder_name+"/EQ_summary.log"
|
|
904
1271
|
with open(energy_list_suumary_file_path, "w") as f:
|
|
905
1272
|
f.write("Summary\n"+"Reason of Termination: "+reason+"\n")
|
|
@@ -911,6 +1278,29 @@ def run_confsearch():
|
|
|
911
1278
|
f.write("conformer of highest energy: "+str(max(energy_list))+"\n")
|
|
912
1279
|
print("structure of highest energy: ", "EQ"+str(energy_list.index(max(energy_list))))
|
|
913
1280
|
f.write("structure of highest energy: "+"EQ"+str(energy_list.index(max(energy_list)))+"\n")
|
|
1281
|
+
if preserve_bond_connectivity:
|
|
1282
|
+
f.write(f"\nBond Connectivity Preservation Results:\n")
|
|
1283
|
+
f.write(f" Reference structure: EQ0 (initial optimized structure)\n")
|
|
1284
|
+
f.write(f" Conformers rejected: {bond_reject_count}\n")
|
|
1285
|
+
f.write(f" Rejected conformers saved in: REJECTED_CONFORMERS/\n")
|
|
1286
|
+
if rejected_energy_list:
|
|
1287
|
+
f.write(f" Rejected conformer energies:\n")
|
|
1288
|
+
for rej_idx, rej_ene in enumerate(rejected_energy_list):
|
|
1289
|
+
f.write(f" REJECTED{rej_idx}: {rej_ene}\n")
|
|
1290
|
+
f.write(f" Lowest rejected energy: {min(rejected_energy_list)}\n")
|
|
1291
|
+
f.write(f" Highest rejected energy: {max(rejected_energy_list)}\n")
|
|
1292
|
+
if use_tabu_search:
|
|
1293
|
+
f.write(f"\nTabu Search Statistics:\n")
|
|
1294
|
+
f.write(f" Alpha (penalty coefficient): {tabu_alpha}\n")
|
|
1295
|
+
f.write(f" Final visit counts: {visit_counts}\n")
|
|
1296
|
+
total_visits = sum(visit_counts) if visit_counts else 0
|
|
1297
|
+
f.write(f" Total conformer switches: {total_visits}\n")
|
|
914
1298
|
|
|
915
1299
|
|
|
916
|
-
print("Conformation search is finished.")
|
|
1300
|
+
print("Conformation search is finished.")
|
|
1301
|
+
if preserve_bond_connectivity:
|
|
1302
|
+
print(f"Total conformers rejected due to bond connectivity change: {bond_reject_count}")
|
|
1303
|
+
if bond_reject_count > 0:
|
|
1304
|
+
print(f"Rejected conformers saved in: {rejected_folder}/")
|
|
1305
|
+
if use_tabu_search:
|
|
1306
|
+
print(f"Tabu Search - Final visit counts: {visit_counts}")
|
multioptpy/moleculardynamics.py
CHANGED
|
@@ -119,35 +119,45 @@ class MD:
|
|
|
119
119
|
|
|
120
120
|
|
|
121
121
|
def exec_md(self, TM, geom_num_list, prev_geom_num_list, B_g, B_e, pre_B_g, iter):
|
|
122
|
+
# Initialize SHAKE constraint if applicable
|
|
122
123
|
if iter == 0 and len(self.constraint_condition_list) > 0:
|
|
123
124
|
self.class_SHAKE = SHAKE(TM.delta_timescale, self.constraint_condition_list)
|
|
124
|
-
|
|
125
|
+
|
|
126
|
+
# Execute Thermostat / Integrator
|
|
127
|
+
if self.mdtype in ["nosehoover", "nvt"]:
|
|
125
128
|
new_geometry = TM.Nose_Hoover_thermostat(geom_num_list, B_g)
|
|
126
129
|
elif self.mdtype == "nosehooverchain":
|
|
127
130
|
new_geometry = TM.Nose_Hoover_chain_thermostat(geom_num_list, B_g)
|
|
128
|
-
|
|
131
|
+
|
|
132
|
+
elif self.mdtype in ["langevin", "baoab"]:
|
|
133
|
+
new_geometry = TM.Langevin_thermostat(geom_num_list, B_g)
|
|
134
|
+
|
|
135
|
+
elif self.mdtype in ["velocityverlet", "nve"]:
|
|
129
136
|
new_geometry = TM.Velocity_Verlet(geom_num_list, B_g, pre_B_g, iter)
|
|
130
137
|
else:
|
|
131
138
|
print("Unexpected method.", self.mdtype)
|
|
132
|
-
raise
|
|
139
|
+
raise ValueError(f"Unknown MD type: {self.mdtype}")
|
|
133
140
|
|
|
141
|
+
# Apply SHAKE constraints
|
|
134
142
|
if iter > 0 and len(self.constraint_condition_list) > 0:
|
|
135
|
-
|
|
136
143
|
new_geometry, tmp_momentum_list = self.class_SHAKE.run(new_geometry, prev_geom_num_list, TM.momentum_list, TM.element_list)
|
|
137
144
|
TM.momentum_list = copy.copy(tmp_momentum_list)
|
|
138
145
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
for i in range(len(geom_num_list)):
|
|
142
|
-
kinetic_ene += np.sum(TM.momentum_list[i] ** 2) / (2 * atomic_mass(TM.element_list[i]))
|
|
146
|
+
# [Optimization] Calculate kinetic energy using vectorized method in Thermostat
|
|
147
|
+
kinetic_ene = TM.calc_tot_kinetic_energy()
|
|
143
148
|
|
|
144
149
|
tot_energy = B_e + kinetic_ene
|
|
145
150
|
print("hamiltonian :", tot_energy, "hartree")
|
|
146
151
|
|
|
147
152
|
self.tot_energy_list.append(tot_energy)
|
|
148
153
|
|
|
154
|
+
# Apply Periodic Boundary Condition
|
|
149
155
|
if len(self.pbc_box) > 0:
|
|
150
156
|
new_geometry = apply_periodic_boundary_condition(new_geometry, TM.element_list, self.pbc_box)
|
|
157
|
+
# Ensure new_geometry remains a numpy array after PBC application
|
|
158
|
+
if not isinstance(new_geometry, np.ndarray):
|
|
159
|
+
new_geometry = np.array(new_geometry, dtype=np.float64)
|
|
160
|
+
|
|
151
161
|
return new_geometry
|
|
152
162
|
|
|
153
163
|
|
|
@@ -221,11 +231,9 @@ class MD:
|
|
|
221
231
|
#-----------------------------------
|
|
222
232
|
with open(self.BPA_FOLDER_DIRECTORY+"input.txt", "w") as f:
|
|
223
233
|
f.write(str(vars(self.args)))
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
pre_B_g.append(np.array([0,0,0], dtype="float64"))
|
|
228
|
-
pre_B_g = np.array(pre_B_g, dtype="float64")
|
|
234
|
+
|
|
235
|
+
# [Optimization] Efficient initialization of pre_B_g
|
|
236
|
+
pre_B_g = np.zeros((len(element_list), 3), dtype=np.float64)
|
|
229
237
|
|
|
230
238
|
#-------------------------------------
|
|
231
239
|
finish_frag = False
|