orca-webui 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.
orca_webui/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """orca-webui: a Gradio web UI for simple ORCA computational-chemistry calculations.
2
+
3
+ Public entry point is :func:`orca_webui.app.main` (exposed as the ``orca-webui``
4
+ console command). Submodules are imported lazily by that entry point so that
5
+ lightweight consumers (e.g. importing :mod:`orca_webui.utils` for its parsing/spectrum
6
+ helpers) do not pull in the full Gradio/FastAPI UI stack.
7
+ """
8
+
9
+ __version__ = "0.1.0"
orca_webui/app.py ADDED
@@ -0,0 +1,104 @@
1
+ """Application entry point.
2
+
3
+ Builds the FastAPI host, mounts the ``/static`` file server and the Gradio Blocks UI
4
+ (the three tabs: conformer generation, calculation, result), and serves everything on
5
+ the first free port at/after 7860.
6
+
7
+ Runtime directories (``data/`` and ``static/``) are created in the **current working
8
+ directory** so an installed copy of the package never writes into its own install
9
+ location; ``styles.css`` ships inside the package and is read from there. Transient
10
+ artifacts left in ``static/`` (and stray ``*.log`` files) by a previous run are cleaned
11
+ up on startup.
12
+ """
13
+ import socket
14
+ from pathlib import Path
15
+
16
+ import gradio as gr
17
+ import uvicorn
18
+ from fastapi import FastAPI
19
+ from fastapi.staticfiles import StaticFiles
20
+
21
+ from .working_directory import working_directory_blocks
22
+ from .conformer_generation import conformer_generation_tab_content
23
+ from .calculation import calculation_tab_content
24
+ from .result import result_tab_content
25
+
26
+ # styles.css is packaged data, resolved relative to this module (read-only).
27
+ _STYLES_PATH = Path(__file__).parent / "styles.css"
28
+
29
+ # Glob patterns for transient files removed from the run directory on startup.
30
+ _TRANSIENT_PATTERNS = (
31
+ "*.log",
32
+ "static/*.html",
33
+ "static/**/*.html",
34
+ "static/**/*.cube",
35
+ "static/**/*.xyz",
36
+ )
37
+
38
+
39
+ def _cleanup_transient_files(run_dir: Path) -> None:
40
+ """Delete transient artifacts left in ``run_dir`` by a previous session."""
41
+ for pattern in _TRANSIENT_PATTERNS:
42
+ for filepath in run_dir.glob(pattern):
43
+ try:
44
+ filepath.unlink()
45
+ except OSError:
46
+ pass # best-effort cleanup; ignore files we cannot remove
47
+
48
+
49
+ def find_available_port(start_port: int = 7860) -> int:
50
+ """Return the first TCP port at/after ``start_port`` that can be bound on localhost."""
51
+ port = start_port
52
+ while True:
53
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
54
+ try:
55
+ s.bind(('localhost', port))
56
+ return port # Available port found
57
+ except OSError:
58
+ port += 1 # Try next port
59
+
60
+
61
+ def build_app(run_dir: Path | None = None) -> FastAPI:
62
+ """Build the FastAPI app with the Gradio UI mounted.
63
+
64
+ ``run_dir`` (default: current working directory) is where the ``data/`` and
65
+ ``static/`` directories are created and served from.
66
+ """
67
+ run_dir = Path.cwd() if run_dir is None else run_dir
68
+ _cleanup_transient_files(run_dir)
69
+
70
+ app = FastAPI()
71
+
72
+ # Working directories live under ./data; transient viewer files under ./static.
73
+ (run_dir / "data").mkdir(parents=True, exist_ok=True)
74
+ static_dir = run_dir / "static"
75
+ static_dir.mkdir(parents=True, exist_ok=True)
76
+
77
+ # mount FastAPI StaticFiles server
78
+ app.mount("/static", StaticFiles(directory=static_dir), name="static")
79
+
80
+ with gr.Blocks(css_paths=_STYLES_PATH) as blocks:
81
+ with gr.Row():
82
+ working_directory_path_state, working_directory_file_list_state = working_directory_blocks()
83
+ with gr.Column(scale=2):
84
+ with gr.Row(min_height=40):
85
+ status_markdown = gr.Markdown()
86
+ with gr.Row():
87
+ with gr.Tabs():
88
+ conformer_generation_tab_content(working_directory_path_state, working_directory_file_list_state, status_markdown)
89
+ calculation_tab_content(working_directory_path_state, working_directory_file_list_state, status_markdown)
90
+ result_tab_content(working_directory_path_state, working_directory_file_list_state, status_markdown)
91
+
92
+ # mount Gradio app to FastAPI app
93
+ return gr.mount_gradio_app(app, blocks, path="/")
94
+
95
+
96
+ def main() -> None:
97
+ """Console-script entry point: build the app and serve it on the first free port."""
98
+ app = build_app()
99
+ port = find_available_port()
100
+ uvicorn.run(app, host="127.0.0.1", port=port, access_log=False)
101
+
102
+
103
+ if __name__ == "__main__":
104
+ main()
@@ -0,0 +1,277 @@
1
+ """Calculation tab: turn a structure file into an ORCA input file and run ORCA.
2
+
3
+ The upper section builds an ``.inp`` file for the chosen calculation type (single
4
+ point, optimization, frequency, TD-DFT, NMR) and method/basis/solvation settings;
5
+ the lower section invokes the external ``orca`` binary on a selected ``.inp`` and
6
+ captures its stdout to a ``.log``.
7
+ """
8
+ import os
9
+ import time
10
+ import multiprocessing
11
+ import psutil
12
+ import subprocess
13
+ import math
14
+ import gradio as gr
15
+ import pandas as pd
16
+ from rdkit import Chem
17
+ from rdkit.Chem import AllChem
18
+ import cclib
19
+ from .utils import *
20
+
21
+ # Physical RAM in whole GB, used as the upper bound / default of the memory slider.
22
+ max_memory = math.floor(psutil.virtual_memory().total / (1024 ** 3))
23
+
24
+ def on_working_directory_file_list_change(working_directory_file_list, input_file_name):
25
+ """Repopulate the input-structure and input-file dropdowns from the file list.
26
+
27
+ Keeps the currently-typed input file name selected if a matching ``.inp`` exists,
28
+ otherwise falls back to the first available input file. Returns updates for the
29
+ structure dropdown and the input-file dropdown.
30
+ """
31
+ structure_file_names = [f for f in working_directory_file_list if f.endswith('.xyz') or f.endswith('.pdb') or f.endswith('.mol') or f.endswith('.mol2') or f.endswith('.log')]
32
+ input_file_names = [f for f in working_directory_file_list if f.endswith('.inp')]
33
+ if input_file_name + ".inp" in input_file_names:
34
+ input_file_name_value = input_file_name + ".inp"
35
+ else:
36
+ input_file_name_value = input_file_names[0] if len(input_file_names) > 0 else None
37
+
38
+ return gr.update(choices=structure_file_names, value=structure_file_names[0] if len(structure_file_names) > 0 else None, interactive=True), \
39
+ gr.update(choices=input_file_names, value=input_file_name_value, interactive=True)
40
+
41
+ def on_change_calculation_type(calculation_type):
42
+ """React to the calculation-type radio changing.
43
+
44
+ Suggests a default input file name, restricts the available method types (NMR
45
+ excludes semi-empirical/CC), swaps the functional list for TD-DFT, and toggles
46
+ visibility of the TD-DFT sliders and the NMR spin-spin-coupling checkbox.
47
+ """
48
+ if calculation_type == "Single-Point":
49
+ file_name = "single_point"
50
+ elif calculation_type == "Geometry Optimization":
51
+ file_name = "geometry_optimization"
52
+ elif calculation_type == "Frequency":
53
+ file_name = "frequency"
54
+ elif calculation_type == "Time-Dependent Density Functional Theory":
55
+ file_name = "tddft"
56
+ else: # calculation_type == "NMR Spectrum"
57
+ file_name = "nmr_spectrum"
58
+
59
+ if calculation_type == "NMR Spectrum":
60
+ method_types = ["HF", "DFT", "MP2"]
61
+ else:
62
+ method_types = ["HF", "DFT", "Semi-empirical", "MP2", "CCSD", "CCSD(T)"]
63
+
64
+ if calculation_type == "Time-Dependent Density Functional Theory":
65
+ functional_dropdown = gr.Dropdown(label="Functional", value="PBE", choices=["CAM-B3LYP", "PBE", "PBE0", "revPBE", "B97-3C", "wB97X", "wB97X-D3"], allow_custom_value=True)
66
+ else:
67
+ functional_dropdown = gr.Dropdown(label="Functional", value="B3LYP", choices=["BLYP", "B3LYP", "CAM-B3LYP", "B3PW91", "PBE", "PBE0", "revPBE", "B97-3C", "M06L", "M062X", "wB97X", "wB97X-D3"], allow_custom_value=True)
68
+
69
+ show_tddft = calculation_type == "Time-Dependent Density Functional Theory"
70
+
71
+ show_spin_spin_couplings = (calculation_type == "NMR Spectrum")
72
+
73
+ return file_name, gr.update(choices=method_types, value="DFT"), functional_dropdown, gr.update(visible=show_tddft), gr.update(visible=show_tddft), gr.update(visible=show_tddft), gr.update(visible=show_spin_spin_couplings)
74
+
75
+ def on_mm_checkbox_change(mm_checkbox: gr.Checkbox):
76
+ """Show/hide the force-field and max-iterations controls with the MM checkbox."""
77
+ return gr.update(visible=mm_checkbox), gr.update(visible=mm_checkbox)
78
+
79
+ def on_solvation_checkbox_change(solvation_checkbox: gr.Checkbox):
80
+ """Show/hide the solvation-model and solvent controls with the solvation checkbox."""
81
+ return gr.update(visible=solvation_checkbox), gr.update(visible=solvation_checkbox)
82
+
83
+ def on_method_type_change(method_type):
84
+ """Toggle method-name/functional/basis controls to match the chosen method type.
85
+
86
+ Semi-empirical shows a method-name dropdown (AM1/PM3) and hides the basis set;
87
+ DFT shows the functional; everything else shows only the basis set.
88
+ """
89
+ show_method_name = (method_type=="Semi-empirical")
90
+ show_functional = method_type=="DFT"
91
+ show_basis_set = not (method_type=="Semi-empirical")
92
+
93
+ if method_type == "Semi-empirical":
94
+ method_names = ["AM1", "PM3"]
95
+ default_method_name = "AM1"
96
+ else:
97
+ method_names = [""]
98
+ default_method_name = ""
99
+
100
+ return gr.update(choices=method_names, value=default_method_name, visible=show_method_name), gr.update(visible=show_functional), gr.update(visible=show_basis_set)
101
+
102
+ def on_generate_input_file(working_directory_path, input_structure_file_dropdown, calculation_type,
103
+ use_mm, force_field, max_iters,
104
+ use_solvation, solvation_model, solvent,
105
+ method_type, method_name, functional, basis_set_textbox, n_states, maxdim, root, spin_spin_coupling, charge, multiplicity,
106
+ n_cores, memory, input_file_name):
107
+ """Generate an ORCA ``.inp`` file for the selected structure and settings.
108
+
109
+ Loads the structure (optionally pre-optimizing it with MMFF/UFF molecular
110
+ mechanics), then dispatches to the matching ``write_*_orca_input`` helper based on
111
+ ``calculation_type``. Returns ``(status_html, file_list)``; errors are reported in
112
+ the status span rather than raised.
113
+ """
114
+ if input_structure_file_dropdown is None or input_structure_file_dropdown == "":
115
+ gr.Warning("Please select an input structure")
116
+ return "", get_files_in_working_directory(working_directory_path)
117
+
118
+ try:
119
+ # Get the molecule object
120
+ file_path = os.path.join(working_directory_path, input_structure_file_dropdown)
121
+ if input_structure_file_dropdown.endswith('.pdb'):
122
+ mol = Chem.MolFromPDBFile(file_path, sanitize=False, removeHs=False)
123
+ elif input_structure_file_dropdown.endswith('.mol'):
124
+ mol = Chem.MolFromMolFile(file_path, sanitize=False, removeHs=False)
125
+ elif input_structure_file_dropdown.endswith('.mol2'):
126
+ mol = Chem.MolFromMol2File(file_path, sanitize=False, removeHs=False)
127
+ elif input_structure_file_dropdown.endswith('.xyz'):
128
+ mol = add_bonds(mol_from_xyz_file(file_path))
129
+ else: # file_name.endswith('.log')
130
+ mol = add_bonds(mol_from_orca_file(file_path))
131
+
132
+ Chem.SanitizeMol(mol)
133
+ if mol.GetNumConformers()==0:
134
+ AllChem.EmbedMolecule(mol)
135
+
136
+ # Optimize geometry with molecular mechanics
137
+ if use_mm:
138
+ if force_field=="MMFF":
139
+ AllChem.MMFFOptimizeMolecule(mol, maxIters=max_iters)
140
+ else: # force_field=="UFF"
141
+ AllChem.UFFOptimizeMolecule(mol, maxIters=max_iters)
142
+
143
+ # Generate input file
144
+ input_file_path = os.path.join(working_directory_path, input_file_name)
145
+ if calculation_type == "Single-Point":
146
+ write_sp_orca_input(mol, input_file_path, method_type, method_name, functional=functional, basis=basis_set_textbox,
147
+ charge=charge, multiplicity=multiplicity,
148
+ solvation=use_solvation, solvation_model=solvation_model, solvent=solvent, n_proc=n_cores, memory=memory)
149
+ elif calculation_type == "Geometry Optimization":
150
+ write_opt_orca_input(mol, input_file_path, method_type, method_name, functional=functional, basis=basis_set_textbox,
151
+ charge=charge, multiplicity=multiplicity,
152
+ solvation=use_solvation, solvation_model=solvation_model, solvent=solvent, n_proc=n_cores, memory=memory)
153
+ elif calculation_type == "Frequency":
154
+ write_opt_freq_orca_input(mol, input_file_path, method_type, method_name, functional=functional, basis=basis_set_textbox,
155
+ charge=charge, multiplicity=multiplicity,
156
+ solvation=use_solvation, solvation_model=solvation_model, solvent=solvent, n_proc=n_cores, memory=memory)
157
+ elif calculation_type == "Time-Dependent Density Functional Theory":
158
+ write_tddft_orca_input(mol, input_file_path, method_type, method_name, functional=functional, basis=basis_set_textbox,
159
+ n_states=n_states, maxdim=maxdim, root=root, charge=charge, multiplicity=multiplicity,
160
+ solvation=use_solvation, solvation_model=solvation_model, solvent=solvent, n_proc=n_cores, memory=memory)
161
+ else: # calculation_type == "NMR Spectrum"
162
+ write_nmr_orca_input(mol, input_file_path, method_type, functional=functional, basis=basis_set_textbox,
163
+ spin_spin_coupling=spin_spin_coupling, charge=charge, multiplicity=multiplicity,
164
+ solvation=use_solvation, solvation_model=solvation_model, solvent=solvent, n_proc=n_cores, memory=memory)
165
+
166
+ status = "Input file generated."
167
+ return f"<span style='color:green;'>{status}</span>", get_files_in_working_directory(working_directory_path)
168
+ except Exception as exc:
169
+ status = exc
170
+ return f"<span style='color:red;'>{status}</span>", get_files_in_working_directory(working_directory_path)
171
+
172
+ def on_run_calculation(working_directory_path, input_file_name):
173
+ """Run the external ``orca`` binary on ``input_file_name`` (a ``.inp`` in the wd).
174
+
175
+ stdout is captured to a same-named ``.log``. Returns ``(status_html, file_list)``
176
+ with the elapsed time on success; a non-zero ORCA exit (or a missing binary) is
177
+ reported in the status span rather than raised.
178
+ """
179
+ if input_file_name is None or input_file_name=="":
180
+ gr.Warning("Please choose an input file.")
181
+ return "", get_files_in_working_directory(working_directory_path)
182
+
183
+ try:
184
+ input_file_path = os.path.join(working_directory_path, input_file_name)
185
+ output_file_path = os.path.join(working_directory_path, os.path.splitext(input_file_name)[0] + ".log")
186
+
187
+ # Run calculation with ORCA
188
+ cmd = ["orca", input_file_path]
189
+ start = time.time()
190
+ print(f"Running command: {' '.join(cmd)} > {output_file_path}")
191
+ with open(output_file_path, 'w') as outfile:
192
+ subprocess.run(cmd, stdout=outfile, check=True)
193
+ end = time.time()
194
+ duration = end - start
195
+
196
+ status = f"Calculation finished ({round(duration, 3)} s)."
197
+ return f"<span style='color:green;'>{status}</span>", get_files_in_working_directory(working_directory_path)
198
+ except Exception as exc:
199
+ status = f"Error running calculation: {exc}"
200
+ return f"<span style='color:red;'>{status}</span>", get_files_in_working_directory(working_directory_path)
201
+
202
+ def calculation_tab_content(working_directory_path_state, working_directory_file_list_state, status_markdown):
203
+ """Build the "Calculation" tab (settings, input generation, run) and wire events.
204
+
205
+ Takes the shared path/file-list states and status line; returns the tab component.
206
+ """
207
+ with gr.Tab("Calculation") as calculation_tab:
208
+ with gr.Accordion("Settings", open=False):
209
+ with gr.Row():
210
+ n_cores_slider = gr.Slider(label="Number of cores", value=1, minimum=1, maximum=1, step=1, interactive=False)
211
+ memory_slider = gr.Slider(label="Memory (GB)", value=max_memory, minimum=1, maximum=max_memory, step=1)
212
+ with gr.Accordion("Generate Input File"):
213
+ with gr.Row():
214
+ with gr.Column(scale=1):
215
+ input_structure_file_dropdown = gr.Dropdown(label="Input structure", choices=[""], value="", interactive=False)
216
+ with gr.Column(scale=4):
217
+ calculation_type_radio = gr.Radio(label="Type of calculation", value="Single-Point", choices=["Single-Point", "Geometry Optimization", "Frequency", "Time-Dependent Density Functional Theory", "NMR Spectrum"])
218
+ with gr.Row():
219
+ with gr.Column(scale=1):
220
+ with gr.Row():
221
+ mm_checkbox = gr.Checkbox(label="Optimize geometry with molecular mechanics", value=False)
222
+ with gr.Row():
223
+ with gr.Column(scale=1):
224
+ force_field_dropdown = gr.Dropdown(label="Force field", value="MMFF", choices=["MMFF", "UFF"], visible=False)
225
+ with gr.Column(scale=1):
226
+ max_iters_slider = gr.Slider(label="Max iterations", value=200, minimum=0, maximum=1000, step=1, visible=False)
227
+ with gr.Row():
228
+ solvation_checkbox = gr.Checkbox(label="Solvation", value=False)
229
+ with gr.Row():
230
+ with gr.Column(scale=1):
231
+ solvation_dropdown = gr.Dropdown(label="Solvation model", value="cpcm", choices=[("SMD", "smd"), ("CPCM", "cpcm")], visible=False)
232
+ with gr.Column(scale=1):
233
+ solvent_dropdown = gr.Dropdown(label="Solvent", value="water", choices=["water", ("DMSO", "dmso"), "nitromethane", "acetonitrile", "methanol", "ethanol", "acetone", "dichloromethane",
234
+ "dichloroethane", ("THF", "thf"), "aniline", "chlorobenzene", "chloroform", ("diethyl ether", "diethylether"),
235
+ "toluene", "benzene", ("CCl4", "ccl4"), "cyclohexane", "heptane"], allow_custom_value=True, visible=False)
236
+ with gr.Column(scale=1):
237
+ with gr.Row():
238
+ method_type_dropdown = gr.Dropdown(label="Type of method", value="DFT", choices=["HF", "DFT", "Semi-empirical", "MP2", "CCSD", "CCSD(T)"])
239
+ method_name_dropdown = gr.Dropdown(label="Method", choices=[], visible=False)
240
+ with gr.Row():
241
+ functional_dropdown = gr.Dropdown(label="Functional", value="B3LYP", choices=["BLYP", "B3LYP", "CAM-B3LYP", "B3PW91", "PBE", "PBE0", "revPBE", "B97-3C", "M06L", "M062X", "wB97X", "wB97X-D3"], allow_custom_value=True)
242
+ basis_set_dropdown = gr.Dropdown(label="Basis set", value="3-21G", choices=["STO-3G", "3-21G", "6-31G", "6-31G(d,p)", "6-31+G(d,p)", "6-31++G(d,p)",
243
+ "6-311G", "6-311G(d,p)", "cc-pVDZ", "cc-pVTZ", "cc-pVQZ", "aug-cc-pVDZ", "aug-cc-pVTZ", "aug-cc-pVQZ",
244
+ "LanL2DZ", "SDD"], allow_custom_value=True)
245
+ with gr.Row():
246
+ n_states_slider = gr.Slider(label="Number of excited states", value=10, minimum=5, maximum=100, step=1, visible=False)
247
+ maxdim_slider = gr.Slider(label="Dimension of expansion space", value=5, minimum=1, maximum=10, step=1, visible=False)
248
+ root_slider = gr.Slider(label="Root state", value=0, minimum=0, maximum=99, step=1, visible=False)
249
+ spin_spin_coupling_checkbox = gr.Checkbox(label="Compute spin-spin couplings", value=False, visible=False)
250
+ with gr.Row():
251
+ charge_slider = gr.Slider(label="Charge", value=0, minimum=-2, maximum=2, step=1)
252
+ multiplicity_dropdown = gr.Dropdown(label="Multiplicity", value=1, choices=[("Singlet", 1), ("Doublet", 2),
253
+ ("Triplet", 3), ("Quartet", 4),
254
+ ("Quintet", 5), ("Sextet ", 6)])
255
+ with gr.Column(scale=1):
256
+ input_file_name_textbox = gr.Textbox(label="File name", value="single_point")
257
+ generate_input_file_button = gr.Button(value="Generate input file")
258
+ with gr.Accordion("Run calculation"):
259
+ with gr.Row():
260
+ with gr.Column(scale=1):
261
+ input_file_name_dropdown = gr.Dropdown(label="Input file", choices=[""], value="", interactive=False)
262
+ with gr.Column(scale=2):
263
+ run_button = gr.Button("Run")
264
+
265
+ working_directory_file_list_state.change(on_working_directory_file_list_change, [working_directory_file_list_state, input_file_name_textbox], [input_structure_file_dropdown, input_file_name_dropdown])
266
+ calculation_type_radio.change(on_change_calculation_type, calculation_type_radio, [input_file_name_textbox, method_type_dropdown, functional_dropdown, n_states_slider, maxdim_slider, root_slider, spin_spin_coupling_checkbox])
267
+ mm_checkbox.change(on_mm_checkbox_change, mm_checkbox, [force_field_dropdown, max_iters_slider])
268
+ solvation_checkbox.change(on_solvation_checkbox_change, solvation_checkbox, [solvation_dropdown, solvent_dropdown])
269
+ method_type_dropdown.change(on_method_type_change, method_type_dropdown, [method_name_dropdown, functional_dropdown, basis_set_dropdown])
270
+ generate_input_file_button.click(on_generate_input_file, [working_directory_path_state, input_structure_file_dropdown, calculation_type_radio,
271
+ mm_checkbox, force_field_dropdown, max_iters_slider, solvation_checkbox, solvation_dropdown, solvent_dropdown,
272
+ method_type_dropdown, method_name_dropdown, functional_dropdown, basis_set_dropdown, n_states_slider, maxdim_slider, root_slider, spin_spin_coupling_checkbox, charge_slider, multiplicity_dropdown,
273
+ n_cores_slider, memory_slider, input_file_name_textbox],
274
+ [status_markdown, working_directory_file_list_state])
275
+ run_button.click(on_run_calculation, [working_directory_path_state, input_file_name_dropdown], [status_markdown, working_directory_file_list_state])
276
+
277
+ return calculation_tab
@@ -0,0 +1,96 @@
1
+ """Conformer-generation tab: draw/enter a molecule and embed 3D conformers.
2
+
3
+ A SMILES string (typed, or produced by the 2D editor) is embedded into ``num_confs``
4
+ RDKit conformers, each written to the working directory as an XYZ/PDB/MOL structure
5
+ file for later ORCA calculations.
6
+ """
7
+ import os
8
+ from rdkit import Chem
9
+ from rdkit.Chem import AllChem
10
+ import gradio as gr
11
+ from gradio_molecule2d import molecule2d
12
+ from .utils import get_files_in_working_directory, conformer_to_xyz_file
13
+
14
+ def on_draw_molecule(molecule_editor: str) -> str:
15
+ """Canonicalize the SMILES coming from the 2D editor (empty string if invalid)."""
16
+ mol = Chem.MolFromSmiles(molecule_editor)
17
+ if mol is None:
18
+ return ""
19
+
20
+ return Chem.MolToSmiles(mol, canonical=True)
21
+
22
+ def on_generate_conformers(working_directory_path: str, input_smiles: str, charge: int,
23
+ multiplicity: int, num_confs: int, file_name: str,
24
+ file_type: str, progress=gr.Progress()):
25
+ """Embed conformers of ``input_smiles`` and write one structure file each.
26
+
27
+ Returns ``(status_html, file_list)`` where ``status_html`` is a colored status
28
+ span and ``file_list`` is the refreshed working-directory listing (fed back into
29
+ the shared file-list state). Errors (invalid SMILES, no embeddable conformer, or
30
+ any RDKit failure) are reported in the status span rather than raised.
31
+ """
32
+ try:
33
+ # Generate conformers
34
+ mol = Chem.MolFromSmiles(input_smiles)
35
+ if mol is None:
36
+ status = 'Error generating conformers: invalid SMILES.'
37
+ return f"<span style='color:red;'>{status}</span>", get_files_in_working_directory(working_directory_path)
38
+ mol = Chem.AddHs(mol)
39
+ # EmbedMultipleConfs may return FEWER conformers than requested (or none for
40
+ # a molecule it cannot embed), so iterate the ids it actually created.
41
+ conf_ids = list(AllChem.EmbedMultipleConfs(mol, numConfs=num_confs))
42
+ if len(conf_ids) == 0:
43
+ status = 'Error generating conformers: RDKit could not embed any conformer for this molecule.'
44
+ return f"<span style='color:red;'>{status}</span>", get_files_in_working_directory(working_directory_path)
45
+
46
+ for i, conf_id in enumerate(progress.tqdm(conf_ids, total=len(conf_ids), desc="Generating")):
47
+ # Create a unique file name for each conformer
48
+ conf_file_path = os.path.join(working_directory_path, f'{file_name}_{i + 1}')
49
+ # Write conformers geometry to file
50
+ if file_type == 'xyz':
51
+ conf_file_path += '.xyz'
52
+ conformer_to_xyz_file(mol, conf_id, conf_file_path, charge, multiplicity)
53
+ elif file_type == 'pdb':
54
+ conf_file_path += '.pdb'
55
+ Chem.MolToPDBFile(mol, conf_file_path, confId=conf_id)
56
+ else: # file_type_dropdown == 'mol'
57
+ conf_file_path += '.mol'
58
+ Chem.MolToMolFile(mol, conf_file_path, confId=conf_id)
59
+
60
+ status = 'Conformers generated.'
61
+ return f"<span style='color:green;'>{status}</span>", get_files_in_working_directory(working_directory_path)
62
+ except Exception as exc:
63
+ status = f'Error generating conformers: {exc}'
64
+ return f"<span style='color:red;'>{status}</span>", get_files_in_working_directory(working_directory_path)
65
+
66
+ def show_selected_file(selected_file):
67
+ gr.Warning(selected_file)
68
+ return selected_file
69
+
70
+ def conformer_generation_tab_content(working_directory_path_state, working_directory_file_list_state, status_markdown):
71
+ """Build the "Conformer generation" tab and wire its events.
72
+
73
+ ``working_directory_path_state`` / ``working_directory_file_list_state`` are the
74
+ shared ``gr.State`` objects from the working-directory column; ``status_markdown``
75
+ is the shared status line. Returns the tab component.
76
+ """
77
+ with gr.Tab("Conformer generation") as conformer_generation_tab:
78
+ with gr.Row():
79
+ with gr.Column(scale=2):
80
+ with gr.Accordion("Molecular Structure"):
81
+ molecule_editor = molecule2d(label="Molecule")
82
+ with gr.Column(scale=1):
83
+ with gr.Accordion("Generate Conformers"):
84
+ input_smiles_texbox = gr.Textbox(label="SMILES")
85
+ charge_slider = gr.Slider(label="Charge", value=0, minimum=-2, maximum=2, step=1)
86
+ multiplicity_dropdown = gr.Dropdown(label="Multiplicity", value=1, choices=[("Singlet", 1), ("Doublet", 2), ("Triplet", 3), ("Quartet", 4), ("Quintet", 5), ("Sextet", 6)])
87
+ num_confs_slider = gr.Slider(label="Number of conformers", value=1, minimum=1, maximum=100, step=1)
88
+ file_name_textbox = gr.Textbox(label="File name", value="conformer")
89
+ file_type_dropdown = gr.Dropdown(label="File type", value="xyz", choices=["xyz", "pdb", "mol"])
90
+ generate_button = gr.Button(value="Generate")
91
+ status_markdown = gr.Markdown()
92
+
93
+ molecule_editor.change(on_draw_molecule, molecule_editor, input_smiles_texbox)
94
+ generate_button.click(on_generate_conformers, [working_directory_path_state, input_smiles_texbox, charge_slider, multiplicity_dropdown, num_confs_slider, file_name_textbox, file_type_dropdown], [status_markdown, working_directory_file_list_state])
95
+
96
+ return conformer_generation_tab