MultiOptPy 1.20.2__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/Calculator/__init__.py +0 -0
- multioptpy/Calculator/ase_calculation_tools.py +424 -0
- multioptpy/Calculator/ase_tools/__init__.py +0 -0
- multioptpy/Calculator/ase_tools/fairchem.py +28 -0
- multioptpy/Calculator/ase_tools/gamess.py +19 -0
- multioptpy/Calculator/ase_tools/gaussian.py +165 -0
- multioptpy/Calculator/ase_tools/mace.py +28 -0
- multioptpy/Calculator/ase_tools/mopac.py +19 -0
- multioptpy/Calculator/ase_tools/nwchem.py +31 -0
- multioptpy/Calculator/ase_tools/orca.py +22 -0
- multioptpy/Calculator/ase_tools/pygfn0.py +37 -0
- multioptpy/Calculator/dxtb_calculation_tools.py +344 -0
- multioptpy/Calculator/emt_calculation_tools.py +458 -0
- multioptpy/Calculator/gpaw_calculation_tools.py +183 -0
- multioptpy/Calculator/lj_calculation_tools.py +314 -0
- multioptpy/Calculator/psi4_calculation_tools.py +334 -0
- multioptpy/Calculator/pwscf_calculation_tools.py +189 -0
- multioptpy/Calculator/pyscf_calculation_tools.py +327 -0
- multioptpy/Calculator/sqm1_calculation_tools.py +611 -0
- multioptpy/Calculator/sqm2_calculation_tools.py +376 -0
- multioptpy/Calculator/tblite_calculation_tools.py +352 -0
- multioptpy/Calculator/tersoff_calculation_tools.py +818 -0
- multioptpy/Constraint/__init__.py +0 -0
- multioptpy/Constraint/constraint_condition.py +834 -0
- multioptpy/Coordinate/__init__.py +0 -0
- multioptpy/Coordinate/polar_coordinate.py +199 -0
- multioptpy/Coordinate/redundant_coordinate.py +638 -0
- multioptpy/IRC/__init__.py +0 -0
- multioptpy/IRC/converge_criteria.py +28 -0
- multioptpy/IRC/dvv.py +544 -0
- multioptpy/IRC/euler.py +439 -0
- multioptpy/IRC/hpc.py +564 -0
- multioptpy/IRC/lqa.py +540 -0
- multioptpy/IRC/modekill.py +662 -0
- multioptpy/IRC/rk4.py +579 -0
- multioptpy/Interpolation/__init__.py +0 -0
- multioptpy/Interpolation/adaptive_interpolation.py +283 -0
- multioptpy/Interpolation/binomial_interpolation.py +179 -0
- multioptpy/Interpolation/geodesic_interpolation.py +785 -0
- multioptpy/Interpolation/interpolation.py +156 -0
- multioptpy/Interpolation/linear_interpolation.py +473 -0
- multioptpy/Interpolation/savitzky_golay_interpolation.py +252 -0
- multioptpy/Interpolation/spline_interpolation.py +353 -0
- multioptpy/MD/__init__.py +0 -0
- multioptpy/MD/thermostat.py +185 -0
- multioptpy/MEP/__init__.py +0 -0
- multioptpy/MEP/pathopt_bneb_force.py +443 -0
- multioptpy/MEP/pathopt_dmf_force.py +448 -0
- multioptpy/MEP/pathopt_dneb_force.py +130 -0
- multioptpy/MEP/pathopt_ewbneb_force.py +207 -0
- multioptpy/MEP/pathopt_gpneb_force.py +512 -0
- multioptpy/MEP/pathopt_lup_force.py +113 -0
- multioptpy/MEP/pathopt_neb_force.py +225 -0
- multioptpy/MEP/pathopt_nesb_force.py +205 -0
- multioptpy/MEP/pathopt_om_force.py +153 -0
- multioptpy/MEP/pathopt_qsm_force.py +174 -0
- multioptpy/MEP/pathopt_qsmv2_force.py +304 -0
- multioptpy/ModelFunction/__init__.py +7 -0
- multioptpy/ModelFunction/avoiding_model_function.py +29 -0
- multioptpy/ModelFunction/binary_image_ts_search_model_function.py +47 -0
- multioptpy/ModelFunction/conical_model_function.py +26 -0
- multioptpy/ModelFunction/opt_meci.py +50 -0
- multioptpy/ModelFunction/opt_mesx.py +47 -0
- multioptpy/ModelFunction/opt_mesx_2.py +49 -0
- multioptpy/ModelFunction/seam_model_function.py +27 -0
- multioptpy/ModelHessian/__init__.py +0 -0
- multioptpy/ModelHessian/approx_hessian.py +147 -0
- multioptpy/ModelHessian/calc_params.py +227 -0
- multioptpy/ModelHessian/fischer.py +236 -0
- multioptpy/ModelHessian/fischerd3.py +360 -0
- multioptpy/ModelHessian/fischerd4.py +398 -0
- multioptpy/ModelHessian/gfn0xtb.py +633 -0
- multioptpy/ModelHessian/gfnff.py +709 -0
- multioptpy/ModelHessian/lindh.py +165 -0
- multioptpy/ModelHessian/lindh2007d2.py +707 -0
- multioptpy/ModelHessian/lindh2007d3.py +822 -0
- multioptpy/ModelHessian/lindh2007d4.py +1030 -0
- multioptpy/ModelHessian/morse.py +106 -0
- multioptpy/ModelHessian/schlegel.py +144 -0
- multioptpy/ModelHessian/schlegeld3.py +322 -0
- multioptpy/ModelHessian/schlegeld4.py +559 -0
- multioptpy/ModelHessian/shortrange.py +346 -0
- multioptpy/ModelHessian/swartd2.py +496 -0
- multioptpy/ModelHessian/swartd3.py +706 -0
- multioptpy/ModelHessian/swartd4.py +918 -0
- multioptpy/ModelHessian/tshess.py +40 -0
- multioptpy/Optimizer/QHAdam.py +61 -0
- multioptpy/Optimizer/__init__.py +0 -0
- multioptpy/Optimizer/abc_fire.py +83 -0
- multioptpy/Optimizer/adabelief.py +58 -0
- multioptpy/Optimizer/adabound.py +68 -0
- multioptpy/Optimizer/adadelta.py +65 -0
- multioptpy/Optimizer/adaderivative.py +56 -0
- multioptpy/Optimizer/adadiff.py +68 -0
- multioptpy/Optimizer/adafactor.py +70 -0
- multioptpy/Optimizer/adam.py +65 -0
- multioptpy/Optimizer/adamax.py +62 -0
- multioptpy/Optimizer/adamod.py +83 -0
- multioptpy/Optimizer/adamw.py +65 -0
- multioptpy/Optimizer/adiis.py +523 -0
- multioptpy/Optimizer/afire_neb.py +282 -0
- multioptpy/Optimizer/block_hessian_update.py +709 -0
- multioptpy/Optimizer/c2diis.py +491 -0
- multioptpy/Optimizer/component_wise_scaling.py +405 -0
- multioptpy/Optimizer/conjugate_gradient.py +82 -0
- multioptpy/Optimizer/conjugate_gradient_neb.py +345 -0
- multioptpy/Optimizer/coordinate_locking.py +405 -0
- multioptpy/Optimizer/dic_rsirfo.py +1015 -0
- multioptpy/Optimizer/ediis.py +417 -0
- multioptpy/Optimizer/eve.py +76 -0
- multioptpy/Optimizer/fastadabelief.py +61 -0
- multioptpy/Optimizer/fire.py +77 -0
- multioptpy/Optimizer/fire2.py +249 -0
- multioptpy/Optimizer/fire_neb.py +92 -0
- multioptpy/Optimizer/gan_step.py +486 -0
- multioptpy/Optimizer/gdiis.py +609 -0
- multioptpy/Optimizer/gediis.py +203 -0
- multioptpy/Optimizer/geodesic_step.py +433 -0
- multioptpy/Optimizer/gpmin.py +633 -0
- multioptpy/Optimizer/gpr_step.py +364 -0
- multioptpy/Optimizer/gradientdescent.py +78 -0
- multioptpy/Optimizer/gradientdescent_neb.py +52 -0
- multioptpy/Optimizer/hessian_update.py +433 -0
- multioptpy/Optimizer/hybrid_rfo.py +998 -0
- multioptpy/Optimizer/kdiis.py +625 -0
- multioptpy/Optimizer/lars.py +21 -0
- multioptpy/Optimizer/lbfgs.py +253 -0
- multioptpy/Optimizer/lbfgs_neb.py +355 -0
- multioptpy/Optimizer/linesearch.py +236 -0
- multioptpy/Optimizer/lookahead.py +40 -0
- multioptpy/Optimizer/nadam.py +64 -0
- multioptpy/Optimizer/newton.py +200 -0
- multioptpy/Optimizer/prodigy.py +70 -0
- multioptpy/Optimizer/purtubation.py +16 -0
- multioptpy/Optimizer/quickmin_neb.py +245 -0
- multioptpy/Optimizer/radam.py +75 -0
- multioptpy/Optimizer/rfo_neb.py +302 -0
- multioptpy/Optimizer/ric_rfo.py +842 -0
- multioptpy/Optimizer/rl_step.py +627 -0
- multioptpy/Optimizer/rmspropgrave.py +65 -0
- multioptpy/Optimizer/rsirfo.py +1647 -0
- multioptpy/Optimizer/rsprfo.py +1056 -0
- multioptpy/Optimizer/sadam.py +60 -0
- multioptpy/Optimizer/samsgrad.py +63 -0
- multioptpy/Optimizer/tr_lbfgs.py +678 -0
- multioptpy/Optimizer/trim.py +273 -0
- multioptpy/Optimizer/trust_radius.py +207 -0
- multioptpy/Optimizer/trust_radius_neb.py +121 -0
- multioptpy/Optimizer/yogi.py +60 -0
- multioptpy/OtherMethod/__init__.py +0 -0
- multioptpy/OtherMethod/addf.py +1150 -0
- multioptpy/OtherMethod/dimer.py +895 -0
- multioptpy/OtherMethod/elastic_image_pair.py +629 -0
- multioptpy/OtherMethod/modelfunction.py +456 -0
- multioptpy/OtherMethod/newton_traj.py +454 -0
- multioptpy/OtherMethod/twopshs.py +1095 -0
- multioptpy/PESAnalyzer/__init__.py +0 -0
- multioptpy/PESAnalyzer/calc_irc_curvature.py +125 -0
- multioptpy/PESAnalyzer/cmds_analysis.py +152 -0
- multioptpy/PESAnalyzer/koopman_analysis.py +268 -0
- multioptpy/PESAnalyzer/pca_analysis.py +314 -0
- multioptpy/Parameters/__init__.py +0 -0
- multioptpy/Parameters/atomic_mass.py +20 -0
- multioptpy/Parameters/atomic_number.py +22 -0
- multioptpy/Parameters/covalent_radii.py +44 -0
- multioptpy/Parameters/d2.py +61 -0
- multioptpy/Parameters/d3.py +63 -0
- multioptpy/Parameters/d4.py +103 -0
- multioptpy/Parameters/dreiding.py +34 -0
- multioptpy/Parameters/gfn0xtb_param.py +137 -0
- multioptpy/Parameters/gfnff_param.py +315 -0
- multioptpy/Parameters/gnb.py +104 -0
- multioptpy/Parameters/parameter.py +22 -0
- multioptpy/Parameters/uff.py +72 -0
- multioptpy/Parameters/unit_values.py +20 -0
- multioptpy/Potential/AFIR_potential.py +55 -0
- multioptpy/Potential/LJ_repulsive_potential.py +345 -0
- multioptpy/Potential/__init__.py +0 -0
- multioptpy/Potential/anharmonic_keep_potential.py +28 -0
- multioptpy/Potential/asym_elllipsoidal_potential.py +718 -0
- multioptpy/Potential/electrostatic_potential.py +69 -0
- multioptpy/Potential/flux_potential.py +30 -0
- multioptpy/Potential/gaussian_potential.py +101 -0
- multioptpy/Potential/idpp.py +516 -0
- multioptpy/Potential/keep_angle_potential.py +146 -0
- multioptpy/Potential/keep_dihedral_angle_potential.py +105 -0
- multioptpy/Potential/keep_outofplain_angle_potential.py +70 -0
- multioptpy/Potential/keep_potential.py +99 -0
- multioptpy/Potential/mechano_force_potential.py +74 -0
- multioptpy/Potential/nanoreactor_potential.py +52 -0
- multioptpy/Potential/potential.py +896 -0
- multioptpy/Potential/spacer_model_potential.py +221 -0
- multioptpy/Potential/switching_potential.py +258 -0
- multioptpy/Potential/universal_potential.py +34 -0
- multioptpy/Potential/value_range_potential.py +36 -0
- multioptpy/Potential/void_point_potential.py +25 -0
- multioptpy/SQM/__init__.py +0 -0
- multioptpy/SQM/sqm1/__init__.py +0 -0
- multioptpy/SQM/sqm1/sqm1_core.py +1792 -0
- multioptpy/SQM/sqm2/__init__.py +0 -0
- multioptpy/SQM/sqm2/calc_tools.py +95 -0
- multioptpy/SQM/sqm2/sqm2_basis.py +850 -0
- multioptpy/SQM/sqm2/sqm2_bond.py +119 -0
- multioptpy/SQM/sqm2/sqm2_core.py +303 -0
- multioptpy/SQM/sqm2/sqm2_data.py +1229 -0
- multioptpy/SQM/sqm2/sqm2_disp.py +65 -0
- multioptpy/SQM/sqm2/sqm2_eeq.py +243 -0
- multioptpy/SQM/sqm2/sqm2_overlapint.py +704 -0
- multioptpy/SQM/sqm2/sqm2_qm.py +578 -0
- multioptpy/SQM/sqm2/sqm2_rep.py +66 -0
- multioptpy/SQM/sqm2/sqm2_srb.py +70 -0
- multioptpy/Thermo/__init__.py +0 -0
- multioptpy/Thermo/normal_mode_analyzer.py +865 -0
- multioptpy/Utils/__init__.py +0 -0
- multioptpy/Utils/bond_connectivity.py +264 -0
- multioptpy/Utils/calc_tools.py +884 -0
- multioptpy/Utils/oniom.py +96 -0
- multioptpy/Utils/pbc.py +48 -0
- multioptpy/Utils/riemann_curvature.py +208 -0
- multioptpy/Utils/symmetry_analyzer.py +482 -0
- multioptpy/Visualization/__init__.py +0 -0
- multioptpy/Visualization/visualization.py +156 -0
- multioptpy/WFAnalyzer/MO_analysis.py +104 -0
- multioptpy/WFAnalyzer/__init__.py +0 -0
- multioptpy/Wrapper/__init__.py +0 -0
- multioptpy/Wrapper/autots.py +1239 -0
- multioptpy/Wrapper/ieip_wrapper.py +93 -0
- multioptpy/Wrapper/md_wrapper.py +92 -0
- multioptpy/Wrapper/neb_wrapper.py +94 -0
- multioptpy/Wrapper/optimize_wrapper.py +76 -0
- multioptpy/__init__.py +5 -0
- multioptpy/entrypoints.py +916 -0
- multioptpy/fileio.py +660 -0
- multioptpy/ieip.py +340 -0
- multioptpy/interface.py +1086 -0
- multioptpy/irc.py +529 -0
- multioptpy/moleculardynamics.py +432 -0
- multioptpy/neb.py +1267 -0
- multioptpy/optimization.py +1553 -0
- multioptpy/optimizer.py +709 -0
- multioptpy-1.20.2.dist-info/METADATA +438 -0
- multioptpy-1.20.2.dist-info/RECORD +246 -0
- multioptpy-1.20.2.dist-info/WHEEL +5 -0
- multioptpy-1.20.2.dist-info/entry_points.txt +9 -0
- multioptpy-1.20.2.dist-info/licenses/LICENSE +674 -0
- multioptpy-1.20.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.spatial import distance_matrix
|
|
3
|
+
from scipy.optimize import linear_sum_assignment
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def rotation_matrix(axis, theta):
|
|
7
|
+
"""
|
|
8
|
+
Create rotation matrix for rotation around an axis by angle theta
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
axis : np.ndarray
|
|
13
|
+
Axis of rotation (3D unit vector)
|
|
14
|
+
theta : float
|
|
15
|
+
Angle of rotation in radians
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
np.ndarray
|
|
20
|
+
3x3 rotation matrix
|
|
21
|
+
"""
|
|
22
|
+
# Ensure axis is normalized
|
|
23
|
+
axis = axis / np.linalg.norm(axis)
|
|
24
|
+
|
|
25
|
+
# Components of axis
|
|
26
|
+
x, y, z = axis
|
|
27
|
+
|
|
28
|
+
# Compute rotation matrix using axis-angle formula
|
|
29
|
+
c = np.cos(theta)
|
|
30
|
+
s = np.sin(theta)
|
|
31
|
+
t = 1 - c
|
|
32
|
+
|
|
33
|
+
matrix = np.array([
|
|
34
|
+
[t*x*x + c, t*x*y - s*z, t*x*z + s*y],
|
|
35
|
+
[t*x*y + s*z, t*y*y + c, t*y*z - s*x],
|
|
36
|
+
[t*x*z - s*y, t*y*z + s*x, t*z*z + c]
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
return matrix
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SymmetryAnalyzer:
|
|
43
|
+
"""Analyzer for molecular symmetry elements and point group determination"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, atoms, coordinates, tol=1e-2, angle_tol=1e-4, max_n_fold=6):
|
|
46
|
+
"""
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
atoms : list of str
|
|
50
|
+
List of atomic symbols
|
|
51
|
+
coordinates : np.ndarray
|
|
52
|
+
Atomic coordinates (n_atoms, 3)
|
|
53
|
+
tol : float
|
|
54
|
+
Distance tolerance for matching atoms (e.g., in Angstroms)
|
|
55
|
+
angle_tol : float
|
|
56
|
+
Tolerance for comparing axis vectors (dot product deviation from 1.0)
|
|
57
|
+
max_n_fold : int
|
|
58
|
+
Maximum n-fold rotation to check
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
self.atoms = atoms
|
|
62
|
+
if coordinates is None or len(coordinates) == 0:
|
|
63
|
+
raise ValueError("Valid coordinates must be provided.")
|
|
64
|
+
|
|
65
|
+
self.coordinates = np.array(coordinates, dtype=np.float64)
|
|
66
|
+
self.n_atoms = len(atoms)
|
|
67
|
+
self.tol = tol
|
|
68
|
+
self.angle_tol = angle_tol
|
|
69
|
+
self.inertia_tol = 1e-6
|
|
70
|
+
self.max_n_fold = max_n_fold
|
|
71
|
+
|
|
72
|
+
# Center molecule at COM
|
|
73
|
+
self.com = np.mean(self.coordinates, axis=0)
|
|
74
|
+
self.coordinates -= self.com
|
|
75
|
+
|
|
76
|
+
# Group coordinates by atom type for efficient checking
|
|
77
|
+
self.pcoords = self._create_pcoords()
|
|
78
|
+
|
|
79
|
+
# Symmetry elements
|
|
80
|
+
self.cn_axes = {}
|
|
81
|
+
self.reflection_planes = []
|
|
82
|
+
self.has_inversion = False
|
|
83
|
+
self.sn_axes = {}
|
|
84
|
+
|
|
85
|
+
# Properties
|
|
86
|
+
self.is_linear_mol = self._check_linearity()
|
|
87
|
+
|
|
88
|
+
def _check_linearity(self):
|
|
89
|
+
"""Check if molecule is linear based on moments of inertia"""
|
|
90
|
+
if self.n_atoms <= 2:
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
# Calculate principal moments of inertia
|
|
94
|
+
inertia = np.zeros((3, 3))
|
|
95
|
+
for i in range(self.n_atoms):
|
|
96
|
+
r = self.coordinates[i]
|
|
97
|
+
r2 = np.sum(r * r)
|
|
98
|
+
# We don't have atomic masses, so assume all are 1.
|
|
99
|
+
# This is fine for symmetry, as we only care about the *ratio* of moments.
|
|
100
|
+
inertia += r2 * np.eye(3) - np.outer(r, r)
|
|
101
|
+
|
|
102
|
+
evals = np.linalg.eigvalsh(inertia)
|
|
103
|
+
|
|
104
|
+
# For a linear molecule, one moment is zero, and the other two are equal.
|
|
105
|
+
# evals[0] is the smallest.
|
|
106
|
+
return evals[0] < self.inertia_tol
|
|
107
|
+
|
|
108
|
+
def _create_pcoords(self):
|
|
109
|
+
"""
|
|
110
|
+
Group coordinates by atom type into a dictionary of dense arrays.
|
|
111
|
+
This is much faster than the previous sparse array method.
|
|
112
|
+
"""
|
|
113
|
+
pcoords = {}
|
|
114
|
+
unique_atoms = set(self.atoms)
|
|
115
|
+
for atom_type in unique_atoms:
|
|
116
|
+
indices = [i for i, atom in enumerate(self.atoms) if atom == atom_type]
|
|
117
|
+
pcoords[atom_type] = self.coordinates[indices]
|
|
118
|
+
return pcoords
|
|
119
|
+
|
|
120
|
+
def _check_op(self, op_matrix):
|
|
121
|
+
"""
|
|
122
|
+
Check if the molecule is invariant under a given 3x3 operation matrix.
|
|
123
|
+
This is the new, robust core of the analyzer.
|
|
124
|
+
"""
|
|
125
|
+
for atom_type, coords in self.pcoords.items():
|
|
126
|
+
if coords.shape[0] == 0:
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
# Apply the operation: (N_atoms, 3) @ (3, 3) -> (N_atoms, 3)
|
|
130
|
+
transformed_coords = coords @ op_matrix.T
|
|
131
|
+
|
|
132
|
+
# Find the distance between all original and transformed atoms
|
|
133
|
+
dist_mat = distance_matrix(coords, transformed_coords)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
# Use the Hungarian algorithm to find the *best one-to-one mapping*
|
|
137
|
+
# This enforces a correct permutation.
|
|
138
|
+
row_ind, col_ind = linear_sum_assignment(dist_mat)
|
|
139
|
+
|
|
140
|
+
# Find the largest distance in this optimal mapping
|
|
141
|
+
max_dist = dist_mat[row_ind, col_ind].max()
|
|
142
|
+
|
|
143
|
+
# If the largest distance is over tolerance, this operation is not a symmetry
|
|
144
|
+
if max_dist > self.tol:
|
|
145
|
+
return False
|
|
146
|
+
except ValueError:
|
|
147
|
+
# linear_sum_assignment can fail if matrix is empty or NaN
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
# If all atom types pass, it's a valid symmetry operation
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
def analyze(self):
|
|
154
|
+
"""Analyze molecular symmetry and determine point group"""
|
|
155
|
+
# Get candidate axes *once*
|
|
156
|
+
candidate_axes = get_possible_axes(self.coordinates, self.com, self.angle_tol)
|
|
157
|
+
|
|
158
|
+
# Find all symmetry elements
|
|
159
|
+
self._find_inversion_center()
|
|
160
|
+
self._find_rotation_axes(candidate_axes)
|
|
161
|
+
self._find_reflection_planes(candidate_axes)
|
|
162
|
+
self._find_improper_rotation_axes(candidate_axes)
|
|
163
|
+
|
|
164
|
+
return self._determine_point_group()
|
|
165
|
+
|
|
166
|
+
def _find_rotation_axes(self, axes):
|
|
167
|
+
"""Detect rotation axes (Cn)"""
|
|
168
|
+
self.cn_axes = {i: [] for i in range(2, self.max_n_fold + 1)}
|
|
169
|
+
|
|
170
|
+
for n in range(self.max_n_fold, 1, -1):
|
|
171
|
+
theta = 2.0 * np.pi / n
|
|
172
|
+
for axis in axes:
|
|
173
|
+
op = rotation_matrix(axis, theta)
|
|
174
|
+
if self._check_op(op):
|
|
175
|
+
self.cn_axes[n].append(axis)
|
|
176
|
+
# De-duplicate axes
|
|
177
|
+
self.cn_axes[n] = strip_identical_axes(self.cn_axes[n], self.angle_tol)
|
|
178
|
+
|
|
179
|
+
def _find_reflection_planes(self, axes):
|
|
180
|
+
"""Detect reflection planes (σ), using axes as plane normals"""
|
|
181
|
+
planes = []
|
|
182
|
+
for normal in axes:
|
|
183
|
+
# Reflection matrix: I - 2 * (n @ n.T)
|
|
184
|
+
op = np.eye(3) - 2 * np.outer(normal, normal)
|
|
185
|
+
if self._check_op(op):
|
|
186
|
+
planes.append(normal)
|
|
187
|
+
self.reflection_planes = strip_identical_axes(planes, self.angle_tol)
|
|
188
|
+
|
|
189
|
+
def _find_inversion_center(self):
|
|
190
|
+
"""Detect inversion center (i)"""
|
|
191
|
+
op = -np.eye(3)
|
|
192
|
+
self.has_inversion = self._check_op(op)
|
|
193
|
+
|
|
194
|
+
def _find_improper_rotation_axes(self, axes):
|
|
195
|
+
"""Detect improper rotation axes (Sn)"""
|
|
196
|
+
self.sn_axes = {i: [] for i in range(2, self.max_n_fold + 1)}
|
|
197
|
+
|
|
198
|
+
for n in range(self.max_n_fold, 1, -1):
|
|
199
|
+
theta = 2.0 * np.pi / n
|
|
200
|
+
for axis in axes:
|
|
201
|
+
# S_n = C_n followed by sigma_h (reflection in plane perp to axis)
|
|
202
|
+
rot_op = rotation_matrix(axis, theta)
|
|
203
|
+
refl_op = np.eye(3) - 2 * np.outer(axis, axis)
|
|
204
|
+
op = refl_op @ rot_op
|
|
205
|
+
|
|
206
|
+
if self._check_op(op):
|
|
207
|
+
self.sn_axes[n].append(axis)
|
|
208
|
+
self.sn_axes[n] = strip_identical_axes(self.sn_axes[n], self.angle_tol)
|
|
209
|
+
|
|
210
|
+
def _determine_point_group(self):
|
|
211
|
+
"""Determine point group based on detected symmetry elements (standard flowchart)"""
|
|
212
|
+
|
|
213
|
+
# 1. Linear molecules
|
|
214
|
+
if self.is_linear_mol:
|
|
215
|
+
return "D∞h" if self.has_inversion else "C∞v"
|
|
216
|
+
|
|
217
|
+
# 2. High symmetry groups
|
|
218
|
+
if self._has_icosahedral_symmetry():
|
|
219
|
+
return "Ih" # I (no inversion) is very rare
|
|
220
|
+
|
|
221
|
+
if self._has_octahedral_symmetry():
|
|
222
|
+
return "Oh" if self.has_inversion else "O"
|
|
223
|
+
|
|
224
|
+
if self._has_tetrahedral_symmetry():
|
|
225
|
+
if self.has_inversion:
|
|
226
|
+
return "Th"
|
|
227
|
+
elif len(self.reflection_planes) > 0:
|
|
228
|
+
# More robust check: Td has 6 reflection planes
|
|
229
|
+
if len(self.reflection_planes) >= 6:
|
|
230
|
+
return "Td"
|
|
231
|
+
else:
|
|
232
|
+
return "T" # Or Th if S4 axes exist but no planes
|
|
233
|
+
else:
|
|
234
|
+
return "T"
|
|
235
|
+
|
|
236
|
+
# 3. Find highest order rotation axis
|
|
237
|
+
max_n = 1
|
|
238
|
+
for n in range(self.max_n_fold, 1, -1):
|
|
239
|
+
if len(self.cn_axes[n]) > 0:
|
|
240
|
+
max_n = n
|
|
241
|
+
break
|
|
242
|
+
|
|
243
|
+
# 4. No principal axis (max_n = 1)
|
|
244
|
+
if max_n == 1:
|
|
245
|
+
if self.has_inversion:
|
|
246
|
+
return "Ci"
|
|
247
|
+
elif len(self.reflection_planes) > 0:
|
|
248
|
+
return "Cs"
|
|
249
|
+
else:
|
|
250
|
+
return "C1"
|
|
251
|
+
|
|
252
|
+
# 5. Has a principal C_n axis
|
|
253
|
+
principal_axis = self.cn_axes[max_n][0]
|
|
254
|
+
|
|
255
|
+
# Check for C2 axes perpendicular to principal axis (D groups)
|
|
256
|
+
perp_c2_axes = [ax for ax in self.cn_axes.get(2, [])
|
|
257
|
+
if abs(np.dot(ax, principal_axis)) < self.angle_tol]
|
|
258
|
+
|
|
259
|
+
if len(perp_c2_axes) >= max_n:
|
|
260
|
+
# --- D Groups ---
|
|
261
|
+
# Check for horizontal plane (sigma_h)
|
|
262
|
+
has_sigma_h = any(abs(np.dot(pl, principal_axis)) > (1.0 - self.angle_tol)
|
|
263
|
+
for pl in self.reflection_planes)
|
|
264
|
+
if has_sigma_h:
|
|
265
|
+
return f"D{max_n}h"
|
|
266
|
+
|
|
267
|
+
# Check for dihedral planes (sigma_d)
|
|
268
|
+
# These are vertical planes that bisect the perp. C2 axes
|
|
269
|
+
n_dihedral_planes = sum(1 for pl in self.reflection_planes
|
|
270
|
+
if abs(np.dot(pl, principal_axis)) < self.angle_tol)
|
|
271
|
+
if n_dihedral_planes >= max_n:
|
|
272
|
+
return f"D{max_n}d"
|
|
273
|
+
|
|
274
|
+
return f"D{max_n}"
|
|
275
|
+
|
|
276
|
+
else:
|
|
277
|
+
# --- C and S Groups ---
|
|
278
|
+
# Check for horizontal plane (sigma_h)
|
|
279
|
+
has_sigma_h = any(abs(np.dot(pl, principal_axis)) > (1.0 - self.angle_tol)
|
|
280
|
+
for pl in self.reflection_planes)
|
|
281
|
+
if has_sigma_h:
|
|
282
|
+
return f"C{max_n}h"
|
|
283
|
+
|
|
284
|
+
# Check for vertical planes (sigma_v)
|
|
285
|
+
n_vertical_planes = sum(1 for pl in self.reflection_planes
|
|
286
|
+
if abs(np.dot(pl, principal_axis)) < self.angle_tol)
|
|
287
|
+
if n_vertical_planes >= max_n:
|
|
288
|
+
return f"C{max_n}v"
|
|
289
|
+
|
|
290
|
+
# Check for S_2n axis coincident with C_n
|
|
291
|
+
n_s2n = 2 * max_n
|
|
292
|
+
if n_s2n in self.sn_axes:
|
|
293
|
+
has_s2n = any(abs(np.dot(sn_ax, principal_axis)) > (1.0 - self.angle_tol)
|
|
294
|
+
for sn_ax in self.sn_axes.get(n_s2n, []))
|
|
295
|
+
if has_s2n:
|
|
296
|
+
return f"S{n_s2n}"
|
|
297
|
+
|
|
298
|
+
return f"C{max_n}"
|
|
299
|
+
|
|
300
|
+
def _has_icosahedral_symmetry(self):
|
|
301
|
+
"""Check for icosahedral symmetry"""
|
|
302
|
+
c5_count = len(self.cn_axes.get(5, []))
|
|
303
|
+
c3_count = len(self.cn_axes.get(3, []))
|
|
304
|
+
return c5_count >= 6 and c3_count >= 10
|
|
305
|
+
|
|
306
|
+
def _has_octahedral_symmetry(self):
|
|
307
|
+
"""Check for octahedral symmetry"""
|
|
308
|
+
c4_count = len(self.cn_axes.get(4, []))
|
|
309
|
+
c3_count = len(self.cn_axes.get(3, []))
|
|
310
|
+
return c4_count >= 3 and c3_count >= 4
|
|
311
|
+
|
|
312
|
+
def _has_tetrahedral_symmetry(self):
|
|
313
|
+
"""Check for tetrahedral symmetry"""
|
|
314
|
+
c3_count = len(self.cn_axes.get(3, []))
|
|
315
|
+
c2_count = len(self.cn_axes.get(2, []))
|
|
316
|
+
return c3_count >= 4 and c2_count >= 3
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def analyze_symmetry(atoms, coordinates, tol=1e-2, angle_tol=1e-4, max_n_fold=6):
|
|
320
|
+
"""
|
|
321
|
+
Analyze molecular symmetry and determine point group
|
|
322
|
+
|
|
323
|
+
Parameters
|
|
324
|
+
----------
|
|
325
|
+
atoms : list of str
|
|
326
|
+
List of atomic symbols
|
|
327
|
+
coordinates : np.ndarray
|
|
328
|
+
Atomic coordinates (n_atoms, 3)
|
|
329
|
+
tol : float
|
|
330
|
+
Distance tolerance for matching atoms (e.g., in Angstroms)
|
|
331
|
+
angle_tol : float
|
|
332
|
+
Tolerance for comparing axis vectors (dot product deviation from 1.0)
|
|
333
|
+
max_n_fold : int
|
|
334
|
+
Maximum n-fold rotation to check
|
|
335
|
+
|
|
336
|
+
Returns
|
|
337
|
+
-------
|
|
338
|
+
str
|
|
339
|
+
Molecular point group
|
|
340
|
+
"""
|
|
341
|
+
try:
|
|
342
|
+
analyzer = SymmetryAnalyzer(atoms, coordinates, tol, angle_tol, max_n_fold)
|
|
343
|
+
return analyzer.analyze()
|
|
344
|
+
except Exception as e:
|
|
345
|
+
print(f"Error during symmetry analysis: {e}")
|
|
346
|
+
return "Unknown"
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# --- Support functions ---
|
|
350
|
+
|
|
351
|
+
def strip_identical_axes(axes, tol):
|
|
352
|
+
"""Remove similar or inverse axes within tolerance using dot product"""
|
|
353
|
+
unique_axes = []
|
|
354
|
+
for axis in axes:
|
|
355
|
+
# Check if axis is parallel or anti-parallel to any already found
|
|
356
|
+
is_duplicate = False
|
|
357
|
+
for unique_axis in unique_axes:
|
|
358
|
+
dot_product = abs(np.dot(axis, unique_axis))
|
|
359
|
+
if dot_product > (1.0 - tol):
|
|
360
|
+
is_duplicate = True
|
|
361
|
+
break
|
|
362
|
+
|
|
363
|
+
if not is_duplicate:
|
|
364
|
+
unique_axes.append(axis)
|
|
365
|
+
return unique_axes
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def get_possible_axes(coords, com, tol):
|
|
369
|
+
"""
|
|
370
|
+
Get possible rotation axes in a molecule.
|
|
371
|
+
Now includes body-diagonals and vector sums/differences.
|
|
372
|
+
"""
|
|
373
|
+
possible_axes = []
|
|
374
|
+
n_atoms = len(coords)
|
|
375
|
+
|
|
376
|
+
# 1. Cartesian axes
|
|
377
|
+
possible_axes.append(np.array([1., 0., 0.]))
|
|
378
|
+
possible_axes.append(np.array([0., 1., 0.]))
|
|
379
|
+
possible_axes.append(np.array([0., 0., 1.]))
|
|
380
|
+
|
|
381
|
+
# 2. Body diagonals (for cubic symmetries)
|
|
382
|
+
diag_axes = [
|
|
383
|
+
[1., 1., 1.], [1., 1., -1.], [1., -1., 1.], [-1., 1., 1.]
|
|
384
|
+
]
|
|
385
|
+
for ax in diag_axes:
|
|
386
|
+
possible_axes.append(np.array(ax) / np.linalg.norm(ax))
|
|
387
|
+
|
|
388
|
+
# 3. Vectors from COM to each atom
|
|
389
|
+
for i in range(n_atoms):
|
|
390
|
+
vec = coords[i] # Coords are already COM-centered
|
|
391
|
+
norm = np.linalg.norm(vec)
|
|
392
|
+
if norm > 1e-6:
|
|
393
|
+
possible_axes.append(vec / norm)
|
|
394
|
+
|
|
395
|
+
# 4. Vectors between atom pairs and their cross/sum/diff
|
|
396
|
+
for i in range(n_atoms):
|
|
397
|
+
for j in range(i + 1, n_atoms):
|
|
398
|
+
# Atom-atom pair vector
|
|
399
|
+
vec = coords[j] - coords[i]
|
|
400
|
+
norm = np.linalg.norm(vec)
|
|
401
|
+
if norm > 1e-6:
|
|
402
|
+
possible_axes.append(vec / norm)
|
|
403
|
+
|
|
404
|
+
# Cross, sum, and diff of COM-atom vectors
|
|
405
|
+
vec1 = coords[i]
|
|
406
|
+
vec2 = coords[j]
|
|
407
|
+
|
|
408
|
+
cross_vec = np.cross(vec1, vec2)
|
|
409
|
+
norm = np.linalg.norm(cross_vec)
|
|
410
|
+
if norm > 1e-6:
|
|
411
|
+
possible_axes.append(cross_vec / norm)
|
|
412
|
+
|
|
413
|
+
sum_vec = vec1 + vec2
|
|
414
|
+
norm = np.linalg.norm(sum_vec)
|
|
415
|
+
if norm > 1e-6:
|
|
416
|
+
possible_axes.append(sum_vec / norm)
|
|
417
|
+
|
|
418
|
+
diff_vec = vec1 - vec2
|
|
419
|
+
norm = np.linalg.norm(diff_vec)
|
|
420
|
+
if norm > 1e-6:
|
|
421
|
+
possible_axes.append(diff_vec / norm)
|
|
422
|
+
|
|
423
|
+
return strip_identical_axes(possible_axes, tol)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
# --- Example Usage ---
|
|
427
|
+
|
|
428
|
+
if __name__ == '__main__':
|
|
429
|
+
# Water (C2v)
|
|
430
|
+
atoms_water = ['O', 'H', 'H']
|
|
431
|
+
coords_water = np.array([
|
|
432
|
+
[0.000000, 0.000000, 0.117300],
|
|
433
|
+
[0.000000, 0.757200, -0.469200],
|
|
434
|
+
[0.000000, -0.757200, -0.469200]
|
|
435
|
+
])
|
|
436
|
+
print(f"Molecule: Water")
|
|
437
|
+
print(f"Point Group: {analyze_symmetry(atoms_water, coords_water)}\n")
|
|
438
|
+
|
|
439
|
+
# Methane (Td)
|
|
440
|
+
atoms_methane = ['C', 'H', 'H', 'H', 'H']
|
|
441
|
+
coords_methane = np.array([
|
|
442
|
+
[0.0, 0.0, 0.0],
|
|
443
|
+
[0.6291, 0.6291, 0.6291],
|
|
444
|
+
[-0.6291, -0.6291, 0.6291],
|
|
445
|
+
[-0.6291, 0.6291, -0.6291],
|
|
446
|
+
[0.6291, -0.6291, -0.6291]
|
|
447
|
+
])
|
|
448
|
+
print(f"Molecule: Methane")
|
|
449
|
+
print(f"Point Group: {analyze_symmetry(atoms_methane, coords_methane)}\n")
|
|
450
|
+
|
|
451
|
+
# Benzene (D6h)
|
|
452
|
+
atoms_benzene = ['C', 'C', 'C', 'C', 'C', 'C', 'H', 'H', 'H', 'H', 'H', 'H']
|
|
453
|
+
coords_benzene = np.array([
|
|
454
|
+
[ 0.0000, 1.397, 0.0], [ 1.210, 0.698, 0.0], [ 1.210, -0.698, 0.0],
|
|
455
|
+
[ 0.0000, -1.397, 0.0], [-1.210, -0.698, 0.0], [-1.210, 0.698, 0.0],
|
|
456
|
+
[ 0.0000, 2.484, 0.0], [ 2.151, 1.242, 0.0], [ 2.151, -1.242, 0.0],
|
|
457
|
+
[ 0.0000, -2.484, 0.0], [-2.151, -1.242, 0.0], [-2.151, 1.242, 0.0]
|
|
458
|
+
])
|
|
459
|
+
print(f"Molecule: Benzene")
|
|
460
|
+
print(f"Point Group: {analyze_symmetry(atoms_benzene, coords_benzene)}\n")
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
# Allene (D2d)
|
|
464
|
+
atoms_allene = ['C', 'C', 'C', 'H', 'H', 'H', 'H']
|
|
465
|
+
coords_allene_new = np.array([
|
|
466
|
+
[ 0.0, 0.0, 0.0], [ 0.0, 0.0, 1.308], [ 0.0, 0.0, -1.308],
|
|
467
|
+
[ 0.0, 0.95, 1.848], [ 0.0, -0.95, 1.848],
|
|
468
|
+
[ 0.95, 0.0, -1.848], [-0.95, 0.0, -1.848]
|
|
469
|
+
])
|
|
470
|
+
print(f"Molecule: Allene")
|
|
471
|
+
print(f"Point Group: {analyze_symmetry(atoms_allene, coords_allene_new)}\n")
|
|
472
|
+
|
|
473
|
+
# SF6 (Oh)
|
|
474
|
+
atoms_sf6 = ['S'] + ['F'] * 6
|
|
475
|
+
coords_sf6 = np.array([
|
|
476
|
+
[0.0, 0.0, 0.0],
|
|
477
|
+
[1.5, 0.0, 0.0], [-1.5, 0.0, 0.0],
|
|
478
|
+
[0.0, 1.5, 0.0], [0.0, -1.5, 0.0],
|
|
479
|
+
[0.0, 0.0, 1.5], [0.0, 0.0, -1.5]
|
|
480
|
+
])
|
|
481
|
+
print(f"Molecule: SF6")
|
|
482
|
+
print(f"Point Group: {analyze_symmetry(atoms_sf6, coords_sf6)}\n")
|
|
File without changes
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from multioptpy.Parameters.parameter import UnitValueLib
|
|
5
|
+
|
|
6
|
+
class Graph:
|
|
7
|
+
def __init__(self, folder_directory):
|
|
8
|
+
self.BPA_FOLDER_DIRECTORY = folder_directory
|
|
9
|
+
return
|
|
10
|
+
|
|
11
|
+
def stem_plot(self, freq_list, int_list, add_file_name=""):
|
|
12
|
+
fig, ax = plt.subplots()
|
|
13
|
+
ax.stem(freq_list, int_list)
|
|
14
|
+
ax.set_title(add_file_name)
|
|
15
|
+
y_max = np.max(int_list) + (np.max(int_list) * 0.1)
|
|
16
|
+
ax.set_xlim(0, 5000)
|
|
17
|
+
ax.set_ylim(0, y_max)
|
|
18
|
+
ax.set_xlabel('Wave number [/ nm]')
|
|
19
|
+
ax.set_ylabel('intensity [a.u.]')
|
|
20
|
+
fig.tight_layout()
|
|
21
|
+
fig.savefig(self.BPA_FOLDER_DIRECTORY+"stem_plot_"+add_file_name+".png", format="png", dpi=300)
|
|
22
|
+
plt.close()
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
def double_plot(self, num_list, energy_list, energy_list_2, add_file_name=""):
|
|
26
|
+
"""
|
|
27
|
+
Plot two energy profiles on a single figure using primary and secondary y-axes
|
|
28
|
+
with proper legends.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
num_list: Iteration numbers (x-axis)
|
|
32
|
+
energy_list: First energy profile (primary y-axis)
|
|
33
|
+
energy_list_2: Second energy profile (secondary y-axis)
|
|
34
|
+
add_file_name: Additional text for the output file name
|
|
35
|
+
"""
|
|
36
|
+
fig, ax1 = plt.subplots(figsize=(10, 6))
|
|
37
|
+
|
|
38
|
+
# Primary axis (left) - Normal energy
|
|
39
|
+
color1 = 'green'
|
|
40
|
+
line1, = ax1.plot(num_list, energy_list, color=color1, linestyle='--', marker='.', label='Normal Energy')
|
|
41
|
+
ax1.set_xlabel('ITR.')
|
|
42
|
+
ax1.set_ylabel('Electronic Energy [kcal/mol]', color=color1)
|
|
43
|
+
ax1.tick_params(axis='y', labelcolor=color1)
|
|
44
|
+
|
|
45
|
+
# Secondary axis (right) - Bias energy
|
|
46
|
+
color2 = 'blue'
|
|
47
|
+
ax2 = ax1.twinx()
|
|
48
|
+
line2, = ax2.plot(num_list, energy_list_2, color=color2, linestyle='--', marker='.', label='Bias Energy')
|
|
49
|
+
ax2.set_ylabel('Electronic Energy [kcal/mol]', color=color2)
|
|
50
|
+
ax2.tick_params(axis='y', labelcolor=color2)
|
|
51
|
+
|
|
52
|
+
# Add legend
|
|
53
|
+
lines = [line1, line2]
|
|
54
|
+
labels = [line.get_label() for line in lines]
|
|
55
|
+
ax1.legend(lines, labels, loc='best')
|
|
56
|
+
|
|
57
|
+
# Title and layout
|
|
58
|
+
plt.title('Energy Profile')
|
|
59
|
+
plt.grid(True, linestyle='--', alpha=0.7)
|
|
60
|
+
plt.tight_layout()
|
|
61
|
+
|
|
62
|
+
# Save figure
|
|
63
|
+
plt.savefig(self.BPA_FOLDER_DIRECTORY + "energy_plot_" + add_file_name + ".png", format="png", dpi=300)
|
|
64
|
+
plt.close()
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
def single_plot(self, num_list, energy_list, file_directory, atom_num, axis_name_1="ITR. ", axis_name_2="cosθ", name="orthogonality"):
|
|
68
|
+
fig, ax = plt.subplots()
|
|
69
|
+
ax.plot(num_list,energy_list, "b--o" , markersize=3)
|
|
70
|
+
|
|
71
|
+
ax.set_title(str(atom_num))
|
|
72
|
+
ax.set_xlabel(axis_name_1)
|
|
73
|
+
ax.set_ylabel(axis_name_2)
|
|
74
|
+
fig.tight_layout()
|
|
75
|
+
fig.savefig(self.BPA_FOLDER_DIRECTORY+"plot_"+name+"_"+str(atom_num)+".png", format="png", dpi=200)
|
|
76
|
+
plt.close()
|
|
77
|
+
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class NEBVisualizer:
|
|
83
|
+
"""Visualization functionality for NEB calculations"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, config):
|
|
86
|
+
self.config = config
|
|
87
|
+
self.color_list = ["b"] # for matplotlib
|
|
88
|
+
|
|
89
|
+
def simple_plot(self, num_list, data_list, file_directory, optimize_num,
|
|
90
|
+
axis_name_1="NODE #", axis_name_2="Value", name="data"):
|
|
91
|
+
"""Create a simple plot"""
|
|
92
|
+
fig, ax = plt.subplots()
|
|
93
|
+
ax.plot(num_list, data_list,
|
|
94
|
+
self.color_list[0] + "--o")
|
|
95
|
+
|
|
96
|
+
ax.set_title(str(optimize_num))
|
|
97
|
+
ax.set_xlabel(axis_name_1)
|
|
98
|
+
ax.set_ylabel(axis_name_2)
|
|
99
|
+
fig.tight_layout()
|
|
100
|
+
fig.savefig(f"{self.config.NEB_FOLDER_DIRECTORY}plot_{name}_{optimize_num}.png",
|
|
101
|
+
format="png", dpi=200)
|
|
102
|
+
plt.close()
|
|
103
|
+
|
|
104
|
+
def simple_scatter_plot(self, num_list, data_list, file_directory, optimize_num,
|
|
105
|
+
axis_name_1="NODE #", axis_name_2="Value", name="data"):
|
|
106
|
+
"""Create a simple scatter plot"""
|
|
107
|
+
fig, ax = plt.subplots()
|
|
108
|
+
ax.scatter(num_list, data_list, color=self.color_list[0], marker='o')
|
|
109
|
+
ax.plot(num_list, data_list, self.color_list[0]+'--o')
|
|
110
|
+
ax.set_title(str(optimize_num))
|
|
111
|
+
ax.set_xlabel(axis_name_1)
|
|
112
|
+
ax.set_ylabel(axis_name_2)
|
|
113
|
+
fig.tight_layout()
|
|
114
|
+
fig.savefig(f"{self.config.NEB_FOLDER_DIRECTORY}plot_{name}_{optimize_num}.png",
|
|
115
|
+
format="png", dpi=200)
|
|
116
|
+
plt.close()
|
|
117
|
+
|
|
118
|
+
def plot_energy(self, num_list, energy_list, optimize_num,
|
|
119
|
+
axis_name_1="NODE #", axis_name_2="Electronic Energy [kcal/mol]", name="energy"):
|
|
120
|
+
"""Plot energy profile"""
|
|
121
|
+
self.simple_plot(num_list, energy_list, "", optimize_num, axis_name_1, axis_name_2, name)
|
|
122
|
+
|
|
123
|
+
def plot_gradient(self, num_list, gradient_norm_list, optimize_num,
|
|
124
|
+
axis_name_1="NODE #", axis_name_2="Gradient (RMS) [a.u.]", name="gradient"):
|
|
125
|
+
"""Plot gradient profile"""
|
|
126
|
+
self.simple_plot(num_list, gradient_norm_list, "", optimize_num, axis_name_1, axis_name_2, name)
|
|
127
|
+
|
|
128
|
+
def plot_orthogonality(self, num_list, cos_list, optimize_num):
|
|
129
|
+
"""Plot orthogonality profile"""
|
|
130
|
+
self.simple_plot(num_list, cos_list, "", optimize_num,
|
|
131
|
+
axis_name_1="NODE #", axis_name_2="cosθ", name="orthogonality")
|
|
132
|
+
|
|
133
|
+
def plot_perpendicular_gradient(self, num_list, force_list, optimize_num, force_type="rms"):
|
|
134
|
+
"""Plot perpendicular gradient profile"""
|
|
135
|
+
if force_type == "rms":
|
|
136
|
+
axis_name_2 = "Perpendicular Gradient (RMS) [a.u.]"
|
|
137
|
+
name = "perp_rms_gradient"
|
|
138
|
+
else:
|
|
139
|
+
axis_name_2 = "Perpendicular Gradient (MAX) [a.u.]"
|
|
140
|
+
name = "perp_max_gradient"
|
|
141
|
+
|
|
142
|
+
self.simple_plot(num_list, force_list, "", optimize_num,
|
|
143
|
+
axis_name_1="NODE #", axis_name_2=axis_name_2, name=name)
|
|
144
|
+
|
|
145
|
+
# For ADDF-like method in ieip.py
|
|
146
|
+
def plot_potential_energy_path(energy_list, path, additional_name=""):
|
|
147
|
+
min_energy = np.min(energy_list)
|
|
148
|
+
energy_list -= min_energy
|
|
149
|
+
energy_list *= UnitValueLib().hartree2kcalmol
|
|
150
|
+
plt.plot(energy_list, marker='.', linestyle='--', color='b')
|
|
151
|
+
plt.xlabel("Iteration")
|
|
152
|
+
plt.ylabel("Energy (kcal/mol)")
|
|
153
|
+
plt.title("Potential Energy Path")
|
|
154
|
+
plt.savefig(path+"/"+additional_name+"_energy_profile.png")
|
|
155
|
+
plt.close()
|
|
156
|
+
return
|