ensemble-analyzer 1.2__tar.gz → 1.2.1__tar.gz

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.
Files changed (111) hide show
  1. {ensemble_analyzer-1.2/src/ensemble_analyzer.egg-info → ensemble_analyzer-1.2.1}/PKG-INFO +1 -1
  2. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/pyproject.toml +1 -1
  3. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/parsers/gaussian.py +44 -52
  4. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/parsers/orca.py +35 -57
  5. ensemble_analyzer-1.2.1/src/ensemble_analyzer/parsers/vibrations.py +228 -0
  6. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1/src/ensemble_analyzer.egg-info}/PKG-INFO +1 -1
  7. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer.egg-info/SOURCES.txt +1 -0
  8. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/LICENSE +0 -0
  9. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/README.md +0 -0
  10. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/setup.cfg +0 -0
  11. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/enan_calculators/__init__.py +0 -0
  12. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/enan_calculators/_aimnet.py +0 -0
  13. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/enan_calculators/_fairchem.py +0 -0
  14. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/enan_calculators/_gaussian.py +0 -0
  15. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/enan_calculators/_mace.py +0 -0
  16. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/enan_calculators/_ml_inference.py +0 -0
  17. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/enan_calculators/_models.py +0 -0
  18. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/enan_calculators/_nwchem.py +0 -0
  19. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/enan_calculators/_orca.py +0 -0
  20. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/enan_calculators/_skala.py +0 -0
  21. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/enan_calculators/_tblite.py +0 -0
  22. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/enan_calculators/_uma.py +0 -0
  23. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/__init__.py +0 -0
  24. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/__main__.py +0 -0
  25. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_clustering/__init__.py +0 -0
  26. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_clustering/cluster_config.py +0 -0
  27. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_clustering/cluster_manager.py +0 -0
  28. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_logger/__init__.py +0 -0
  29. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_logger/create_log.py +0 -0
  30. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_logger/formatter.py +0 -0
  31. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_logger/logger.py +0 -0
  32. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_managers/__init__.py +0 -0
  33. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_managers/calculation_config.py +0 -0
  34. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_managers/calculation_executor.py +0 -0
  35. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_managers/calculator_orchestration.py +0 -0
  36. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_managers/checkpoint_manager.py +0 -0
  37. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_managers/prism_backend.py +0 -0
  38. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_managers/protocol_executor.py +0 -0
  39. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_managers/protocol_manager.py +0 -0
  40. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_managers/pruning_manager.py +0 -0
  41. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_parser_arguments.py +0 -0
  42. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_parser_parameter.py +0 -0
  43. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_spectral/__init__.py +0 -0
  44. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_spectral/base.py +0 -0
  45. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_spectral/comp_electronic.py +0 -0
  46. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_spectral/comp_vibronic.py +0 -0
  47. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_spectral/compare.py +0 -0
  48. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_spectral/experimental.py +0 -0
  49. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_spectral/graph_default.py +0 -0
  50. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/_title.py +0 -0
  51. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/api.py +0 -0
  52. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/calculators/__init__.py +0 -0
  53. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/calculators/_ml_base.py +0 -0
  54. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/calculators/aimnet.py +0 -0
  55. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/calculators/base.py +0 -0
  56. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/calculators/fairchem.py +0 -0
  57. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/calculators/gaussian.py +0 -0
  58. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/calculators/mace.py +0 -0
  59. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/calculators/nwchem.py +0 -0
  60. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/calculators/orca.py +0 -0
  61. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/calculators/skala_calc.py +0 -0
  62. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/calculators/tblite.py +0 -0
  63. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/calculators/uma.py +0 -0
  64. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/cli/__init__.py +0 -0
  65. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/cli/check_setup.py +0 -0
  66. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/cli/clustering.py +0 -0
  67. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/cli/get_av_energy.py +0 -0
  68. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/cli/pickle_editor/__init__.py +0 -0
  69. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/cli/pickle_editor/cli.py +0 -0
  70. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/cli/pickle_editor/core.py +0 -0
  71. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/cli/pickle_editor/tui.py +0 -0
  72. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/cli/protocol_wizard/__init__.py +0 -0
  73. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/cli/protocol_wizard/app_state.py +0 -0
  74. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/cli/protocol_wizard/fields.py +0 -0
  75. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/cli/protocol_wizard/main.py +0 -0
  76. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/cli/protocol_wizard/screens.py +0 -0
  77. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/cli/protocol_wizard/templates.py +0 -0
  78. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/cli/regraph.py +0 -0
  79. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/clustering.py +0 -0
  80. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/conformer/__init__.py +0 -0
  81. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/conformer/conformer.py +0 -0
  82. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/conformer/energy_data.py +0 -0
  83. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/conformer/spectral_data.py +0 -0
  84. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/constants.py +0 -0
  85. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/ensemble_io.py +0 -0
  86. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/graph.py +0 -0
  87. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/io_utils.py +0 -0
  88. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/launch.py +0 -0
  89. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/mode_analysis.py +0 -0
  90. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/parameters_file/basis_sets +0 -0
  91. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/parameters_file/default_protocol.json +0 -0
  92. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/parameters_file/default_threshold.json +0 -0
  93. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/parameters_file/functionals +0 -0
  94. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/parsers/__init__.py +0 -0
  95. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/parsers/base.py +0 -0
  96. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/parsers/nwchem.py +0 -0
  97. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/protocol/__init__.py +0 -0
  98. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/protocol/protocol.py +0 -0
  99. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/protocol/solvent.py +0 -0
  100. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/rrho.py +0 -0
  101. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer/validators.py +0 -0
  102. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer.egg-info/dependency_links.txt +0 -0
  103. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer.egg-info/entry_points.txt +0 -0
  104. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer.egg-info/requires.txt +0 -0
  105. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/src/ensemble_analyzer.egg-info/top_level.txt +0 -0
  106. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/test/test_api.py +0 -0
  107. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/test/test_clustering.py +0 -0
  108. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/test/test_ensemble_io.py +0 -0
  109. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/test/test_high_level.py +0 -0
  110. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/test/test_io_utils.py +0 -0
  111. {ensemble_analyzer-1.2 → ensemble_analyzer-1.2.1}/test/test_mode_analysis.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ensemble-analyzer
3
- Version: 1.2
3
+ Version: 1.2.1
4
4
  Summary: Automatic Refinement of Conformational Ensemble
5
5
  Author-email: Andrea Pellegrini <andrea.pellegrini15@unibo.it>, Paolo Righi <paolo.righi@unibo.it>, Andrea Mazzanti <andrea.mazzanti@unibo.it>, Michele Mancinelli <michele.mancinelli@unibo.it>
6
6
  Maintainer-email: Andrea Pellegrini <andrea.pellegrini15@unibo.it>, Paolo Righi <paolo.righi@unibo.it>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ensemble-analyzer"
7
- version = '1.2'
7
+ version = '1.2.1'
8
8
  description = "Automatic Refinement of Conformational Ensemble"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -1,6 +1,10 @@
1
1
 
2
2
 
3
3
  from ensemble_analyzer.parsers.base import BaseParser, register_parser
4
+ from ensemble_analyzer.parsers.vibrations import (
5
+ gaussian_get_freq, gaussian_get_ir_intensities, gaussian_get_normal_modes,
6
+ gaussian_align_normal_modes, to_displacements,
7
+ )
4
8
 
5
9
  import re
6
10
  import numpy as np
@@ -29,6 +33,12 @@ class GaussianParser(BaseParser):
29
33
  "start_spec": "Excited states from",
30
34
  "e_freq": "\n\n\n",
31
35
  "idx_freq": 1, # index for frequency in frequency table
36
+ # frequency section (same keys as frogchem regex_gaussian)
37
+ "freq_section": "Harmonic frequencies (cm**-1), IR intensities (KM/Mole), Raman scattering",
38
+ "freq_end": "\n\n\n",
39
+ "freq_pattern": r'Frequencies\s*--\s*((?:[+-]?\d+\.\d+\s*)+)',
40
+ "ir_pattern": r'IR Inten\s*--\s*((?:[+-]?\d+\.\d+\s*)+)',
41
+ "rot_pattern": r'Rot\. str\.\s*--\s*((?:[+-]?\d+\.\d+\s*)+)',
32
42
  "opt_done": "Optimization completed",
33
43
  "geom_start": """Input orientation:
34
44
  ---------------------------------------------------------------------
@@ -59,68 +69,50 @@ class GaussianParser(BaseParser):
59
69
 
60
70
  def parse_freq(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
61
71
  """
62
- Parse vibrational frequencies, IR and VCD intensities.
72
+ Parse vibrational frequencies, IR and VCD intensities (FROGChem parser).
63
73
 
64
74
  Returns:
65
75
  Tuple: (Frequencies array, IR spectrum array, VCD spectrum array).
66
76
  """
67
77
 
68
- if not self.regex['s_freq'] in self.fl:
78
+ if not self.regex['freq_section'] in self.fl:
69
79
  return np.array([]), np.zeros(shape=(1,2)), np.zeros(shape=(1,2))
70
-
71
- fl = self.get_filtered_text(start=self.regex['s_freq'], end=self.regex['e_freq'])
72
- freq_pattern = re.compile(r'Frequencies\s*--\s*((?:[+-]?\d+\.\d+\s*)+)')
73
- ir_pattern = re.compile(r'IR Inten\s*--\s*((?:[+-]?\d+\.\d+\s*)+)')
74
- rot_pattern = re.compile(r'Rot\. str\.\s*--\s*((?:[+-]?\d+\.\d+\s*)+)')
75
80
 
76
- # extract and flatten into a single list of floats
77
- frequencies = np.array([float(x) for m in freq_pattern.findall(fl) for x in m.split()])
78
- ir_inten = np.array([float(x) for m in ir_pattern.findall(fl) for x in m.split()])
79
- rot_str = np.array([float(x) for m in rot_pattern.findall(fl) for x in m.split()])
81
+ frequencies = gaussian_get_freq(self.fl, self.regex)
82
+ ir = gaussian_get_ir_intensities(self.fl, self.regex)
80
83
 
81
- return frequencies, np.column_stack((frequencies,ir_inten)), np.column_stack((frequencies,rot_str))
84
+ # VCD: rotational strengths, only printed with freq=VCD
85
+ fl = self.get_filtered_text(start=self.regex['freq_section'], end=self.regex['freq_end'])
86
+ rot_str = np.array([float(x) for m in re.findall(self.regex['rot_pattern'], fl) for x in m.split()])
87
+ if len(rot_str) == len(frequencies) and len(rot_str) > 0:
88
+ vcd = np.column_stack((frequencies, rot_str))
89
+ else:
90
+ vcd = np.zeros(shape=(1, 2))
91
+
92
+ return frequencies, ir, vcd
82
93
 
83
94
  def parse_normal_modes(self, n_atoms: int) -> np.ndarray:
84
- if self.regex['s_freq'] not in self.fl:
85
- return np.empty((0, n_atoms, 3))
86
- fl = self.get_filtered_text(start=self.regex['s_freq'], end=self.regex['e_freq'])
87
- parts = fl.split('Frequencies --')
88
- if len(parts) < 2:
89
- return np.empty((0, n_atoms, 3))
90
- all_modes = []
91
- for chunk in parts[1:]:
92
- lines = chunk.split('\n')
93
- freq_vals = re.findall(r'[-+]?\d+\.\d+(?:[eE][+-]?\d+)?', lines[0])
94
- n_freq = min(len(freq_vals), 3)
95
- if n_freq == 0:
96
- continue
97
- idx = 1
98
- while idx < len(lines):
99
- line = lines[idx].strip()
100
- if not line:
101
- idx += 1
102
- continue
103
- if re.match(r'^\s*\d+\s+\d+', line):
104
- break
105
- idx += 1
106
- mode_disps = np.zeros((n_freq, n_atoms, 3))
107
- for i in range(idx, min(idx + n_atoms, len(lines))):
108
- line = lines[i].strip()
109
- if not line or not re.match(r'^\d+\s+\d+', line):
110
- break
111
- parts_line = line.split()
112
- if len(parts_line) < 2 + 3 * n_freq:
113
- continue
114
- atom_idx = int(parts_line[0]) - 1
115
- vals = [float(x) for x in parts_line[2:]]
116
- for k in range(n_freq):
117
- mode_disps[k, atom_idx, 0] = vals[3 * k]
118
- mode_disps[k, atom_idx, 1] = vals[3 * k + 1]
119
- mode_disps[k, atom_idx, 2] = vals[3 * k + 2]
120
- all_modes.append(mode_disps)
121
- if not all_modes:
122
- return np.empty((0, n_atoms, 3))
123
- return np.vstack(all_modes)
95
+ """Cartesian displacement vectors of the vibrational modes (FROGChem parser).
96
+
97
+ Gaussian prints them in the standard orientation; they are rotated onto the input
98
+ orientation, i.e. the frame of parse_geom.
99
+
100
+ Returns:
101
+ np.ndarray: (n_modes, n_atoms, 3), same order as the frequencies of parse_freq.
102
+ Empty (0, n_atoms, 3) array when the frequency section is missing.
103
+ """
104
+ empty = np.empty((0, n_atoms, 3))
105
+ try:
106
+ matrix = gaussian_get_normal_modes(self.fl, self.regex, N=3 * n_atoms)
107
+ except ValueError:
108
+ return empty
109
+ except Exception as err:
110
+ self.log.warning(f"Normal modes could not be parsed: {err}")
111
+ return empty
112
+ if matrix.shape[1] == 0:
113
+ return empty
114
+ matrix = gaussian_align_normal_modes(self.fl, matrix)
115
+ return to_displacements(matrix, n_atoms)
124
116
 
125
117
  def parse_tddft(self) -> Tuple[np.ndarray, np.ndarray]:
126
118
  """
@@ -1,6 +1,7 @@
1
1
 
2
2
  from ensemble_analyzer.parsers.base import BaseParser, register_parser
3
3
  from ensemble_analyzer.constants import FACTOR_EV_CM_1
4
+ from ensemble_analyzer.parsers.vibrations import orca_get_freq, orca_get_normal_modes, to_displacements
4
5
 
5
6
  import re
6
7
  import numpy as np
@@ -50,6 +51,9 @@ class OrcaParser(BaseParser):
50
51
  "s_freq": "VIBRATIONAL FREQUENCIES",
51
52
  "e_freq": "------------",
52
53
  "idx_freq": 1, # index for frequency in frequency table
54
+ # frequency / normal-mode blocks (same keys as frogchem regex_ORCA)
55
+ "frequency": r'VIBRATIONAL FREQUENCIES([\S\s]*?)\n\n\n',
56
+ "normal_modes": r'NORMAL MODES([\S\s]*?)\n\n\n',
53
57
  "opt_done": "THE OPTIMIZATION HAS CONVERGED",
54
58
  "geom_start": """CARTESIAN COORDINATES (ANGSTROEM)
55
59
  ---------------------------------""",
@@ -88,6 +92,9 @@ State Energy Wavelength R MX MY MZ
88
92
  "s_freq": "VIBRATIONAL FREQUENCIES",
89
93
  "e_freq": "------------",
90
94
  "idx_freq": 1, # index for frequency in frequency table
95
+ # frequency / normal-mode blocks (same keys as frogchem regex_ORCA)
96
+ "frequency": r'VIBRATIONAL FREQUENCIES([\S\s]*?)\n\n\n',
97
+ "normal_modes": r'NORMAL MODES([\S\s]*?)\n\n\n',
91
98
  "opt_done": "THE OPTIMIZATION HAS CONVERGED",
92
99
  "geom_start": """CARTESIAN COORDINATES (ANGSTROEM)
93
100
  ---------------------------------""",
@@ -136,13 +143,9 @@ State Energy Wavelength R MX MY MZ
136
143
  if not self.regex['s_freq'] in self.fl:
137
144
  return np.array([]), np.zeros(shape=(1,2)), np.zeros(shape=(1,2))
138
145
 
139
- fl = self.get_filtered_text(self.regex['s_freq'], end='\n\n\n')
140
-
141
- pattern = r'(?:\d+:)\s*(-?\d+.\d*)'
142
- # freq
143
- freq = np.array(re.findall(pattern, fl, flags=re.MULTILINE), dtype=float)
144
- self._freq_mask = freq != 0
145
- freq = freq[self._freq_mask]
146
+ # freq (FROGChem parser): translations/rotations are printed as zeros and dropped here
147
+ freq = self._all_freq()
148
+ freq = freq[freq != 0]
146
149
 
147
150
  # IR
148
151
  if self.regex['s_IR'] not in self.fl:
@@ -160,58 +163,33 @@ State Energy Wavelength R MX MY MZ
160
163
 
161
164
  return freq, ir, vcd
162
165
 
166
+ def _all_freq(self) -> np.ndarray:
167
+ """All ORCA frequencies (cm-1), translations/rotations included as zeros."""
168
+ if not re.search(self.regex['frequency'], self.fl):
169
+ return np.array([])
170
+ return orca_get_freq(self.fl, self.regex)[1]
171
+
163
172
  def parse_normal_modes(self, n_atoms: int) -> np.ndarray:
164
- if "NORMAL MODES" not in self.fl:
165
- return np.empty((0, n_atoms, 3))
173
+ """Cartesian displacement vectors of the vibrational modes (FROGChem parser).
174
+
175
+ Returns:
176
+ np.ndarray: (n_modes, n_atoms, 3), same order as the frequencies of parse_freq
177
+ (translation/rotation modes, i.e. zero frequencies, are dropped). Empty
178
+ (0, n_atoms, 3) array when the NORMAL MODES block is missing.
179
+ """
180
+ empty = np.empty((0, n_atoms, 3))
166
181
  try:
167
- block = self.fl.split("NORMAL MODES")[-1].split("\n\n\n")[0]
168
- lines = block.split("\n")
169
- n_total = 3 * n_atoms
170
- max_col = -1
171
- for line in lines:
172
- stripped = line.strip()
173
- if not stripped or stripped.startswith("-"):
174
- continue
175
- header_match = re.match(r'^\s*(\d+(?:\s+\d+)*)\s*$', stripped)
176
- if header_match:
177
- cols = [int(x) for x in header_match.group(1).split()]
178
- if cols and cols[-1] > max_col:
179
- max_col = cols[-1]
180
- if max_col < 0:
181
- return np.empty((0, n_atoms, 3))
182
- matrix = np.zeros((n_total, max_col + 1))
183
- current_columns = []
184
- for line in lines:
185
- stripped = line.strip()
186
- if not stripped or stripped.startswith("-"):
187
- continue
188
- header_match = re.match(r'^\s*(\d+(?:\s+\d+)*)\s*$', stripped)
189
- if header_match:
190
- current_columns = [int(x) for x in header_match.group(1).split()]
191
- continue
192
- data_match = re.match(
193
- r'^\s*(\d+)\s+((?:[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?\s*)+)', stripped
194
- )
195
- if data_match and current_columns:
196
- row_idx = int(data_match.group(1))
197
- coeff_str = data_match.group(2)
198
- coeffs = [float(c) for c in re.findall(
199
- r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?', coeff_str
200
- )]
201
- for i, c in enumerate(coeffs):
202
- if i < len(current_columns) and row_idx < n_total:
203
- col = current_columns[i]
204
- matrix[row_idx, col] = c
205
- n_modes = matrix.shape[1]
206
- all_modes = np.zeros((n_modes, n_atoms, 3))
207
- for mode_idx in range(n_modes):
208
- vec = matrix[:n_total, mode_idx]
209
- all_modes[mode_idx] = vec.reshape(n_atoms, 3)
210
- if hasattr(self, '_freq_mask') and self._freq_mask is not None and len(self._freq_mask) == len(all_modes):
211
- all_modes = all_modes[self._freq_mask]
212
- return all_modes
213
- except Exception:
214
- return np.empty((0, n_atoms, 3))
182
+ matrix = orca_get_normal_modes(self.fl, self.regex, N=3 * n_atoms)
183
+ except ValueError:
184
+ return empty
185
+ except Exception as err:
186
+ self.log.warning(f"Normal modes could not be parsed: {err}")
187
+ return empty
188
+ modes = to_displacements(matrix, n_atoms)
189
+ freq = self._all_freq()
190
+ if len(freq) == len(modes):
191
+ modes = modes[freq != 0]
192
+ return modes
215
193
 
216
194
  def parse_tddft(self) -> Tuple[np.ndarray, np.ndarray]:
217
195
  """
@@ -0,0 +1,228 @@
1
+ """Vibrational-analysis parsers shared with FROGChem (frogchem).
2
+
3
+ COPY of the FROGChem frequency parsers -- keep in sync with:
4
+ frogchem/orca_parser/hessian.py get_freq, get_normal_modes, to_displacements
5
+ frogchem/gaussian_parser/hessian.py get_freq, get_ir_intensities, get_normal_modes,
6
+ align_normal_modes (+ private helpers)
7
+ Functions are renamed with an ``orca_`` / ``gaussian_`` prefix; bodies are identical.
8
+ They take the whole output text ``fl`` and a regex dict ``reg`` with the FROGChem keys.
9
+
10
+ Layout of the normal modes returned by ``*_get_normal_modes``: (3N, n_modes), rows
11
+ x0, y0, z0, x1, ...; ``to_displacements`` turns it into (n_modes, N, 3) displacement vectors.
12
+ """
13
+ import re
14
+ import logging
15
+ import numpy as np
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # ORCA (frogchem/orca_parser/hessian.py)
22
+ # ---------------------------------------------------------------------------
23
+
24
+ def orca_get_freq(fl, reg):
25
+ """Mode numbers and frequencies (cm-1) of the last VIBRATIONAL FREQUENCIES block.
26
+
27
+ Translations/rotations are included as zero frequencies, imaginary modes are negative.
28
+
29
+ NOTE: copied verbatim in ensemble_analyzer/parsers/vibrations.py (orca_get_freq).
30
+ """
31
+ f_ = re.findall(reg['frequency'], fl)[-1]
32
+ match = np.array(re.findall(r'(\d+):\s+(-?[\d.]+)\s+cm\*\*-1*', f_), dtype=np.float64)
33
+ numbers_mode, freq = match[:, 0].astype(int), match[:, 1]
34
+ return numbers_mode, freq.astype(np.float64)
35
+
36
+
37
+ def orca_get_normal_modes(fl, reg, N):
38
+ """Cartesian displacement vectors of the NORMAL MODES block.
39
+
40
+ Returns an (N, n_modes) matrix: N = 3 * n_atoms rows (x0, y0, z0, x1, ...), one column per
41
+ mode as numbered by ORCA (translations/rotations included, so n_modes = N for a full Hessian).
42
+ The number of columns is read from the block headers.
43
+
44
+ NOTE: copied verbatim in ensemble_analyzer/parsers/vibrations.py (orca_get_normal_modes).
45
+ Keep the two copies in sync.
46
+ """
47
+ matches = re.findall(reg['normal_modes'], fl, re.DOTALL)
48
+ if not matches:
49
+ raise ValueError("Pattern 'normal_modes' non trovato nel file")
50
+
51
+ target_text = matches[-1].split('\n\n')[-1]
52
+ header_pattern = r'^\s*(\d+(?:\s+\d+)*)\s*$'
53
+ data_pattern = r'^\s*(\d+)\s+((?:[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?\s*)+)'
54
+
55
+ lines = target_text.strip().split('\n')
56
+ n_cols = max(
57
+ (int(line.split()[-1]) + 1 for line in lines if re.match(header_pattern, line.strip())),
58
+ default=N,
59
+ )
60
+ matrix = np.zeros((N, n_cols))
61
+ current_columns = []
62
+
63
+ for line in lines:
64
+ line = line.strip()
65
+ if not line:
66
+ continue
67
+
68
+ header_match = re.match(header_pattern, line)
69
+ if header_match:
70
+ current_columns = [int(x) for x in header_match.group(1).split()]
71
+ continue
72
+
73
+ data_match = re.match(data_pattern, line)
74
+ if data_match and current_columns:
75
+ try:
76
+ row_idx = int(data_match.group(1))
77
+ coefficients_str = data_match.group(2)
78
+ coefficients = re.findall(r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?', coefficients_str)
79
+ coefficients = [float(c) for c in coefficients]
80
+
81
+ for i, coeff in enumerate(coefficients):
82
+ if i < len(current_columns) and row_idx < N and current_columns[i] < n_cols:
83
+ matrix[row_idx, current_columns[i]] = coeff
84
+ except (ValueError, IndexError) as e:
85
+ logger.warning("Errore processando riga dati '%s': %s", line, e)
86
+ continue
87
+
88
+ return matrix
89
+
90
+
91
+ def to_displacements(normal_modes, n_atoms):
92
+ """(3N, n_modes) normal-mode matrix -> (n_modes, n_atoms, 3) displacement vectors.
93
+
94
+ NOTE: copied verbatim in ensemble_analyzer/parsers/vibrations.py.
95
+ """
96
+ normal_modes = np.asarray(normal_modes, dtype=np.float64)
97
+ return np.ascontiguousarray(normal_modes[:3 * n_atoms].T).reshape(-1, n_atoms, 3)
98
+
99
+
100
+ # ---------------------------------------------------------------------------
101
+ # Gaussian (frogchem/gaussian_parser/hessian.py)
102
+ # ---------------------------------------------------------------------------
103
+
104
+ _ORIENT_ROW = re.compile(r'^\s*\d+\s+(\d+)\s+(?:-?\d+\s+)?(-?\d+\.\d+)\s+(-?\d+\.\d+)\s+(-?\d+\.\d+)\s*$')
105
+ _MODE_ROW = re.compile(r'^\s*(\d+)\s+(\d+)((?:\s+[-+]?\d+\.\d+)+)\s*$')
106
+ _FLOAT = re.compile(r'[-+]?\d+\.\d+')
107
+
108
+
109
+ def _freq_block(fl, reg):
110
+ # last frequency section (with freq=HPModes the standard 3-column section is printed last)
111
+ return fl.split(reg['freq_section'])[-1].split(reg['freq_end'])[0]
112
+
113
+
114
+ def gaussian_get_freq(fl, reg):
115
+ if reg['freq_section'] not in fl:
116
+ return np.array([])
117
+
118
+ block = _freq_block(fl, reg)
119
+ freq = np.array([float(x) for m in re.findall(reg['freq_pattern'], block) for x in m.split()])
120
+ return freq
121
+
122
+
123
+ def gaussian_get_ir_intensities(fl, reg):
124
+ if reg['freq_section'] not in fl:
125
+ return np.zeros(shape=(1, 2))
126
+
127
+ block = _freq_block(fl, reg)
128
+ freq = np.array([float(x) for m in re.findall(reg['freq_pattern'], block) for x in m.split()])
129
+ ir = np.array([float(x) for m in re.findall(reg['ir_pattern'], block) for x in m.split()])
130
+ if len(freq) == 0 or len(ir) == 0:
131
+ return np.zeros(shape=(1, 2))
132
+ return np.column_stack((freq, ir))
133
+
134
+
135
+ def gaussian_get_normal_modes(fl, reg, N):
136
+ """Cartesian displacement vectors of the normal modes, in Gaussian's *standard orientation*.
137
+
138
+ Returns an (N, n_modes) matrix, N = 3 * n_atoms rows (x0, y0, z0, x1, ...), one column per
139
+ vibrational mode in the order of get_freq (same layout as orca_parser.hessian.get_normal_modes,
140
+ but Gaussian prints no translations/rotations). Read from the standard 3-modes-per-block table
141
+ ('Atom AN X Y Z ...'). Use align_normal_modes to express them in the frame of the parsed
142
+ geometry. Raises ValueError when the file has no frequency section.
143
+ """
144
+ if reg['freq_section'] not in fl:
145
+ raise ValueError("Frequency section not found")
146
+
147
+ columns = [] # one (N,) vector per mode
148
+ block_modes = 0 # modes in the current block (from its 'Frequencies --' line)
149
+ in_table = False
150
+ for line in _freq_block(fl, reg).splitlines():
151
+ freq_match = re.search(reg['freq_pattern'], line)
152
+ if freq_match:
153
+ block_modes = len(freq_match.group(1).split())
154
+ block = [np.zeros(N) for _ in range(block_modes)]
155
+ columns.extend(block)
156
+ in_table = False
157
+ continue
158
+ if block_modes and line.split()[:2] == ['Atom', 'AN']:
159
+ in_table = True
160
+ continue
161
+ if not in_table:
162
+ continue
163
+ row = _MODE_ROW.match(line)
164
+ if row is None:
165
+ in_table = False
166
+ continue
167
+ atom = int(row.group(1)) - 1
168
+ values = [float(x) for x in _FLOAT.findall(row.group(3))]
169
+ if len(values) != 3 * block_modes or 3 * atom + 2 >= N:
170
+ logger.warning("Skipping malformed normal-mode row: %r", line)
171
+ continue
172
+ for k in range(block_modes):
173
+ block[k][3 * atom:3 * atom + 3] = values[3 * k:3 * k + 3]
174
+
175
+ if not columns:
176
+ return np.zeros((N, 0))
177
+ return np.column_stack(columns)
178
+
179
+
180
+ def _last_orientation(fl, header):
181
+ """Coordinates (Å) of the last orientation table with the given header, or None."""
182
+ if header not in fl:
183
+ return None
184
+ coords, dashes = [], 0
185
+ for line in fl.split(header)[-1].splitlines():
186
+ if line.strip().startswith('---'):
187
+ dashes += 1
188
+ if dashes >= 3:
189
+ break
190
+ continue
191
+ if dashes == 2:
192
+ match = _ORIENT_ROW.match(line)
193
+ if not match:
194
+ break
195
+ coords.append([float(match.group(i)) for i in (2, 3, 4)])
196
+ return np.array(coords, dtype=float) if coords else None
197
+
198
+
199
+ def _kabsch(P, Q):
200
+ """Proper rotation R and RMSD (Å) such that (P - <P>) @ R ≈ (Q - <Q>)."""
201
+ Pc, Qc = P - P.mean(axis=0), Q - Q.mean(axis=0)
202
+ U, _, Vt = np.linalg.svd(Pc.T @ Qc)
203
+ d = np.sign(np.linalg.det(U @ Vt))
204
+ R = U @ np.diag([1.0, 1.0, d]) @ Vt
205
+ rmsd = float(np.sqrt(np.mean(np.sum((Pc @ R - Qc) ** 2, axis=1))))
206
+ return R, rmsd
207
+
208
+
209
+ def gaussian_align_normal_modes(fl, normal_modes, target='Input orientation:', source='Standard orientation:', tol=1e-2):
210
+ """Rotate (3N, n_modes) normal modes from the standard orientation to the `target` frame.
211
+
212
+ Gaussian prints the normal coordinates in the standard orientation, while the parsers return
213
+ the input-orientation geometry when it is printed. The rotation is the Kabsch fit of the last
214
+ standard-orientation table onto the last `target` table; when one of the two tables is
215
+ missing (e.g. NoSymm jobs print only the input orientation) the modes are returned unchanged.
216
+ """
217
+ std = _last_orientation(fl, source)
218
+ tgt = _last_orientation(fl, target)
219
+ if std is None or tgt is None or std.shape != tgt.shape or normal_modes.shape[0] != std.size:
220
+ return normal_modes
221
+ R, rmsd = _kabsch(std, tgt)
222
+ if rmsd > tol:
223
+ logger.warning("Standard and input orientations do not superimpose (RMSD %.3g Å): "
224
+ "normal modes left in the standard orientation", rmsd)
225
+ return normal_modes
226
+ n_atoms = std.shape[0]
227
+ disp = normal_modes.T.reshape(-1, n_atoms, 3) @ R
228
+ return np.ascontiguousarray(disp.reshape(-1, 3 * n_atoms).T)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ensemble-analyzer
3
- Version: 1.2
3
+ Version: 1.2.1
4
4
  Summary: Automatic Refinement of Conformational Ensemble
5
5
  Author-email: Andrea Pellegrini <andrea.pellegrini15@unibo.it>, Paolo Righi <paolo.righi@unibo.it>, Andrea Mazzanti <andrea.mazzanti@unibo.it>, Michele Mancinelli <michele.mancinelli@unibo.it>
6
6
  Maintainer-email: Andrea Pellegrini <andrea.pellegrini15@unibo.it>, Paolo Righi <paolo.righi@unibo.it>
@@ -97,6 +97,7 @@ src/ensemble_analyzer/parsers/base.py
97
97
  src/ensemble_analyzer/parsers/gaussian.py
98
98
  src/ensemble_analyzer/parsers/nwchem.py
99
99
  src/ensemble_analyzer/parsers/orca.py
100
+ src/ensemble_analyzer/parsers/vibrations.py
100
101
  src/ensemble_analyzer/protocol/__init__.py
101
102
  src/ensemble_analyzer/protocol/protocol.py
102
103
  src/ensemble_analyzer/protocol/solvent.py