annabossler commited on
Commit
2fe706b
·
verified ·
1 Parent(s): d6059b9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +214 -105
app.py CHANGED
@@ -1,24 +1,60 @@
1
  import os
2
- import tempfile
3
  import numpy as np
4
  import gradio as gr
5
  from ase.io import read, write
6
  from ase.io.trajectory import Trajectory
7
- import hashlib
8
 
9
- # ==== Forzar visor HTML con 3Dmol.js (sin gradio_molecule3d) ====
10
- HAVE_MOL3D = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- # ==== Fallback HTML con 3Dmol.js ====
13
  def traj_to_html(traj_path, width=520, height=520, interval_ms=200):
14
- """
15
- Lee una .traj de ASE y genera un visor HTML (3Dmol.js) con animación.
16
- """
17
  if not traj_path or not os.path.exists(traj_path):
18
  return "<div style='color:#b00; padding:20px;'>No trajectory file found</div>"
19
-
20
- viewer_id = f"viewer_{abs(hash(traj_path)) % 100000}"
21
-
22
  try:
23
  traj = Trajectory(traj_path)
24
  if len(traj) == 0:
@@ -26,57 +62,43 @@ def traj_to_html(traj_path, width=520, height=520, interval_ms=200):
26
  except Exception as e:
27
  return f"<div style='color:#b00; padding:20px;'>Error: {e}</div>"
28
 
29
- xyz_frames = []
30
- for atoms in traj:
31
- symbols = atoms.get_chemical_symbols()
32
- coords = atoms.get_positions()
33
- parts = [str(len(symbols)), "frame"]
34
- for s, (x, y, z) in zip(symbols, coords):
35
- parts.append(f"{s} {x:.6f} {y:.6f} {z:.6f}")
36
- xyz_frames.append("\n".join(parts))
37
-
38
- frames_json = str(xyz_frames).replace("'", '"')
39
-
40
- html = f"""
41
  <div style="margin-bottom:10px; padding:10px; background:#f5f5f5; border-radius:5px;">
42
- <strong>🧬 3D Molecular Viewer</strong> — {len(xyz_frames)} frames
43
  </div>
44
  <div id="{viewer_id}" style="width:{width}px; height:{height}px; position:relative; border:2px solid #ddd; border-radius:8px; background:#fafafa;"></div>
45
  <script>
46
- if (typeof window.$3Dmol === 'undefined') {{
47
- var script = document.createElement('script');
48
- script.src = 'https://3dmol.org/build/3Dmol-min.js';
49
- script.onload = function() {{ setTimeout(function() {{ initViewer_{viewer_id}(); }}, 100); }};
50
- document.head.appendChild(script);
51
- }} else {{
52
- initViewer_{viewer_id}();
53
- }}
54
- function initViewer_{viewer_id}() {{
55
- var el = document.getElementById("{viewer_id}");
56
- if (!el || typeof $3Dmol === "undefined") return;
57
- var viewer = $3Dmol.createViewer(el, {{backgroundColor: 'white'}});
58
- var frames = {frames_json};
59
- var currentFrame = 0;
60
- function showFrame(i) {{
61
- viewer.clear();
62
- viewer.addModel(frames[i], "xyz");
63
- viewer.setStyle({{}}, {{stick: {{}}, sphere: {{}}}});
64
- viewer.zoomTo();
65
- viewer.render();
66
  }}
67
- showFrame(0);
68
- if (frames.length > 1) {{
69
- setInterval(function() {{
70
- currentFrame = (currentFrame + 1) % frames.length;
71
- showFrame(currentFrame);
72
- }}, {interval_ms});
73
  }}
74
- }}
75
  </script>
76
  """
77
- return html
78
 
79
- # ==== OrbMol SPE ====
 
 
80
  from orb_models.forcefield import pretrained
81
  from orb_models.forcefield.calculator import ORBCalculator
82
 
@@ -84,25 +106,18 @@ _MODEL_CALC = None
84
  def _load_orbmol_calc():
85
  global _MODEL_CALC
86
  if _MODEL_CALC is None:
87
- orbff = pretrained.orb_v3_conservative_inf_omat(
88
- device="cpu", precision="float32-high"
89
- )
90
  _MODEL_CALC = ORBCalculator(orbff, device="cpu")
91
  return _MODEL_CALC
92
 
93
  def predict_molecule(structure_file, charge=0, spin_multiplicity=1):
94
- """
95
- Single Point Energy + fuerzas (OrbMol). Maneja archivos de Gradio correctamente.
96
- """
97
  try:
98
  calc = _load_orbmol_calc()
99
  if not structure_file:
100
  return "Error: Please upload a structure file", "Error"
 
101
 
102
- # En Gradio (con type='filepath'), structure_file es directamente un path (str)
103
- file_path = structure_file
104
-
105
- # Verificar que el archivo existe y no está vacío
106
  if not os.path.exists(file_path):
107
  return f"Error: File not found: {file_path}", "Error"
108
  if os.path.getsize(file_path) == 0:
@@ -116,34 +131,70 @@ def predict_molecule(structure_file, charge=0, spin_multiplicity=1):
116
  forces = atoms.get_forces() # eV/Å
117
 
118
  lines = [f"Total Energy: {energy:.6f} eV", "", "Atomic Forces:"]
 
119
  for i, fc in enumerate(forces):
120
  lines.append(f"Atom {i+1}: [{fc[0]:.4f}, {fc[1]:.4f}, {fc[2]:.4f}] eV/Å")
121
- max_force = float(np.max(np.linalg.norm(forces, axis=1)))
122
- lines += ["", f"Max Force: {max_force:.4f} eV/Å"]
123
 
124
  return "\n".join(lines), "Calculation completed with OrbMol"
125
  except Exception as e:
126
  return f"Error during calculation: {e}", "Error"
127
 
128
- # ==== Simulaciones (helpers) ====
 
 
129
  from simulation_scripts_orbmol import (
130
  run_md_simulation,
131
  run_relaxation_simulation,
132
  )
133
 
134
- # ==== Wrappers (MD / Relax): usan directamente el path ====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  def md_wrapper(structure_file, charge, spin, steps, tempK, timestep_fs, ensemble):
136
  try:
137
  if not structure_file:
138
- return ("Error: Please upload a structure file", None, "", "", "", None, "")
139
-
140
- file_path = structure_file # ya es un path (str)
141
-
142
- # Validaciones rápidas
143
  if not os.path.exists(file_path):
144
- return ("Error: File not found: " + str(file_path), None, "", "", "", None, "")
145
  if os.path.getsize(file_path) == 0:
146
- return ("Error: Empty file: " + str(file_path), None, "", "", "", None, "")
147
 
148
  traj_path, log_text, script_text, explanation = run_md_simulation(
149
  file_path,
@@ -156,25 +207,22 @@ def md_wrapper(structure_file, charge, spin, steps, tempK, timestep_fs, ensemble
156
  int(spin),
157
  )
158
  status = f"MD completed: {int(steps)} steps at {int(tempK)} K ({ensemble})"
159
-
160
  html_value = traj_to_html(traj_path)
161
- return (status, traj_path, log_text, script_text, explanation, None, html_value)
162
-
 
163
  except Exception as e:
164
- return (f"Error: {e}", None, "", "", "", None, "")
165
 
166
  def relax_wrapper(structure_file, steps, fmax, charge, spin, relax_cell):
167
  try:
168
  if not structure_file:
169
- return ("Error: Please upload a structure file", None, "", "", "", None, "")
170
-
171
- file_path = structure_file # ya es un path (str)
172
-
173
- # Validaciones rápidas
174
  if not os.path.exists(file_path):
175
- return ("Error: File not found: " + str(file_path), None, "", "", "", None, "")
176
  if os.path.getsize(file_path) == 0:
177
- return ("Error: Empty file: " + str(file_path), None, "", "", "", None, "")
178
 
179
  traj_path, log_text, script_text, explanation = run_relaxation_simulation(
180
  file_path,
@@ -185,14 +233,26 @@ def relax_wrapper(structure_file, steps, fmax, charge, spin, relax_cell):
185
  bool(relax_cell),
186
  )
187
  status = f"Relaxation finished (≤ {int(steps)} steps, fmax={float(fmax)} eV/Å)"
188
-
189
  html_value = traj_to_html(traj_path)
190
- return (status, traj_path, log_text, script_text, explanation, None, html_value)
191
-
 
192
  except Exception as e:
193
- return (f"Error: {e}", None, "", "", "", None, "")
194
-
195
- # ==== UI ====
 
 
 
 
 
 
 
 
 
 
 
 
196
  with gr.Blocks(theme=gr.themes.Ocean(), title="OrbMol Demo") as demo:
197
  with gr.Tabs():
198
  # -------- SPE --------
@@ -200,25 +260,25 @@ with gr.Blocks(theme=gr.themes.Ocean(), title="OrbMol Demo") as demo:
200
  with gr.Row():
201
  with gr.Column(scale=2):
202
  gr.Markdown("# OrbMol — Quantum-Accurate Molecular Predictions")
203
- gr.Markdown("Upload molecular structure files (.xyz, .pdb, .cif, .traj) for energy and force calculations.")
204
-
205
- # IMPORTANTE: type='filepath'
206
  xyz_input = gr.File(
207
- label="Upload Structure File (.xyz/.pdb/.cif/.traj/.mol/.sdf)",
208
  file_types=[".xyz", ".pdb", ".cif", ".traj", ".mol", ".sdf"],
209
  file_count="single",
210
- type="filepath"
211
  )
212
-
213
  with gr.Row():
214
  charge_input = gr.Slider(minimum=-10, maximum=10, value=0, step=1, label="Charge")
215
  spin_input = gr.Slider(minimum=1, maximum=11, value=1, step=1, label="Spin Multiplicity")
216
  run_spe = gr.Button("Run OrbMol Prediction", variant="primary")
217
-
218
- with gr.Column(variant="panel", min_width=500):
219
  spe_out = gr.Textbox(label="Energy & Forces", lines=15, interactive=False)
220
  spe_status = gr.Textbox(label="Status", interactive=False, max_lines=1)
221
 
 
 
 
222
  run_spe.click(predict_molecule, [xyz_input, charge_input, spin_input], [spe_out, spe_status])
223
 
224
  # -------- MD --------
@@ -226,15 +286,12 @@ with gr.Blocks(theme=gr.themes.Ocean(), title="OrbMol Demo") as demo:
226
  with gr.Row():
227
  with gr.Column(scale=2):
228
  gr.Markdown("## Molecular Dynamics Simulation")
229
- gr.Markdown("Upload your molecular structure and configure MD parameters.")
230
-
231
  xyz_md = gr.File(
232
- label="Upload Structure File (.xyz/.pdb/.cif/.traj/.mol/.sdf)",
233
  file_types=[".xyz", ".pdb", ".cif", ".traj", ".mol", ".sdf"],
234
  file_count="single",
235
- type="filepath"
236
  )
237
-
238
  with gr.Row():
239
  charge_md = gr.Slider(minimum=-10, maximum=10, value=0, step=1, label="Charge")
240
  spin_md = gr.Slider(minimum=1, maximum=11, value=1, step=1, label="Spin Multiplicity")
@@ -247,4 +304,56 @@ with gr.Blocks(theme=gr.themes.Ocean(), title="OrbMol Demo") as demo:
247
  run_md_btn = gr.Button("Run MD Simulation", variant="primary")
248
 
249
  with gr.Column(variant="panel", min_width=520):
250
- md_status = gr.Text_
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
+ import re
3
  import numpy as np
4
  import gradio as gr
5
  from ase.io import read, write
6
  from ase.io.trajectory import Trajectory
 
7
 
8
+ # =========================
9
+ # 3Dmol.js helpers (preview y trayectorias)
10
+ # =========================
11
+
12
+ def _atoms_to_xyz_block(atoms):
13
+ symbols = atoms.get_chemical_symbols()
14
+ coords = atoms.get_positions()
15
+ parts = [str(len(symbols)), "frame"]
16
+ for s, (x, y, z) in zip(symbols, coords):
17
+ parts.append(f"{s} {x:.6f} {y:.6f} {z:.6f}")
18
+ return "\n".join(parts)
19
+
20
+ def structure_to_html(file_path, width=520, height=520):
21
+ """Renderiza estructura única en 3Dmol.js sin correr cálculos."""
22
+ if not file_path or not os.path.exists(file_path):
23
+ return "<div style='color:#b00; padding:20px;'>No structure file found</div>"
24
+ try:
25
+ atoms = read(file_path)
26
+ except Exception as e:
27
+ return f"<div style='color:#b00; padding:20px;'>Error reading structure: {e}</div>"
28
+
29
+ xyz_block = _atoms_to_xyz_block(atoms).replace("`", "\\`")
30
+ viewer_id = f"viewer_{abs(hash(('single', file_path))) % 100000}"
31
+ return f"""
32
+ <div style="margin-bottom:10px; padding:10px; background:#f5f5f5; border-radius:5px;">
33
+ <strong>🧬 3D Molecular Viewer</strong> — preview (no compute)
34
+ </div>
35
+ <div id="{viewer_id}" style="width:{width}px; height:{height}px; position:relative; border:2px solid #ddd; border-radius:8px; background:#fafafa;"></div>
36
+ <script>
37
+ const _load3Dmol = (ok) => {{
38
+ if (typeof window.$3Dmol !== 'undefined') return ok();
39
+ const s=document.createElement('script');
40
+ s.src='https://3dmol.org/build/3Dmol-min.js';
41
+ s.onload=ok; document.head.appendChild(s);
42
+ }};
43
+ _load3Dmol(() => {{
44
+ const el=document.getElementById("{viewer_id}");
45
+ if(!el||typeof $3Dmol==='undefined') return;
46
+ const v=$3Dmol.createViewer(el, {{backgroundColor:'white'}});
47
+ v.addModel(`{xyz_block}`, "xyz");
48
+ v.setStyle({{}}, {{stick:{{}}, sphere:{{}}}});
49
+ v.zoomTo(); v.render();
50
+ }});
51
+ </script>
52
+ """
53
 
 
54
  def traj_to_html(traj_path, width=520, height=520, interval_ms=200):
55
+ """Animación 3Dmol.js desde .traj de ASE."""
 
 
56
  if not traj_path or not os.path.exists(traj_path):
57
  return "<div style='color:#b00; padding:20px;'>No trajectory file found</div>"
 
 
 
58
  try:
59
  traj = Trajectory(traj_path)
60
  if len(traj) == 0:
 
62
  except Exception as e:
63
  return f"<div style='color:#b00; padding:20px;'>Error: {e}</div>"
64
 
65
+ frames = [_atoms_to_xyz_block(at) for at in traj]
66
+ frames_json = str(frames).replace("'", '"')
67
+ viewer_id = f"viewer_{abs(hash(traj_path)) % 100000}"
68
+ return f"""
 
 
 
 
 
 
 
 
69
  <div style="margin-bottom:10px; padding:10px; background:#f5f5f5; border-radius:5px;">
70
+ <strong>🧬 3D Molecular Viewer</strong> — {len(frames)} frames
71
  </div>
72
  <div id="{viewer_id}" style="width:{width}px; height:{height}px; position:relative; border:2px solid #ddd; border-radius:8px; background:#fafafa;"></div>
73
  <script>
74
+ const _load3Dmol = (ok) => {{
75
+ if (typeof window.$3Dmol !== 'undefined') return ok();
76
+ const s=document.createElement('script');
77
+ s.src='https://3dmol.org/build/3Dmol-min.js';
78
+ s.onload=ok; document.head.appendChild(s);
79
+ }};
80
+ _load3Dmol(() => {{
81
+ const el=document.getElementById("{viewer_id}");
82
+ if(!el||typeof $3Dmol==='undefined') return;
83
+ const v=$3Dmol.createViewer(el, {{backgroundColor:'white'}});
84
+ const frames={frames_json};
85
+ let idx=0;
86
+ function draw(i){{
87
+ v.clear(); v.addModel(frames[i], "xyz");
88
+ v.setStyle({{}}, {{stick:{{}}, sphere:{{}}}});
89
+ v.zoomTo(); v.render();
 
 
 
 
90
  }}
91
+ draw(0);
92
+ if(frames.length>1){{
93
+ setInterval(()=>{{ idx=(idx+1)%frames.length; draw(idx); }}, {interval_ms});
 
 
 
94
  }}
95
+ }});
96
  </script>
97
  """
 
98
 
99
+ # =========================
100
+ # OrbMol (SPE)
101
+ # =========================
102
  from orb_models.forcefield import pretrained
103
  from orb_models.forcefield.calculator import ORBCalculator
104
 
 
106
  def _load_orbmol_calc():
107
  global _MODEL_CALC
108
  if _MODEL_CALC is None:
109
+ orbff = pretrained.orb_v3_conservative_inf_omat(device="cpu", precision="float32-high")
 
 
110
  _MODEL_CALC = ORBCalculator(orbff, device="cpu")
111
  return _MODEL_CALC
112
 
113
  def predict_molecule(structure_file, charge=0, spin_multiplicity=1):
114
+ """Single Point Energy + fuerzas (OrbMol)."""
 
 
115
  try:
116
  calc = _load_orbmol_calc()
117
  if not structure_file:
118
  return "Error: Please upload a structure file", "Error"
119
+ file_path = structure_file # gr.File(type='filepath') -> str
120
 
 
 
 
 
121
  if not os.path.exists(file_path):
122
  return f"Error: File not found: {file_path}", "Error"
123
  if os.path.getsize(file_path) == 0:
 
131
  forces = atoms.get_forces() # eV/Å
132
 
133
  lines = [f"Total Energy: {energy:.6f} eV", "", "Atomic Forces:"]
134
+ norms = np.linalg.norm(forces, axis=1)
135
  for i, fc in enumerate(forces):
136
  lines.append(f"Atom {i+1}: [{fc[0]:.4f}, {fc[1]:.4f}, {fc[2]:.4f}] eV/Å")
137
+ lines += ["", f"Max Force: {float(np.max(norms)):.4f} eV/Å"]
 
138
 
139
  return "\n".join(lines), "Calculation completed with OrbMol"
140
  except Exception as e:
141
  return f"Error during calculation: {e}", "Error"
142
 
143
+ # =========================
144
+ # Simulaciones (helpers) y parsing de log
145
+ # =========================
146
  from simulation_scripts_orbmol import (
147
  run_md_simulation,
148
  run_relaxation_simulation,
149
  )
150
 
151
+ ENERGY_PATTERNS = [
152
+ re.compile(r"(?:^|\\s)(?:step|iter|i)\\s*[:=]?\\s*(\\d+).*?(?:E|energy)\\s*[:=]?\\s*(-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?)"),
153
+ re.compile(r"(?:^|\\s)E(?:nergy)?\\s*[:=]?\\s*(-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?)"),
154
+ ]
155
+
156
+ def extract_energy_series(log_text):
157
+ steps, energies = [], []
158
+ if not log_text:
159
+ return steps, energies
160
+ for line in log_text.splitlines():
161
+ m = ENERGY_PATTERNS[0].search(line)
162
+ if m:
163
+ steps.append(int(m.group(1)))
164
+ energies.append(float(m.group(2)))
165
+ continue
166
+ m2 = ENERGY_PATTERNS[1].search(line)
167
+ if m2:
168
+ steps.append(len(steps))
169
+ energies.append(float(m2.group(1)))
170
+ return steps, energies
171
+
172
+ def plot_energy(steps, energies, title):
173
+ import matplotlib.pyplot as plt
174
+ fig = plt.figure(figsize=(5,3.2))
175
+ if steps:
176
+ plt.plot(steps, energies, linewidth=1.6)
177
+ plt.xlabel("Step")
178
+ else:
179
+ plt.plot(range(len(energies)), energies, linewidth=1.6)
180
+ plt.xlabel("Index")
181
+ plt.ylabel("Energy (eV)")
182
+ plt.title(title)
183
+ plt.tight_layout()
184
+ return fig
185
+
186
+ # =========================
187
+ # Wrappers MD / Relax (devuelven: status, traj, log, script, explain, html, plot)
188
+ # =========================
189
  def md_wrapper(structure_file, charge, spin, steps, tempK, timestep_fs, ensemble):
190
  try:
191
  if not structure_file:
192
+ return ("Error: Please upload a structure file", None, "", "", "", "", None)
193
+ file_path = structure_file
 
 
 
194
  if not os.path.exists(file_path):
195
+ return ("Error: File not found: " + str(file_path), None, "", "", "", "", None)
196
  if os.path.getsize(file_path) == 0:
197
+ return ("Error: Empty file: " + str(file_path), None, "", "", "", "", None)
198
 
199
  traj_path, log_text, script_text, explanation = run_md_simulation(
200
  file_path,
 
207
  int(spin),
208
  )
209
  status = f"MD completed: {int(steps)} steps at {int(tempK)} K ({ensemble})"
 
210
  html_value = traj_to_html(traj_path)
211
+ s, e = extract_energy_series(log_text)
212
+ fig = plot_energy(s, e, "MD: Energy vs Step")
213
+ return (status, traj_path, log_text, script_text, explanation, html_value, fig)
214
  except Exception as e:
215
+ return (f"Error: {e}", None, "", "", "", "", None)
216
 
217
  def relax_wrapper(structure_file, steps, fmax, charge, spin, relax_cell):
218
  try:
219
  if not structure_file:
220
+ return ("Error: Please upload a structure file", None, "", "", "", "", None)
221
+ file_path = structure_file
 
 
 
222
  if not os.path.exists(file_path):
223
+ return ("Error: File not found: " + str(file_path), None, "", "", "", "", None)
224
  if os.path.getsize(file_path) == 0:
225
+ return ("Error: Empty file: " + str(file_path), None, "", "", "", "", None)
226
 
227
  traj_path, log_text, script_text, explanation = run_relaxation_simulation(
228
  file_path,
 
233
  bool(relax_cell),
234
  )
235
  status = f"Relaxation finished (≤ {int(steps)} steps, fmax={float(fmax)} eV/Å)"
 
236
  html_value = traj_to_html(traj_path)
237
+ s, e = extract_energy_series(log_text)
238
+ fig = plot_energy(s, e, "Relaxation: Energy vs Step")
239
+ return (status, traj_path, log_text, script_text, explanation, html_value, fig)
240
  except Exception as e:
241
+ return (f"Error: {e}", None, "", "", "", "", None)
242
+
243
+ def preview_structure(structure_file):
244
+ """Preview inmediato al subir archivo en SPE."""
245
+ if not structure_file:
246
+ return "<div style='color:#b00; padding:20px;'>Upload a file to preview</div>"
247
+ if not os.path.exists(structure_file):
248
+ return "<div style='color:#b00; padding:20px;'>File not found</div>"
249
+ if os.path.getsize(structure_file) == 0:
250
+ return "<div style='color:#b00; padding:20px;'>Empty file</div>"
251
+ return structure_to_html(structure_file)
252
+
253
+ # =========================
254
+ # UI
255
+ # =========================
256
  with gr.Blocks(theme=gr.themes.Ocean(), title="OrbMol Demo") as demo:
257
  with gr.Tabs():
258
  # -------- SPE --------
 
260
  with gr.Row():
261
  with gr.Column(scale=2):
262
  gr.Markdown("# OrbMol — Quantum-Accurate Molecular Predictions")
263
+ gr.Markdown("Upload molecular structure files (.xyz, .pdb, .cif, .traj, .mol, .sdf) for preview and calculation.")
 
 
264
  xyz_input = gr.File(
265
+ label="Upload Structure File",
266
  file_types=[".xyz", ".pdb", ".cif", ".traj", ".mol", ".sdf"],
267
  file_count="single",
268
+ type="filepath",
269
  )
 
270
  with gr.Row():
271
  charge_input = gr.Slider(minimum=-10, maximum=10, value=0, step=1, label="Charge")
272
  spin_input = gr.Slider(minimum=1, maximum=11, value=1, step=1, label="Spin Multiplicity")
273
  run_spe = gr.Button("Run OrbMol Prediction", variant="primary")
274
+ with gr.Column(variant="panel", min_width=520):
275
+ spe_preview = gr.HTML(label="Structure Preview")
276
  spe_out = gr.Textbox(label="Energy & Forces", lines=15, interactive=False)
277
  spe_status = gr.Textbox(label="Status", interactive=False, max_lines=1)
278
 
279
+ # Preview inmediato (sin cálculo)
280
+ xyz_input.change(preview_structure, inputs=[xyz_input], outputs=[spe_preview])
281
+ # Cálculo SPE
282
  run_spe.click(predict_molecule, [xyz_input, charge_input, spin_input], [spe_out, spe_status])
283
 
284
  # -------- MD --------
 
286
  with gr.Row():
287
  with gr.Column(scale=2):
288
  gr.Markdown("## Molecular Dynamics Simulation")
 
 
289
  xyz_md = gr.File(
290
+ label="Upload Structure File",
291
  file_types=[".xyz", ".pdb", ".cif", ".traj", ".mol", ".sdf"],
292
  file_count="single",
293
+ type="filepath",
294
  )
 
295
  with gr.Row():
296
  charge_md = gr.Slider(minimum=-10, maximum=10, value=0, step=1, label="Charge")
297
  spin_md = gr.Slider(minimum=1, maximum=11, value=1, step=1, label="Spin Multiplicity")
 
304
  run_md_btn = gr.Button("Run MD Simulation", variant="primary")
305
 
306
  with gr.Column(variant="panel", min_width=520):
307
+ md_status = gr.Textbox(label="MD Status", interactive=False)
308
+ md_traj = gr.File(label="Trajectory (.traj)", interactive=False)
309
+ md_html = gr.HTML(label="Trajectory Viewer")
310
+ md_log = gr.Textbox(label="Log", interactive=False, lines=15, max_lines=25)
311
+ md_script = gr.Code(label="Reproduction Script", language="python", interactive=False, lines=20, max_lines=30)
312
+ md_explain = gr.Markdown()
313
+ md_plot = gr.Plot(label="Energy vs Step")
314
+
315
+ run_md_btn.click(
316
+ md_wrapper,
317
+ inputs=[xyz_md, charge_md, spin_md, steps_md, temp_md, timestep_md, ensemble_md],
318
+ outputs=[md_status, md_traj, md_log, md_script, md_explain, md_html, md_plot],
319
+ )
320
+
321
+ # -------- Relax --------
322
+ with gr.Tab("Relaxation / Optimization"):
323
+ with gr.Row():
324
+ with gr.Column(scale=2):
325
+ gr.Markdown("## Structure Relaxation/Optimization")
326
+ xyz_rlx = gr.File(
327
+ label="Upload Structure File",
328
+ file_types=[".xyz", ".pdb", ".cif", ".traj", ".mol", ".sdf"],
329
+ file_count="single",
330
+ type="filepath",
331
+ )
332
+ steps_rlx = gr.Slider(minimum=1, maximum=2000, value=300, step=1, label="Max Steps")
333
+ fmax_rlx = gr.Slider(minimum=0.001, maximum=0.5, value=0.05, step=0.001, label="Fmax (eV/Å)")
334
+ with gr.Row():
335
+ charge_rlx = gr.Slider(minimum=-10, maximum=10, value=0, step=1, label="Charge")
336
+ spin_rlx = gr.Slider(minimum=1, maximum=11, value=1, step=1, label="Spin")
337
+ relax_cell = gr.Checkbox(False, label="Relax Unit Cell")
338
+ run_rlx_btn = gr.Button("Run Optimization", variant="primary")
339
+
340
+ with gr.Column(variant="panel", min_width=520):
341
+ rlx_status = gr.Textbox(label="Status", interactive=False)
342
+ rlx_traj = gr.File(label="Trajectory (.traj)", interactive=False)
343
+ rlx_html = gr.HTML(label="Final Structure / Trajectory")
344
+ rlx_log = gr.Textbox(label="Log", interactive=False, lines=15, max_lines=25)
345
+ rlx_script = gr.Code(label="Reproduction Script", language="python", interactive=False, lines=20, max_lines=30)
346
+ rlx_explain = gr.Markdown()
347
+ rlx_plot = gr.Plot(label="Energy vs Step")
348
+
349
+ run_rlx_btn.click(
350
+ relax_wrapper,
351
+ inputs=[xyz_rlx, steps_rlx, fmax_rlx, charge_rlx, spin_rlx, relax_cell],
352
+ outputs=[rlx_status, rlx_traj, rlx_log, rlx_script, rlx_explain, rlx_html, rlx_plot],
353
+ )
354
+
355
+ print("Starting OrbMol model loading…")
356
+ _ = _load_orbmol_calc()
357
+
358
+ if __name__ == "__main__":
359
+ demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)