DescripyTor 0.1.0__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.
Files changed (58) hide show
  1. MolFeatures/Getting_started_with_examples/descriptor_extraction_toolkit/_dx_check.py +382 -0
  2. MolFeatures/Getting_started_with_examples/descriptor_extraction_toolkit/descriptor_extractor.py +703 -0
  3. MolFeatures/Getting_started_with_examples/descriptor_extraction_toolkit/feature_packages.py +652 -0
  4. MolFeatures/Getting_started_with_examples/descriptor_extraction_toolkit/make_picker.py +506 -0
  5. MolFeatures/Getting_started_with_examples/descriptor_extraction_toolkit/webapp/app.py +1835 -0
  6. MolFeatures/M1_pre_calculations/__init__.py +0 -0
  7. MolFeatures/M1_pre_calculations/main.py +69 -0
  8. MolFeatures/M1_pre_calculations/outwards_senders.py +104 -0
  9. MolFeatures/M1_pre_calculations/pre_calculation_editors.py +74 -0
  10. MolFeatures/M2_data_extractor/__init__.py +45 -0
  11. MolFeatures/M2_data_extractor/cube_sterimol.py +616 -0
  12. MolFeatures/M2_data_extractor/data_extractor.py +2284 -0
  13. MolFeatures/M2_data_extractor/extractor_utils/__init__.py +0 -0
  14. MolFeatures/M2_data_extractor/extractor_utils/bond_angle_length_utils.py +247 -0
  15. MolFeatures/M2_data_extractor/extractor_utils/dipole_utils.py +275 -0
  16. MolFeatures/M2_data_extractor/extractor_utils/sterimol_utils.py +913 -0
  17. MolFeatures/M2_data_extractor/extractor_utils/sterimol_utils_old.py +859 -0
  18. MolFeatures/M2_data_extractor/extractor_utils/vibrations_utils.py +692 -0
  19. MolFeatures/M2_data_extractor/feather_extractor.py +740 -0
  20. MolFeatures/M2_data_extractor/feature_extraction_gui.html +6363 -0
  21. MolFeatures/M2_data_extractor/gaussian_handler.py +235 -0
  22. MolFeatures/M2_data_extractor/gui_server.py +454 -0
  23. MolFeatures/M2_data_extractor/morfeus_sterics.py +214 -0
  24. MolFeatures/M2_data_extractor/sterimol_standalone.py +1781 -0
  25. MolFeatures/M3_modeler/__init__.py +0 -0
  26. MolFeatures/M3_modeler/modeling.py +3566 -0
  27. MolFeatures/M3_modeler/modeling_utils.py +1200 -0
  28. MolFeatures/M3_modeler/plot.py +3862 -0
  29. MolFeatures/M3_modeler/plot_backup.py +3772 -0
  30. MolFeatures/MolAlign/MCS_optimization.py +131 -0
  31. MolFeatures/MolAlign/RDKIT_utils.py +655 -0
  32. MolFeatures/MolAlign/__init__.py +0 -0
  33. MolFeatures/MolAlign/df_funs.py +99 -0
  34. MolFeatures/MolAlign/extract_bo.py +251 -0
  35. MolFeatures/MolAlign/fmcs.py +2654 -0
  36. MolFeatures/MolAlign/gaussian_handler.py +193 -0
  37. MolFeatures/MolAlign/loss.py +19 -0
  38. MolFeatures/MolAlign/molecules/results/json_swap.py +120 -0
  39. MolFeatures/MolAlign/pybel_ff.py +56 -0
  40. MolFeatures/MolAlign/renumbering.py +821 -0
  41. MolFeatures/MolAlign/rotations.py +14 -0
  42. MolFeatures/MolAlign/trial.py +48 -0
  43. MolFeatures/__init__.py +16 -0
  44. MolFeatures/__main__.py +1848 -0
  45. MolFeatures/utils/__init__.py +0 -0
  46. MolFeatures/utils/_build_viewer_nb.py +894 -0
  47. MolFeatures/utils/constants.py +64 -0
  48. MolFeatures/utils/file_handlers.py +154 -0
  49. MolFeatures/utils/help_functions.py +1818 -0
  50. MolFeatures/utils/mcs_and_alignment.py +339 -0
  51. MolFeatures/utils/obabel_wrappers.py +21 -0
  52. MolFeatures/utils/rdkit_nx_wrappers.py +67 -0
  53. MolFeatures/utils/rdkit_utils.py +158 -0
  54. MolFeatures/utils/visualize.py +875 -0
  55. descripytor-0.1.0.dist-info/METADATA +723 -0
  56. descripytor-0.1.0.dist-info/RECORD +58 -0
  57. descripytor-0.1.0.dist-info/WHEEL +5 -0
  58. descripytor-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,382 @@
1
+ """
2
+ Preflight checks for descriptor_extractor.py runs.
3
+
4
+ This script is intentionally lightweight: it validates paths, optional packages,
5
+ file-name alignment, Gaussian log contents, and atom-index ranges before a run.
6
+ It does not extract descriptors.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import contextlib
13
+ import glob
14
+ import importlib.util
15
+ import io
16
+ import json
17
+ import os
18
+ import re
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ pd = None
23
+ PANDAS_IMPORT_ERROR = None
24
+
25
+
26
+ ENGINE_PACKAGES = {
27
+ "rdkit": ["rdkit", "ase"],
28
+ "rdkit_fp": ["rdkit", "ase"],
29
+ "mordred": ["mordred", "rdkit", "ase"],
30
+ "deepchem": ["deepchem"],
31
+ "rafbl": ["moltop"],
32
+ "qm": ["autoqchem"],
33
+ "aqme_qdescp": ["aqme"],
34
+ "morfeus_sterimol": ["morfeus"],
35
+ "morfeus_suite": ["morfeus"],
36
+ }
37
+
38
+ XYZ_ENGINES = {
39
+ "xyz_sterimol",
40
+ "xyz_geometry",
41
+ "xyz_buried_volume",
42
+ "morfeus_sterimol",
43
+ "morfeus_suite",
44
+ "rdkit",
45
+ "rdkit_fp",
46
+ "mordred",
47
+ "deepchem",
48
+ "rafbl",
49
+ "aqme_qdescp",
50
+ }
51
+ FEATHER_ENGINES = {"descripytor_full", "descripytor_steric"}
52
+ LOG_ENGINES = {"qm"}
53
+
54
+
55
+ class Report:
56
+ def __init__(self):
57
+ self.errors = []
58
+ self.warnings = []
59
+ self.ok = []
60
+
61
+ def add(self, level, message):
62
+ getattr(self, level).append(message)
63
+
64
+ def print(self):
65
+ for title, items, mark in (
66
+ ("OK", self.ok, "[ok]"),
67
+ ("Warnings", self.warnings, "[warn]"),
68
+ ("Errors", self.errors, "[error]"),
69
+ ):
70
+ if not items:
71
+ continue
72
+ print(f"\n{title}")
73
+ for item in items:
74
+ print(f" {mark} {item}")
75
+ print(f"\nSummary: {len(self.errors)} errors, {len(self.warnings)} warnings, {len(self.ok)} ok")
76
+
77
+ @property
78
+ def exit_code(self):
79
+ return 2 if self.errors else (1 if self.warnings else 0)
80
+
81
+
82
+ def have(module):
83
+ try:
84
+ return importlib.util.find_spec(module) is not None
85
+ except Exception:
86
+ return False
87
+
88
+
89
+ def get_pandas():
90
+ global pd, PANDAS_IMPORT_ERROR
91
+ if pd is not None or PANDAS_IMPORT_ERROR is not None:
92
+ return pd
93
+ try:
94
+ with contextlib.redirect_stderr(io.StringIO()):
95
+ import pandas as _pd
96
+ pd = _pd
97
+ except Exception as e: # pragma: no cover - environment-specific
98
+ PANDAS_IMPORT_ERROR = e
99
+ pd = None
100
+ return pd
101
+
102
+
103
+ def load_config(path):
104
+ with open(path, "r", encoding="utf-8") as f:
105
+ return json.load(f)
106
+
107
+
108
+ def enabled_engines(cfg):
109
+ return {
110
+ name: ecfg
111
+ for name, ecfg in cfg.get("engines", {}).items()
112
+ if ecfg.get("enabled", True)
113
+ }
114
+
115
+
116
+ def list_stems(folder, pattern):
117
+ if not folder or not os.path.isdir(folder):
118
+ return []
119
+ return sorted(Path(p).stem for p in glob.glob(os.path.join(folder, pattern)))
120
+
121
+
122
+ def xyz_atom_counts(xyz_dir):
123
+ counts = {}
124
+ elements = {}
125
+ for path in glob.glob(os.path.join(xyz_dir, "*.xyz")):
126
+ stem = Path(path).stem
127
+ try:
128
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
129
+ lines = [x.strip() for x in f.readlines() if x.strip()]
130
+ n = int(lines[0])
131
+ els = [line.split()[0] for line in lines[2 : 2 + n]]
132
+ counts[stem] = len(els)
133
+ elements[stem] = els
134
+ except Exception:
135
+ counts[stem] = 0
136
+ elements[stem] = []
137
+ return counts, elements
138
+
139
+
140
+ def feather_summaries(feather_dir):
141
+ counts = {}
142
+ elements = {}
143
+ capabilities = {}
144
+ pandas = get_pandas()
145
+ if pandas is None or not have("pyarrow"):
146
+ return counts, elements, capabilities
147
+ feather_read_disabled = False
148
+ for path in glob.glob(os.path.join(feather_dir, "*.feather")):
149
+ if feather_read_disabled:
150
+ break
151
+ stem = Path(path).stem
152
+ try:
153
+ with contextlib.redirect_stderr(io.StringIO()):
154
+ df = pandas.read_feather(path)
155
+ cols = [str(c).strip() for c in df.columns]
156
+ df.columns = cols
157
+ if {"atom", "x", "y", "z"}.issubset(cols):
158
+ xyz = df[["atom", "x", "y", "z"]].dropna()
159
+ elements[stem] = xyz["atom"].astype(str).tolist()
160
+ counts[stem] = len(elements[stem])
161
+ else:
162
+ xyz = df.iloc[:, 0:4].dropna()
163
+ elements[stem] = xyz.iloc[:, 0].astype(str).tolist()
164
+ counts[stem] = len(elements[stem])
165
+ capabilities[stem] = {
166
+ "dipole": all(c in cols for c in ("dip_x", "dip_y", "dip_z")),
167
+ "freq": "Frequency" in cols,
168
+ "ir": "IR" in cols,
169
+ "nbo": "nbo_charge" in cols,
170
+ "hirshfeld": "hirshfeld_charge" in cols,
171
+ "cm5": "cm5_charge" in cols,
172
+ }
173
+ except Exception:
174
+ feather_read_disabled = True
175
+ counts.clear()
176
+ elements.clear()
177
+ capabilities.clear()
178
+ return counts, elements, capabilities
179
+
180
+
181
+ def flatten_indices(value):
182
+ out = []
183
+ if value is None:
184
+ return out
185
+ if isinstance(value, int):
186
+ return [value]
187
+ if isinstance(value, (list, tuple)):
188
+ for item in value:
189
+ out.extend(flatten_indices(item))
190
+ return out
191
+
192
+
193
+ def max_picked_index(cfg):
194
+ mx = 0
195
+ engines = cfg.get("engines", {})
196
+ full = engines.get("descripytor_full", {}).get("atoms", {})
197
+ for key, val in full.items():
198
+ if key in {"stretch_threshold", "bend_threshold"}:
199
+ continue
200
+ ints = [x for x in flatten_indices(val) if isinstance(x, int)]
201
+ if ints:
202
+ mx = max(mx, max(ints))
203
+ for name, ecfg in engines.items():
204
+ for key in ("pairs", "angles", "bond_lengths", "sterimol_pairs", "cone_atoms", "pyramid_atoms"):
205
+ ints = [x for x in flatten_indices(ecfg.get(key)) if isinstance(x, int)]
206
+ if ints:
207
+ mx = max(mx, max(ints))
208
+ for key in ("metal_index",):
209
+ if isinstance(ecfg.get(key), int):
210
+ mx = max(mx, ecfg[key])
211
+ return mx
212
+
213
+
214
+ def scan_logs(log_dir):
215
+ summary = {}
216
+ for path in glob.glob(os.path.join(log_dir, "*.log")) + glob.glob(os.path.join(log_dir, "*.out")):
217
+ stem = Path(path).stem
218
+ try:
219
+ text = Path(path).read_text(encoding="utf-8", errors="ignore")
220
+ summary[stem] = {
221
+ "normal_termination": "Normal termination" in text,
222
+ "freq": "Frequencies --" in text,
223
+ "dipole": "Dipole moment" in text,
224
+ "standard_orientation": "Standard orientation" in text,
225
+ "nbo": bool(re.search(r"\bNBO\b|Natural Population|NPA", text, re.I)),
226
+ }
227
+ except Exception:
228
+ summary[stem] = {}
229
+ return summary
230
+
231
+
232
+ def compare_name_sets(report, label_a, a, label_b, b):
233
+ if not a or not b:
234
+ return
235
+ set_a, set_b = set(a), set(b)
236
+ missing_b = sorted(set_a - set_b)[:10]
237
+ missing_a = sorted(set_b - set_a)[:10]
238
+ if not missing_a and not missing_b:
239
+ report.add("ok", f"{label_a} names match {label_b} names ({len(set_a & set_b)} shared)")
240
+ else:
241
+ report.add("warnings", f"name mismatch: {label_a} not in {label_b}: {missing_b or 'none'}; {label_b} not in {label_a}: {missing_a or 'none'}")
242
+
243
+
244
+ def run_checks(cfg):
245
+ report = Report()
246
+ engines = enabled_engines(cfg)
247
+ report.add("ok", f"enabled engines: {', '.join(engines) if engines else 'none'}")
248
+
249
+ root_dir = cfg.get("root_dir")
250
+ feather_dir = cfg.get("feather_dir")
251
+ xyz_dir = cfg.get("xyz_dir")
252
+ log_dir = cfg.get("log_dir") or engines.get("qm", {}).get("log_dir")
253
+ derive_xyz = bool(cfg.get("derive_xyz_from_feathers"))
254
+
255
+ if root_dir and os.path.isdir(root_dir):
256
+ report.add("ok", f"root_dir exists: {root_dir}")
257
+ elif root_dir:
258
+ report.add("warnings", f"root_dir not found: {root_dir}")
259
+
260
+ needs_feather = bool(FEATHER_ENGINES & set(engines)) or derive_xyz
261
+ needs_xyz = bool(XYZ_ENGINES & set(engines)) and not derive_xyz
262
+ needs_log = bool(LOG_ENGINES & set(engines))
263
+
264
+ feather_counts, feather_elements, feather_caps = {}, {}, {}
265
+ if needs_feather:
266
+ if not feather_dir or not os.path.isdir(feather_dir):
267
+ report.add("errors", f"feather_dir is required but not found: {feather_dir}")
268
+ else:
269
+ stems = list_stems(feather_dir, "*.feather")
270
+ report.add("ok", f"found {len(stems)} feather files")
271
+ feather_counts, feather_elements, feather_caps = feather_summaries(feather_dir)
272
+ if get_pandas() is None:
273
+ report.add("warnings", "pandas is not importable; feather contents were not scanned")
274
+ elif not have("pyarrow"):
275
+ report.add("warnings", "pyarrow is not importable; feather contents were not scanned")
276
+ elif stems and not feather_counts:
277
+ report.add("warnings", "feather metadata scan failed; check pyarrow/numpy compatibility in this environment")
278
+
279
+ xyz_counts, xyz_elements = {}, {}
280
+ if needs_xyz:
281
+ if not xyz_dir or not os.path.isdir(xyz_dir):
282
+ report.add("errors", f"xyz_dir is required but not found: {xyz_dir}")
283
+ else:
284
+ stems = list_stems(xyz_dir, "*.xyz")
285
+ report.add("ok", f"found {len(stems)} xyz files")
286
+ xyz_counts, xyz_elements = xyz_atom_counts(xyz_dir)
287
+
288
+ if needs_log:
289
+ if not log_dir or not os.path.isdir(log_dir):
290
+ report.add("errors", f"log_dir is required for qm but not found: {log_dir}")
291
+ else:
292
+ logs = scan_logs(log_dir)
293
+ report.add("ok", f"found {len(logs)} Gaussian log/out files")
294
+ bad = [k for k, v in logs.items() if not v.get("normal_termination")]
295
+ if bad:
296
+ report.add("warnings", f"logs without Normal termination: {bad[:10]}")
297
+ if logs and not any(v.get("freq") for v in logs.values()):
298
+ report.add("warnings", "no frequency blocks found in logs; vibration features may be empty")
299
+ if logs and not any(v.get("dipole") for v in logs.values()):
300
+ report.add("warnings", "no dipole blocks found in logs")
301
+
302
+ if feather_counts and xyz_counts:
303
+ compare_name_sets(report, "feather", feather_counts, "xyz", xyz_counts)
304
+ shared = sorted(set(feather_elements) & set(xyz_elements))
305
+ mismatched = [s for s in shared if feather_elements[s] != xyz_elements[s]]
306
+ if mismatched:
307
+ report.add("warnings", f"atom element/order mismatch between feather and xyz for: {mismatched[:10]}")
308
+ elif shared:
309
+ report.add("ok", f"atom element/order matches for {len(shared)} shared molecules")
310
+
311
+ picked_max = max_picked_index(cfg)
312
+ if picked_max:
313
+ counts = feather_counts or xyz_counts
314
+ too_small = [name for name, n in counts.items() if n and picked_max > n]
315
+ if too_small:
316
+ report.add("errors", f"picked atom index {picked_max} exceeds atom count for: {too_small[:10]}")
317
+ elif counts:
318
+ report.add("ok", f"picked atom indices fit scanned molecules (max index {picked_max})")
319
+
320
+ full_atoms = engines.get("descripytor_full", {}).get("atoms", {})
321
+ if full_atoms.get("dipole") and feather_caps:
322
+ missing = [k for k, v in feather_caps.items() if not v.get("dipole")]
323
+ if missing:
324
+ report.add("warnings", f"dipole selected but dipole columns missing in feathers: {missing[:10]}")
325
+ if (full_atoms.get("stretching_vibration") or full_atoms.get("bending_vibration") or full_atoms.get("ring_vibration")) and feather_caps:
326
+ missing = [k for k, v in feather_caps.items() if not v.get("freq")]
327
+ if missing:
328
+ report.add("warnings", f"vibration selections present but Frequency column missing: {missing[:10]}")
329
+
330
+ for engine, packages in ENGINE_PACKAGES.items():
331
+ if engine not in engines:
332
+ continue
333
+ missing = [pkg for pkg in packages if not have(pkg)]
334
+ if missing:
335
+ report.add("warnings", f"{engine} enabled but missing import(s): {', '.join(missing)}")
336
+ else:
337
+ report.add("ok", f"{engine} optional imports available")
338
+
339
+ return report
340
+
341
+
342
+ def print_install_report(cfg):
343
+ engines = enabled_engines(cfg)
344
+ needed = []
345
+ for name in engines:
346
+ needed.extend(ENGINE_PACKAGES.get(name, []))
347
+ missing = sorted({pkg for pkg in needed if not have(pkg)})
348
+ if not missing:
349
+ print("All optional imports for enabled engines are available.")
350
+ return
351
+ pip_names = {
352
+ "morfeus": "morfeus-ml",
353
+ }
354
+ print("Missing optional packages:")
355
+ for pkg in missing:
356
+ print(f" - {pkg}")
357
+ print("\nSuggested pip command:")
358
+ print(" pip install " + " ".join(pip_names.get(pkg, pkg) for pkg in missing))
359
+
360
+
361
+ def main(argv=None):
362
+ ap = argparse.ArgumentParser(description="Preflight descriptor extraction config.")
363
+ ap.add_argument("--config", default="run_config.json", help="Path to run_config.json")
364
+ ap.add_argument("--install-report", action="store_true", help="Show missing optional packages for enabled engines")
365
+ args = ap.parse_args(argv)
366
+
367
+ if not os.path.exists(args.config):
368
+ print(f"[error] config not found: {args.config}")
369
+ return 2
370
+ cfg = load_config(args.config)
371
+
372
+ if args.install_report:
373
+ print_install_report(cfg)
374
+ return 0
375
+
376
+ report = run_checks(cfg)
377
+ report.print()
378
+ return report.exit_code
379
+
380
+
381
+ if __name__ == "__main__":
382
+ raise SystemExit(main())