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/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
- sort_distance_matrix = np.sort(distance_matrix)
586
- return sort_distance_matrix
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
- folder_name = os.path.splitext(args.INPUT)[0]+"_"+str(int(args.base_force))+"KJ_CS_REPORT"
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 energy in energy_list:
852
- f.write(str(energy)+"\n")
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 i % 5 == 0:
887
- idx = switch_conformer(energy_list, sampling_temperature*10)
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
- idx = switch_conformer(energy_list, sampling_temperature)
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
- f.write("Trial "+str(i)+": Switch conformer: EQ"+str(idx)+"\n")
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}")
@@ -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
- if self.mdtype == "nosehoover" or self.mdtype == "nvt":
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
- elif self.mdtype == "velocityverlet" or self.mdtype == "nve":
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
- kinetic_ene = 0.0
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
- pre_B_g = []
225
-
226
- for i in range(len(element_list)):
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