GeminiSim / app.py
SKekana's picture
Read to push to Agents-MCP-Hackathon org
75fa629
import gradio as gr
import os
from dotenv import load_dotenv
import google.generativeai as genai
import ase
from ase.build import molecule # Often used, good to have for exec scope if needed
from ase.calculators.emt import EMT # Common default
from ase.optimize import BFGS # Common optimizer
from ase.visualize.plot import plot_atoms
import matplotlib
matplotlib.use('Agg') # Use Agg backend for non-interactive plotting
import matplotlib.pyplot as plt
from PIL import Image
import io
import base64
import traceback
import contextlib
# Load environment variables from .env file (for GEMINI_API_KEY)
load_dotenv()
# --- Gemini API Configuration ---
API_KEY = os.getenv("GEMINI_API_KEY")
# MODEL = 'gemini-2.0-flash'
MODEL = 'gemini-2.5-flash-preview-04-17'
# MODEL = 'gemini-2.5-pro-exp-03-25'
if not API_KEY:
print("WARNING: GEMINI_API_KEY environment variable not set. App will not function.")
else:
try:
genai.configure(api_key=API_KEY)
MODEL = genai.GenerativeModel(MODEL)
print("Gemini model initialized successfully.")
except Exception as e:
print(f"ERROR: Could not initialize Gemini model: {e}")
MODEL = None
# --- Prompts for Gemini ---
PROMPT_FOR_ASE_CODE_GENERATION = """
You are an expert in molecular simulations using the Atomic Simulation Environment (ASE) and Python.
A user wants to perform the following task: "{user_query}"
Generate a complete and runnable Python script using ASE to perform this task.
The script MUST:
1. Include all necessary import statements (e.g., `import ase`, `from ase.build import molecule`, `from ase.calculators.emt import EMT`, `from ase.optimize import BFGS`, etc.). Do not assume any modules are pre-imported.
2. Define the atomic structure.
3. Set up an appropriate ASE calculator.
4. Perform the simulation as requested (e.g., geometry optimization, molecular dynamics, single point energy calculation). If non is specified perform an appropriate simulation.
5. Ensure the final ASE `Atoms` object, representing the result of the simulation (e.g., optimized structure), is assigned to a variable named `atoms_object`. This is crucial for visualization.
6. Optionally, print any relevant information or results from the simulation to standard output (e.g., final energy, bond lengths, status messages).
Provide ONLY the Python code block. Do not include any explanations, comments, or markdown formatting before or after the code block itself.
The script will be executed in an environment where ASE and its dependencies are installed."""
PROMPT_FOR_ANALYSIS = """
You are an AI assistant specialized in interpreting results from molecular simulations performed with the Atomic Simulation Environment (ASE).
The user's original simulation request was:
"{user_query}"
The following Python ASE code was generated and executed to address this request:
```python
{generated_code}
```
The execution of this code produced the following standard output (stdout/stderr):
```
{simulation_output}
```
{structure_info}
Based on all the above information, please provide a concise analysis:
1. Briefly explain what the simulation aimed to do and what steps were performed by the code.
2. Summarize the key results obtained from the simulation output (e.g., final energy, convergence status, important structural parameters if mentioned in output).
3. If a molecular structure was visualized, comment on it in relation to the simulation (e.g., "The visualized structure shows the molecule after optimization").
4. Conclude whether the simulation likely achieved the user's original request.
5. If there were any errors or issues, please mention them.
Present the analysis in a clear, easy-to-understand manner. Avoid overly technical jargon where possible.
Provide ONLY the Markdown code block. Do not include any explanations, comments before or after the code block itself.
"""
# --- Helper Functions ---
def plot_atoms_to_pil_image(atoms_obj):
"""Converts an ASE Atoms object to a PIL Image for display."""
if not isinstance(atoms_obj, ase.Atoms):
return None
try:
fig, ax = plt.subplots()
# Use ase.visualize.plot_atoms for plotting
plot_atoms(atoms_obj, ax, radii=0.3, rotation=('0x,0y,0z'))
ax.set_xlabel("X (Å)")
ax.set_ylabel("Y (Å)")
ax.axis('equal')
buf = io.BytesIO()
fig.savefig(buf, format='png', bbox_inches='tight')
plt.close(fig) # Close the figure to free up memory
buf.seek(0)
return Image.open(buf)
except Exception as e:
print(f"Error plotting atoms: {traceback.format_exc()}")
return None
# --- Core Simulation Logic ---
def simulate_and_analyze(user_query_str):
if not MODEL:
error_msg = "Gemini model not initialized. Please check API key and configuration."
return "# Error: Model not initialized.", error_msg, None, error_msg
generated_code_str = ""
simulation_stdout_str = ""
pil_image = None
analysis_text_str = ""
try:
# 1. Generate ASE code from user query
prompt_for_code = PROMPT_FOR_ASE_CODE_GENERATION.format(user_query=user_query_str)
response_code = MODEL.generate_content(prompt_for_code)
generated_code_str = response_code.text.strip()
# Remove potential markdown backticks if Gemini adds them
if generated_code_str.startswith("```python"):
generated_code_str = generated_code_str[len("```python"):].strip()
if generated_code_str.endswith("```"):
generated_code_str = generated_code_str[:-len("```")].strip()
except Exception as e:
error_msg = f"Error generating ASE code with Gemini: {traceback.format_exc()}"
return f"# Error: Could not generate ASE code.\n{error_msg}", error_msg, None, error_msg
# 2. Execute generated ASE code
atoms_object_from_exec = None
execution_namespace = {}
captured_output = io.StringIO()
exec_error_msg = None
try:
with contextlib.redirect_stdout(captured_output):
# Provide a minimal, controlled global scope for exec
# ASE and its modules should be imported by the generated code itself
exec(generated_code_str, {'__builtins__': __builtins__, 'ase': ase}, execution_namespace)
simulation_stdout_str = captured_output.getvalue()
atoms_object_from_exec = execution_namespace.get('atoms_object')
if atoms_object_from_exec is not None and not isinstance(atoms_object_from_exec, ase.Atoms):
warning_msg = f"Warning: 'atoms_object' was found but is not a valid ASE Atoms object (type: {type(atoms_object_from_exec)}). Visualization might fail."
simulation_stdout_str += "\n" + warning_msg
atoms_object_from_exec = None # Invalidate if not correct type
elif 'atoms_object' not in execution_namespace:
simulation_stdout_str += "\nWarning: 'atoms_object' variable not found after code execution. Visualization will not be available."
except Exception as e:
exec_error_msg = f"Error during ASE code execution:\n{traceback.format_exc()}"
simulation_stdout_str = captured_output.getvalue() # Get any output before error
if simulation_stdout_str:
simulation_stdout_str += f"\n\n{exec_error_msg}"
else:
simulation_stdout_str = exec_error_msg
atoms_object_from_exec = None # Ensure it's None on error
# 3. Plot atoms object if available
if atoms_object_from_exec and isinstance(atoms_object_from_exec, ase.Atoms):
pil_image = plot_atoms_to_pil_image(atoms_object_from_exec)
structure_info_for_analysis = "A molecular structure was generated and visualized."
else:
structure_info_for_analysis = "No molecular structure was available or successfully generated for visualization."
if exec_error_msg and not atoms_object_from_exec : # If exec failed before atoms_object could be assigned
structure_info_for_analysis += f" This might be due to the execution error: {exec_error_msg.splitlines()[0]}"
# 4. Generate analysis using Gemini
try:
prompt_for_analysis = PROMPT_FOR_ANALYSIS.format(
user_query=user_query_str,
generated_code=generated_code_str,
simulation_output=simulation_stdout_str,
structure_info=structure_info_for_analysis
)
response_analysis = MODEL.generate_content(prompt_for_analysis)
analysis_text_str = response_analysis.text
analysis_text_str = analysis_text_str.strip()
if analysis_text_str.startswith("```markdown"):
analysis_text_str = analysis_text_str[len("```markdown"):].strip()
if analysis_text_str.endswith("```"):
analysis_text_str = analysis_text_str[:-len("```")].strip()
except Exception as e:
analysis_text_str = f"Error generating analysis with Gemini: {traceback.format_exc()}"
if exec_error_msg: # If code exec failed, make analysis reflect that primarily
analysis_text_str = f"Could not perform analysis as the ASE code execution failed.\nDetails: {exec_error_msg}\n\nGemini analysis generation also failed: {e}"
elif not generated_code_str:
analysis_text_str = f"Could not perform analysis as ASE code generation failed.\nGemini analysis generation also failed: {e}"
return generated_code_str, simulation_stdout_str, pil_image, analysis_text_str
# --- Gradio Interface ---
with gr.Blocks(theme=gr.themes.Soft(primary_hue="teal", secondary_hue="orange")) as demo:
gr.Markdown("# ⚛️ GeminiSim: ASE Simulations via Natural Language 💬")
gr.Markdown(
"Describe your atomic/molecular simulation task in plain English. "
"Gemini AI will generate ASE Python code, run the simulation, and provide an analysis of the results.\n\n"
"**⚠️ SECURITY WARNING:** This tool executes AI-generated Python code using `exec()`. "
"This is inherently unsafe if the AI generates malicious code. Use with caution and only with trusted inputs, "
"especially in non-sandboxed environments."
)
with gr.Row():
with gr.Column(scale=2):
user_query_input = gr.Textbox(
label="Your Simulation Request:",
placeholder="e.g., 'Optimize a water molecule (H2O) using EMT and show the final geometry and energy.'",
lines=10,
elem_id="user_query_textbox"
)
with gr.Column(scale=1):
gr.Markdown("### ✨ Example Prompts:")
with gr.Tabs():
with gr.TabItem("Simple Molecules"):
gr.Examples(
examples=[
"Optimize a water molecule (H2O) using EMT and show the final geometry and energy.",
"Optimize a CO molecule using BFGS and show its bond length.",
"Build a methane molecule (CH4) and calculate its energy with EMT.",
"Simulate a single gold (Au) atom.",
"Calculate the binding energy of an O2 molecule."
],
inputs=[user_query_input],
)
with gr.TabItem("Complex Simulations"):
gr.Examples(
examples=[
"Simulate a basic alcohol molecule (e.g., methanol) and report key bond lengths.",
"Relax an ammonia molecule (NH3) and display its bond angles.",
"Optimize a CO2 molecule and display its linearity.",
"Simulate a water dimer and analyze the hydrogen bond length.",
"Create a small gold cluster (Au3) and calculate its binding energy."
],
inputs=[user_query_input],
)
submit_button = gr.Button("🚀 Simulate & Analyze", variant="primary")
with gr.Accordion("🔬 Results", open=True):
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### 🤖 Generated ASE Code")
generated_code_display = gr.Code(language="python", label="Generated ASE Python Code", lines=15, interactive=False)
gr.Markdown("### 🖥️ Simulation Standard Output")
simulation_stdout_display = gr.Textbox(label="Simulation stdout/stderr", lines=8, interactive=False, show_copy_button=True)
with gr.Column(scale=1):
gr.Markdown("### 🖼️ Molecular Visualization")
molecule_plot_display = gr.Image(label="Molecular Structure", type="pil", interactive=False, height=350, show_download_button=True)
gr.Markdown("### 🧠 AI Analysis")
ai_analysis_display = gr.Markdown(label="Gemini's Analysis ", show_copy_button=True)
gr.Markdown("---")
gr.Markdown("Built with [Gradio](https://gradio.app/), [ASE](https://wiki.fysik.dtu.dk/ase/), and [Google Gemini](https://ai.google.dev/).")
submit_button.click(
fn=simulate_and_analyze,
inputs=[user_query_input],
outputs=[generated_code_display, simulation_stdout_display, molecule_plot_display, ai_analysis_display]
)
if __name__ == "__main__":
if not API_KEY or not MODEL:
print("ERROR: Gemini API key not set or model failed to initialize. Gradio server will not start.")
# Optionally, could display an error in Gradio itself if it were to launch.
else:
print("Launching Gradio server...")
demo.launch() # Add share=True if you want a public link (be mindful of security warning!)