VirtuDockDL is your comprehensive solution for streamlining the process of drug discovery and molecular analysis. With our platform, you can harness the power of deep learning to perform virtual screening, evaluate molecular activities, and predict binding affinities with unprecedented accuracy and speed.
Welcome to VirtuDockDL – Your Automated Virtual Screening Companion
529 |
VirtuDockDL leverages the power of deep learning to streamline the drug discovery process, making it faster, more accurate, and accessible. Whether you're refining protein structures, prioritizing ligands, or diving deep into molecular docking, VirtuDockDL is here to guide you every step of the way.
530 |
531 |
Getting Started:
532 |
533 |
Upload Your Data: Begin by uploading your protein files and ligand datasets. VirtuDockDL accepts PDB files for proteins and CSV files for ligands. Ensure your ligand files are formatted correctly, with 'SMILES' and 'Activity' columns for virtual screening.
534 |
Ligand Prioritization: Use our Graph Neural Network (GNN) model to efficiently prioritize ligands. This process helps in narrowing down potential candidates by predicting their pharmacological profiles.
535 |
Protein Refinement: Upload your protein structures for refinement. Our platform will optimize your proteins to ensure accurate docking results, improving the prediction of ligand interactions.
536 |
Molecular Docking: With your ligands prioritized and protein refined, proceed to the Molecular Docking tab. Here, VirtuDockDL simulates the interaction between your ligands and protein targets, helping identify the most promising compounds.
537 |
Analysis and Download Results: Once docking is complete, analyze the results directly on VirtuDockDL. You can download the detailed reports and visualizations for further analysis.
538 |
539 |
540 |
Tips for Success:
541 |
Ensure your input files are correctly formatted and contain all necessary information. Utilize the "De Novo Molecule Generation" feature to explore new ligands based on specified criteria, enhancing your drug discovery process. Take advantage of our re-screening feature to iteratively refine your search for the optimal ligand.
542 |
543 |
Technical Support:
544 |
Should you encounter any issues or have questions, please refer to our FAQ section or reach out to our support team. VirtuDockDL is continuously evolving, and your feedback is invaluable to us.
545 |
546 |
Disclaimer:
547 |
VirtuDockDL is designed for research purposes only. Users are responsible for the interpretation of the results, and it is recommended to corroborate the findings with experimental data.
548 |
549 |
Let's Revolutionize Drug Discovery Together
550 |
VirtuDockDL is more than a tool; it's your partner in the quest to discover new and effective therapeutics. Explore the possibilities, push the boundaries of what's achievable, and embark on a journey of innovation and discovery.
551 |
552 |
553 |
554 |
555 |
556 |
557 |
558 |
559 |
560 |
561 |
562 |
563 |
564 |
565 |
752 |
753 |
754 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | import random
2 | from flask import Flask, render_template, request, flash, redirect, url_for, send_from_directory, after_this_request
3 | from flask import send_from_directory, jsonify
4 | import os
5 | from flask import session
6 | import logging
7 | import json
8 | from openmm.app import PDBFile, Modeller, ForceField, Simulation, PME, HBonds
9 | from openmm import LangevinMiddleIntegrator
10 | from openmm.unit import kelvin, picosecond, picoseconds, nanometer
11 | from pathlib import Path
12 | from Bio.PDB import PDBIO
13 | from torch_geometric.data import Data
14 | from flask import Flask, request, jsonify
15 | from rdkit.Chem import PandasTools
16 | import zipfile
17 | import uuid
18 | import subprocess
19 | import shutil
20 | import time
21 | import requests
22 | from werkzeug.utils import secure_filename
23 | from flask import send_from_directory
24 | import torch.nn.functional as F
25 | from torch_geometric.nn import GCNConv, global_mean_pool
26 | from torch_geometric.data import Data
27 | from Bio.PDB import PDBParser
28 | import numpy as np
29 | from werkzeug.utils import secure_filename
30 | import csv
31 | import dgl
32 | import torch
33 | import torch.nn as nn
34 | from rdkit import Chem
35 | from rdkit.Chem import Descriptors, AllChem, MACCSkeys, RDKFingerprint
36 | import pandas as pd
37 | from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
38 | from torch.utils.data import Dataset, DataLoader
39 | from sklearn.model_selection import StratifiedShuffleSplit
40 | import numpy as np
41 | from flask import Flask
42 | from gnn_model import GNN
43 | from flask import Flask, request, render_template, flash, send_file
44 | import matplotlib.pyplot as plt
45 | from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
46 | from torch.utils.data import Dataset, DataLoader
47 | from sklearn.model_selection import StratifiedShuffleSplit
48 | import numpy as np
49 | import matplotlib.pyplot as plt
50 | from sklearn.metrics import roc_auc_score, roc_curve
51 | from torch.utils.data import DataLoader
52 | from gnn_model import GNN # Import GNN class from gnn_model.py
53 | from gnn_utils import mol_to_graph, MoleculeDataset, collate
54 | from sklearn.mixture import GaussianMixture
55 | from sklearn.metrics import silhouette_score, davies_bouldin_score
56 | from gnn_model import GNN
57 | from gnn_utils import collate, mol_to_graph, MoleculeDataset
58 | import torch
59 | from torch_geometric.data import Data
60 | from Bio.PDB import PDBParser, CaPPBuilder
61 | import mdtraj as md
62 | import openmm
63 | from openmm.app import * # This will import the necessary 'app' module classes and functions
64 | from openmm import *
65 | from openmm.unit import *
66 | from openmm.app import PDBFile, Modeller, ForceField
67 | from pdbfixer import PDBFixer
68 | from simtk.openmm.app import PDBFile
69 | import matplotlib.pyplot as plt
70 | from io import BytesIO
71 | from flask import Flask, send_from_directory, url_for, current_app, flash, redirect, render_template
72 | from datetime import datetime
73 |
74 | app = Flask(__name__)
75 | app.config['SECRET_KEY'] = 'your_secret_key'
76 | app.config['UPLOADED_FILES_DIR'] = 'uploaded_files'
77 | app.config['GENERATED_FILES_DIR'] = 'generated_files'
78 | app.config['uploaded_files_dir'] = 'uploaded_files'
79 | app.config['generated_files_dir'] = 'generated_files'
80 | app.config['UPLOAD_FOLDER'] = 'uploads'
81 | app.config['DOCKING_RESULTS_DIR'] = 'docking_results'
82 | app.config['ALLOWED_EXTENSIONS'] = {'csv', 'zip', 'pdb', 'sdf'}
83 |
84 | # Ensure directories exist
85 | for directory in [app.config['GENERATED_FILES_DIR'], app.config['UPLOADED_FILES_DIR'], app.config['generated_files_dir'], app.config['uploaded_files_dir'], app.config['UPLOAD_FOLDER'], app.config['DOCKING_RESULTS_DIR']]:
86 | os.makedirs(directory, exist_ok=True)
87 |
88 | # Directory setup
89 | for directory in [app.config['GENERATED_FILES_DIR'], app.config['UPLOADED_FILES_DIR'], app.config['generated_files_dir'], app.config['uploaded_files_dir'],app.config['UPLOAD_FOLDER']]:
90 | os.makedirs(directory, exist_ok=True)
91 |
92 | def save_data_to_csv(data, filename):
93 | """Save data to CSV format."""
94 | with open(filename, 'w', newline='') as csv_file:
95 | fieldnames = ['SMILES', 'Activity']
96 | writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
97 | writer.writeheader()
98 | for smiles, activity in data:
99 | writer.writerow({'SMILES': smiles, 'Activity': activity})
100 |
101 |
102 | def preprocess_csv(file):
103 | """Preprocess the uploaded CSV file."""
104 | try:
105 | # Read the CSV file into a DataFrame
106 | df = pd.read_csv(file)
107 |
108 | # Check if the CSV file contains the required columns
109 | if 'SMILES' not in df.columns or 'Activity' not in df.columns:
110 | flash('The CSV file must have "SMILES" and "Activity" columns.', 'error')
111 | return None
112 |
113 | # Canonicalize and validate SMILES strings
114 | df['SMILES'] = df['SMILES'].apply(lambda x: Chem.MolToSmiles(Chem.MolFromSmiles(x), canonical=True) if Chem.MolFromSmiles(x) is not None else None)
115 |
116 | # Drop rows with None (invalid SMILES) values
117 | df.dropna(subset=['SMILES'], inplace=True)
118 |
119 | # Save the preprocessed data in CSV format
120 | timestamp = int(time.time())
121 | filename = f'uploaded_data_{timestamp}.csv'
122 | save_data_to_csv(df.values.tolist(), filename)
123 | #flash('CSV file uploaded and saved successfully.', 'success')
124 |
125 | return df
126 | except Exception as e:
127 | flash(f'Error processing the CSV file: {str(e)}', 'error')
128 | return None
129 |
130 |
131 | def train_and_evaluate_model(train_dataloader, test_dataloader, model, optimizer, criterion, scheduler):
132 | """Train and evaluate the GNN model."""
133 | best_val_loss = float('inf')
134 | patience = 10
135 | stop_counter = 0
136 | checkpoint_path = 'best_model.pth'
137 | for epoch in range(50):
138 | model.train()
139 | train_loss = 0
140 | for batched_graph, batched_features, batched_labels in train_dataloader:
141 | if batched_graph is None:
142 | continue
143 | optimizer.zero_grad()
144 | outputs = model(batched_graph, batched_features)
145 | loss = criterion(outputs, batched_labels)
146 | train_loss += loss.item()
147 | loss.backward()
148 | optimizer.step()
149 | train_loss /= len(train_dataloader)
150 | print(f"Epoch {epoch + 1}, Train Loss: {train_loss:.4f}")
151 | model.eval()
152 | val_loss = 0
153 | for batched_graph, batched_features, batched_labels in test_dataloader:
154 | if batched_graph is None:
155 | continue
156 | with torch.no_grad():
157 | outputs = model(batched_graph, batched_features)
158 | loss = criterion(outputs, batched_labels)
159 | val_loss += loss.item()
160 | val_loss /= len(test_dataloader)
161 | scheduler.step(val_loss)
162 | print(f"Epoch {epoch + 1}, Validation Loss: {val_loss:.4f}")
163 | if val_loss < best_val_loss:
164 | best_val_loss = val_loss
165 | stop_counter = 0
166 | torch.save(model.state_dict(), checkpoint_path)
167 | else:
168 | stop_counter += 1
169 | if stop_counter >= patience:
170 | print("Early stopping triggered.")
171 | break
172 | model.load_state_dict(torch.load(checkpoint_path))
173 | return model
174 | def download_clusters():
175 | return send_file(os.path.join(app.config['GENERATED_FILES_DIR'], 'final_clusters.csv'), as_attachment=True,
176 | attachment_filename='final_clusters.csv')
177 |
178 | @app.route('/download', methods=['GET'])
179 | def download():
180 | return send_file(os.path.join(app.config['GENERATED_FILES_DIR'], 'generated_molecules.csv'), as_attachment=True)
181 |
182 | @app.route('/', methods=['GET', 'POST'])
183 | def index():
184 | timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
185 | final_compounds_filename = f'final_compounds_{timestamp}.csv'
186 | final_clusters_filename = f'final_clusters_{timestamp}.csv'
187 | cluster_plot_filename = f'cluster_plot_{timestamp}.png'
188 | model = GNN(1, 64, 2)
189 | criterion = nn.CrossEntropyLoss()
190 | optimizer = torch.optim.RMSprop(model.parameters(), lr=0.001, weight_decay=5e-4)
191 | scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', factor=0.5, patience=5)
192 | virtual_screening = False
193 | uploaded_file_path = None
194 | generated_file_path = None
195 | generated_molecules = None
196 | plot_file_path = None
197 |
198 | if request.method == 'POST':
199 | if 'file' in request.files:
200 | file = request.files['file']
201 | if file.filename != '':
202 | filename = f'Molecules_{timestamp}.csv'
203 | uploaded_file_path = os.path.join(app.config['UPLOADED_FILES_DIR'], filename)
204 | file.save(uploaded_file_path)
205 | #flash('CSV file uploaded and saved successfully.', 'success')
206 |
207 | # Load data and preprocess
208 | data = pd.read_csv(uploaded_file_path)
209 | smiles = data["SMILES"].tolist()
210 | labels = data["Activity"].astype(int).tolist()
211 | full_dataset = MoleculeDataset(smiles, labels)
212 |
213 | splitter = StratifiedShuffleSplit(n_splits=1, train_size=0.8, random_state=42)
214 | train_indices, test_indices = next(splitter.split(smiles, labels))
215 | train_dataset = [full_dataset[i] for i in train_indices]
216 | test_dataset = [full_dataset[i] for i in test_indices]
217 |
218 | train_dataloader = DataLoader(train_dataset, batch_size=32, shuffle=True, collate_fn=collate)
219 | test_dataloader = DataLoader(test_dataset, batch_size=32, shuffle=False, collate_fn=collate)
220 |
221 | #flash('Dataset prepared and split into training and testing sets successfully.', 'success')
222 |
223 | # Train the model
224 | model = train_and_evaluate_model(train_dataloader, test_dataloader, model, optimizer, criterion,
225 | scheduler)
226 |
227 | all_predictions, all_targets = [], []
228 | for batched_graph, batched_features, batched_labels in test_dataloader:
229 | if batched_graph is None:
230 | continue
231 |
232 | with torch.no_grad():
233 | outputs = model(batched_graph, batched_features)
234 | _, predicted = torch.max(outputs, 1)
235 | all_predictions.extend(predicted.cpu().numpy())
236 | all_targets.extend(batched_labels.cpu().numpy())
237 |
238 | accuracy = accuracy_score(all_targets, all_predictions)
239 | precision = precision_score(all_targets, all_predictions)
240 | recall = recall_score(all_targets, all_predictions)
241 | f1 = f1_score(all_targets, all_predictions)
242 |
243 | print(f"Test Accuracy: {accuracy:.4f}")
244 | print(f"Precision: {precision:.4f}")
245 | print(f"Recall: {recall:.4f}")
246 | print(f"F1 Score: {f1:.4f}")
247 |
248 | # Evaluate the model on test data
249 | model.eval()
250 | all_probabilities = []
251 |
252 | for batched_graph, batched_features, batched_labels in test_dataloader:
253 | if batched_graph is None:
254 | continue
255 | with torch.no_grad():
256 | outputs = model(batched_graph, batched_features)
257 | probabilities = torch.softmax(outputs, dim=1)
258 | all_probabilities.extend(probabilities.cpu().numpy())
259 | # Calculate AUC
260 | all_probabilities = np.array(all_probabilities)
261 | true_labels = np.array(all_targets)
262 | class_1_probs = all_probabilities[:, 1]
263 |
264 | auc = roc_auc_score(true_labels, class_1_probs)
265 |
266 | print(f"AUC: {auc:.4f}")
267 | final_compounds = [(smiles[idx], class_1_probs[i]) for i, idx in enumerate(test_indices) if
268 | class_1_probs[i] >= 0.7]
269 | sorted_compounds = sorted(final_compounds, key=lambda x: x[1], reverse=True)
270 | final_df = pd.DataFrame(sorted_compounds, columns=['Compound', 'Probability'])
271 | generated_file_path = os.path.join(app.config['GENERATED_FILES_DIR'], final_compounds_filename)
272 | final_df.to_csv(generated_file_path, index=False)
273 |
274 | print("File saved successfully!")
275 | final_df = pd.read_csv(generated_file_path)
276 | # Extract the probabilities for clustering
277 | X = final_df[['Probability']].values
278 |
279 | # Fit the Gaussian Mixture Model
280 | n_clusters = 3 # you can change this to the desired number of clusters
281 | gmm = GaussianMixture(n_components=n_clusters, random_state=42)
282 | final_df['Cluster'] = gmm.fit_predict(X)
283 | # Evaluate clustering performance
284 | silhouette_avg = silhouette_score(X, final_df['Cluster'])
285 | davies_bouldin = davies_bouldin_score(X, final_df['Cluster'])
286 | print(f"Silhouette Score: {silhouette_avg:.4f}")
287 | print(f"Davies-Bouldin Score: {davies_bouldin:.4f}")
288 | # Save final results in a file
289 | final_clusters_file_path = os.path.join(app.config['GENERATED_FILES_DIR'], final_clusters_filename)
290 | final_df.to_csv(final_clusters_file_path, index=False)
291 | print("Final clusters saved successfully!")
292 |
293 | # Extract the probabilities and clusters for plotting
294 | X = final_df[['Probability']].values
295 | clusters = final_df['Cluster'].values
296 |
297 | # Create a scatter plot
298 | plt.figure(figsize=(10, 6))
299 | for cluster in range(n_clusters):
300 | cluster_points = X[clusters == cluster]
301 | plt.scatter(cluster_points[:, 0], cluster_points[:, 0], label=f"Cluster {cluster}")
302 |
303 | # Plot the centroids
304 | centroids = gmm.means_
305 | plt.scatter(centroids[:, 0], centroids[:, 0], c='red', marker='X', label='Centroids')
306 |
307 | # Add labels and legend
308 | plt.xlabel('Probability')
309 | plt.ylabel('Probability')
310 | plt.title('Cluster Plot')
311 | plt.legend()
312 |
313 | # Save the plot as an image file
314 | plot_file_path = os.path.join('static', cluster_plot_filename) # Assuming your static folder is set up correctly
315 | plt.savefig(plot_file_path)
316 | plt.close()
317 | # Perform virtual screening
318 | file_path = os.path.join(app.config['GENERATED_FILES_DIR'], final_clusters_filename)
319 | if not os.path.exists(file_path):
320 | flash('File "final_clusters.csv" does not exist. Please generate the clusters first.', 'warning')
321 | else:
322 | # Read CSV files into pandas dataframes
323 | compounds_df = pd.read_csv(os.path.join(app.config['GENERATED_FILES_DIR'], 'final_compounds.csv'))
324 | clusters_df = pd.read_csv(file_path)
325 | # Convert dataframes to HTML tables
326 | compounds_table = compounds_df.to_html(classes='table table-striped table-bordered', index=False)
327 | clusters_table = clusters_df.to_html(classes='table table-striped table-bordered', index=False)
328 |
329 | virtual_screening = True
330 | # Pass data to template
331 | return render_template('upload.html', virtual_screening=virtual_screening,
332 | final_clusters_filename=final_clusters_filename,
333 | final_compounds_filename=final_compounds_filename,
334 | compounds_table=compounds_table, clusters_table=clusters_table,
335 | plot_file_path=plot_file_path[len('static/'):],
336 | generated_file_path=generated_file_path,
337 | final_clusters_file_path=final_clusters_file_path) # Added clusters_table
338 | # Add return statement for GET request
339 | return render_template('upload.html', virtual_screening=virtual_screening,
340 | uploaded_file_path=uploaded_file_path,
341 | generated_file_path=generated_file_path,
342 | generated_molecules=generated_molecules,
343 | plot_file_path=plot_file_path)
344 |
345 | def allow_files(filename):
346 | ALLOWED_EXTENSIONS = {'csv'}
347 | return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
348 | # Ensure the static file serving route can handle the new library cluster plot image
349 | @app.route('/images/')
350 | def uploaded_file(filename):
351 | return send_from_directory('path to/PycharmProjects/pythonProject3/generated_files', filename)
352 | @app.route('/download/sdf_zip')
353 | def download_sdf_zip():
354 | return send_from_directory(app.config['GENERATED_FILES_DIR'], 'compounds_sdf.zip', as_attachment=True)
355 | def get_compound_name_from_pubchem(smiles_string):
356 | # URL for the PubChem PUG-REST service
357 | url = f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/smiles/{smiles_string}/synonyms/JSON"
358 | try:
359 | response = requests.get(url)
360 | response.raise_for_status()
361 | data = response.json()
362 | name = data['InformationList']['Information'][0]['Synonym'][0]
363 | return name
364 | except requests.exceptions.HTTPError as http_err:
365 | print(f"HTTP error occurred: {http_err}")
366 | except Exception as err:
367 | print(f"An error occurred: {err}")
368 | return None
369 |
370 | @app.route('/rescreening', methods=['POST'])
371 | def rescreening():
372 | if request.method == 'POST':
373 | if 'file' in request.files:
374 | file = request.files['file']
375 | if file and allow_files(file.filename):
376 | filename = 'New_Library.csv'
377 | uploaded_file_path = os.path.join(app.config['UPLOADED_FILES_DIR'], filename)
378 | file.save(uploaded_file_path)
379 | #flash('CSV file uploaded and saved successfully.', 'success')
380 | # Load the trained model
381 | model = GNN(1, 64, 2)
382 | model.load_state_dict(torch.load("best_model.pth"))
383 | model.eval()
384 | # Create a dataset for the new library of compounds
385 | new_data = pd.read_csv(uploaded_file_path)
386 | new_smiles = new_data["SMILES"].tolist()
387 | new_dataset = MoleculeDataset(new_smiles, [0] * len(new_smiles)) # labels are not used in prediction
388 | # Use the model to predict the drug-like potential for each compound in the library
389 | new_dataloader = DataLoader(new_dataset, batch_size=32, shuffle=False, collate_fn=collate)
390 |
391 | all_probabilities = []
392 | for batched_graph, batched_features, _ in new_dataloader:
393 | if batched_graph is None:
394 | continue
395 |
396 | with torch.no_grad():
397 | outputs = model(batched_graph, batched_features)
398 | probabilities = torch.softmax(outputs, dim=1)
399 |
400 | all_probabilities.extend(probabilities.cpu().numpy())
401 | # Evaluate the results and perform clustering if needed
402 | all_probabilities = np.array(all_probabilities)
403 | class_1_probs = all_probabilities[:, 1]
404 | # Save final compounds with their respective predicted probabilities
405 | final_compounds = [(new_smiles[i], class_1_probs[i]) for i in range(len(new_smiles))if class_1_probs[i] > 0.7]
406 | print(final_compounds)
407 | sorted_compounds = sorted(final_compounds, key=lambda x: x[1], reverse=True)
408 | final_df = pd.DataFrame(sorted_compounds, columns=['Compound', 'Probability'])
409 | generated_file_path = os.path.join(app.config['GENERATED_FILES_DIR'], 'new_library_predictions.csv')
410 | final_df.to_csv(generated_file_path, index=False)
411 |
412 | # Extract the probabilities for clustering
413 | X = final_df[['Probability']].values
414 |
415 | # Fit the Gaussian Mixture Model
416 | n_clusters = 3 # you can change this to the desired number of clusters
417 | gmm = GaussianMixture(n_components=n_clusters, random_state=42)
418 | final_df['Cluster'] = gmm.fit_predict(X)
419 |
420 | # Save final results in a file
421 | final_clusters_file_path = os.path.join(app.config['GENERATED_FILES_DIR'], 'new_library_clusters.csv')
422 | final_df.to_csv(final_clusters_file_path, index=False)
423 | # Convert SMILES to SDF
424 | compounds_sdf_dir = os.path.join(app.config['GENERATED_FILES_DIR'], 'compounds_sdf')
425 | if not os.path.exists(compounds_sdf_dir):
426 | os.makedirs(compounds_sdf_dir)
427 |
428 | # Save compounds to SDF with PubChem names
429 | for index, row in final_df.iterrows():
430 | mol = Chem.MolFromSmiles(row['Compound'])
431 | compound_name = get_compound_name_from_pubchem(
432 | row['Compound']) or f"Compound_{index}" # Fetch compound name
433 | if mol:
434 | mol.SetProp("_Name", compound_name)
435 | mol.SetProp("Probability", str(row['Probability']))
436 | mol.SetProp("Cluster", str(row['Cluster']))
437 |
438 | # Create a filename from the compound name
439 | safe_filename = secure_filename(
440 | compound_name) # Use secure_filename to ensure it's safe for file paths
441 | sdf_filename = f"{safe_filename}.sdf" if safe_filename else f"Compound_{index}.sdf"
442 | sdf_path = os.path.join(compounds_sdf_dir, sdf_filename)
443 |
444 | with Chem.SDWriter(sdf_path) as writer:
445 | writer.write(mol)
446 |
447 | # Convert the molecule to SDF format
448 | sdf_path = os.path.join(compounds_sdf_dir, f"Compound_{index}.sdf")
449 | with Chem.SDWriter(sdf_path) as writer:
450 | writer.write(mol)
451 |
452 | # Zip the SDF directory
453 | sdf_zipfile_path = os.path.join(app.config['GENERATED_FILES_DIR'], 'compounds_sdf.zip')
454 | with zipfile.ZipFile(sdf_zipfile_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
455 | for root, _, files in os.walk(compounds_sdf_dir):
456 | for file in files:
457 | file_path = os.path.join(root, file)
458 | zipf.write(file_path, arcname=os.path.relpath(file_path, compounds_sdf_dir))
459 |
460 | # Clean up the individual SDF files after zipping by removing the directory
461 | shutil.rmtree(compounds_sdf_dir)
462 |
463 | # Extract the probabilities and clusters for plotting
464 | clusters = final_df['Cluster'].values
465 |
466 | # Create a scatter plot
467 | plt.figure(figsize=(10, 6))
468 | for cluster in range(n_clusters):
469 | cluster_points = X[clusters == cluster]
470 | plt.scatter(cluster_points[:, 0], cluster_points[:, 0], label=f"Cluster {cluster}")
471 |
472 | # Plot the centroids
473 | centroids = gmm.means_
474 | plt.scatter(centroids[:, 0], centroids[:, 0], c='red', marker='X', label='Centroids')
475 |
476 | # Add labels and legend
477 | plt.xlabel('Probability')
478 | plt.ylabel('Probability')
479 | plt.title('Cluster Plot')
480 | plt.legend()
481 |
482 | # Save the plot as an image file
483 | new_plot_file_path = os.path.join(app.config['GENERATED_FILES_DIR'], 'new_library_cluster_plot.png')
484 | plt.savefig(new_plot_file_path)
485 | plt.close()
486 |
487 | # Convert dataframes to HTML tables
488 | compound_table = final_df.to_html(classes='table table-striped table-bordered', index=False)
489 | cluster_table = final_df.to_html(classes='table table-striped table-bordered', index=False)
490 | #clusters_table = final_df[['Compound', 'Cluster']].to_html(classes='table table-striped table-bordered',
491 | #index=False)
492 |
493 | return render_template('upload.html', success=True, compound_table=compound_table,
494 | cluster_table=cluster_table, plot_file_p='new_library_cluster_plot.png',
495 | sdf_zip_file='compounds_sdf.zip')
496 | else:
497 | return render_template('upload.html')
498 |
499 | def generate_de_novo_molecules(num_molecules, apply_lipinski=True):
500 | generated_mol = []
501 | elements = ['C', 'H', 'O', 'N'] # You can expand this list with more elements
502 |
503 | # Correctly obtain the directory path from app.config
504 | generated_files_dir = app.config['GENERATED_FILES_DIR']
505 |
506 | while len(generated_mol) < num_molecules:
507 | compound = ''.join(random.choice(elements) for _ in range(5, 20)) # Generate a compound
508 | mol = Chem.MolFromSmiles(compound)
509 | if mol is None:
510 | continue
511 |
512 | activity = 0
513 |
514 | if apply_lipinski:
515 | molecular_weight = Descriptors.MolWt(mol)
516 | logP = Descriptors.MolLogP(mol)
517 | num_h_donors = Descriptors.NumHDonors(mol)
518 | num_h_acceptors = Descriptors.NumHAcceptors(mol)
519 | # Check if the molecule meets desired property criteria
520 | if 150 <= molecular_weight <= 500 and -2 <= logP <= 5 and num_h_donors <= 5 and num_h_acceptors <= 10:
521 | generated_mol.append((Chem.MolToSmiles(mol, canonical=True), activity))
522 | else:
523 | generated_mol.append((Chem.MolToSmiles(mol, canonical=True), activity))
524 |
525 | # Use the corrected directory path
526 | filename = 'Molecules.csv'
527 | generated_file_p = os.path.join(generated_files_dir, filename)
528 | save_data_to_csv(generated_mol, generated_file_p)
529 |
530 | return generated_mol
531 |
532 | @app.route('/generate', methods=['POST'])
533 | def generate_molecules():
534 | num_molecules = int(request.form['num_molecules'])
535 | apply_lipinski = request.form.get('options') == 'lipinski'
536 | generated_molecules = generate_de_novo_molecules(num_molecules, apply_lipinski)
537 |
538 | # Assuming save_data_to_csv handles the saving process correctly
539 | filename = 'Molecules.csv'
540 | file_path = os.path.join(app.config['GENERATED_FILES_DIR'], filename)
541 | save_data_to_csv(generated_molecules, file_path)
542 |
543 | return send_file(file_path, as_attachment=True, download_name=filename)
544 |
545 |
546 | @app.route('/downloads/')
547 | def downloads(filename):
548 | directory = app.config['GENERATED_FILES_DIR']
549 | try:
550 | return send_from_directory(directory, filename, as_attachment=True)
551 | except FileNotFoundError:
552 | return "File not found.", 404
553 |
554 | # Create a logger to capture the output typically sent to Flask's app.logger
555 | logging.basicConfig(level=logging.DEBUG)
556 | logger = logging.getLogger(__name__)
557 |
558 | def perform_protein_refinement(protein_file_path):
559 | timestamp = int(time.time())
560 | # Updated file names with timestamp
561 | stripped_pdb_filename = f'protein_stripped_{timestamp}.pdb'
562 | fixed_pdb_filename = f'fixed_output_{timestamp}.pdb'
563 | minimized_pdb_filename = f'minimized_protein_{timestamp}.pdb'
564 | ramachandran_plot_filename = f'ramachandran_plot_{timestamp}.png'
565 | sasa_per_residue_plot_filename = f'sasa_per_residue_plot_{timestamp}.png'
566 | logger.debug(f"Starting protein refinement for: {protein_file_path}")
567 | traj = md.load(protein_file_path)
568 | protein = traj.topology.select('protein')
569 | stripped_traj = traj.atom_slice(protein)
570 | stripped_traj.save(stripped_pdb_filename)
571 |
572 | fixer = PDBFixer(stripped_pdb_filename)
573 | fixer.findMissingResidues()
574 | fixer.findMissingAtoms()
575 | fixer.addMissingAtoms()
576 | fixer.addMissingHydrogens(7.4)
577 | with open(fixed_pdb_filename, 'w') as f:
578 | PDBFile.writeFile(fixer.topology, fixer.positions, f)
579 | logger.debug("Protein fixed with PDBFixer. Saved to " + fixed_pdb_filename)
580 |
581 | pdb = PDBFile(fixed_pdb_filename)
582 | modeller = Modeller(pdb.topology, pdb.positions)
583 | forcefield = ForceField('amber14-all.xml', 'amber14/tip3pfb.xml')
584 | try:
585 | modeller.addHydrogens(forcefield)
586 | logger.debug("Added hydrogens to the model.")
587 | except Exception as e:
588 | logger.error(f"An error occurred while adding hydrogens: {e}")
589 | raise
590 |
591 | system = forcefield.createSystem(modeller.topology, nonbondedMethod=PME, nonbondedCutoff=1 * nanometer, constraints=HBonds)
592 | integrator = LangevinMiddleIntegrator(300 * kelvin, 1 / picosecond, 0.004 * picoseconds)
593 | simulation = Simulation(modeller.topology, system, integrator)
594 | simulation.context.setPositions(modeller.positions)
595 | simulation.minimizeEnergy(maxIterations=500)
596 | with open(minimized_pdb_filename, 'w') as f:
597 | state = simulation.context.getState(getPositions=True)
598 | PDBFile.writeFile(modeller.topology, state.getPositions(), f)
599 | logger.debug("Minimized protein structure saved to " + minimized_pdb_filename)
600 |
601 | traj = md.load(minimized_pdb_filename) # Corrected variable name
602 | # Generate and save Ramachandran plot
603 | phi, psi = md.compute_phi(traj), md.compute_psi(traj)
604 | phi_angles, psi_angles = np.rad2deg(md.compute_dihedrals(traj, phi[0])), np.rad2deg(
605 | md.compute_dihedrals(traj, psi[0]))
606 |
607 | plt.figure(figsize=(8, 6))
608 | plt.scatter(phi_angles, psi_angles, s=2, c='blue', alpha=0.5)
609 | # For alpha helices
610 | plt.fill_betweenx(np.arange(-180, 50, 1), -100, -45, color='orange', alpha=0.25)
611 | plt.fill_betweenx(np.arange(-100, 180, 1), 45, 100, color='orange', alpha=0.25)
612 |
613 | # For beta sheets
614 | plt.fill_between(np.arange(-180, 180, 1), 135, 180, color='green', alpha=0.25)
615 | plt.fill_between(np.arange(-180, 180, 1), -180, -135, color='green', alpha=0.25)
616 |
617 | plt.xlim(-180, 180)
618 | plt.ylim(-180, 180)
619 | plt.xlabel('Phi (φ) angles (degrees)')
620 | plt.ylabel('Psi (ψ) angles (degrees)')
621 | plt.title('Ramachandran Plot with Highlighted Secondary Structure Regions')
622 | plt.grid(True)
623 |
624 | # Annotations for secondary structure types
625 | plt.text(-75, 150, 'β-sheet', horizontalalignment='center', verticalalignment='center', color='green',
626 | alpha=0.75)
627 | plt.text(-60, -60, 'α-helix', horizontalalignment='center', verticalalignment='center', color='orange',
628 | alpha=0.75)
629 | plt.text(60, 60, 'α-helix', horizontalalignment='center', verticalalignment='center', color='orange',
630 | alpha=0.75)
631 | plt.text(100, -160, 'β-sheet', horizontalalignment='center', verticalalignment='center', color='green',
632 | alpha=0.75)
633 |
634 | plt.savefig(f'static/{ramachandran_plot_filename}')
635 | plt.close()
636 | # Compute SASA and plot average SASA per residue
637 | sasa = md.shrake_rupley(traj, mode='residue')
638 | # Plot SASA for each residue
639 | plt.plot(np.mean(sasa, axis=0))
640 | plt.title('Average Solvent Accessible Surface Area (SASA) per residue')
641 | plt.xlabel('Residue')
642 | plt.ylabel('SASA (nm²)')
643 | plt.savefig(f'static/{sasa_per_residue_plot_filename}')
644 | plt.close()
645 |
646 | return {
647 | 'stripped_pdb': stripped_pdb_filename,
648 | 'fixed_pdb': fixed_pdb_filename,
649 | 'minimized_pdb': minimized_pdb_filename,
650 | 'ramachandran_plot': f'static/{ramachandran_plot_filename}',
651 | 'sasa_per_residue_plot': f'static/{sasa_per_residue_plot_filename}'
652 | }
653 |
654 |
655 | def allowed_file(filename):
656 | ALLOWED_EXTENSIONS = {'pdb'} # Add or remove file extensions as needed.
657 | return '.' in filename and \
658 | filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
659 |
660 |
661 | @app.route('/protein_refinement', methods=['GET', 'POST'])
662 | def protein_refinement():
663 | try:
664 | if request.method == 'POST':
665 | # Check if the post request has the file part
666 | if 'file' not in request.files:
667 | flash('No file part', 'error')
668 | return redirect(request.url)
669 | file = request.files['file']
670 | if file.filename == '':
671 | flash('No selected file', 'error')
672 | return redirect(request.url)
673 | if file and allowed_file(file.filename):
674 | filename = secure_filename(file.filename)
675 | protein_file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
676 | file.save(protein_file_path)
677 | #flash('Protein file uploaded successfully.', 'success')
678 | perform_protein_refinement(protein_file_path)
679 | file2 = 'ramachandran_plot.png'
680 | file3 = 'sasa_per_residue_plot.png'
681 | result_files = perform_protein_refinement(protein_file_path)
682 | # Generate download links and visualization data for the protein refinement results
683 | download_links = {
684 | 'stripped_protein': url_for('uploa', filename=result_files['stripped_pdb']),
685 | 'fixed_protein': url_for('uploa', filename=result_files['fixed_pdb']),
686 | 'minimized_protein': url_for('uploa', filename=result_files['minimized_pdb']),
687 | 'ramachandran_plot': url_for('static', filename=os.path.basename(result_files['ramachandran_plot'])),
688 | 'sasa_per_residue_plot': url_for('static', filename=os.path.basename(result_files['sasa_per_residue_plot']))
689 | }
690 |
691 | return render_template('upload.html', download_links=download_links, random=int(time.time()), active_tab='protein_refinement')
692 | except Exception as e:
693 | app.logger.error(f"An error occurred during protein refinement: {str(e)}")
694 | flash('An error occurred during processing.', 'error')
695 | return redirect(request.url)
696 | return render_template('upload.html', active_tab='protein_refinement')
697 | @app.route('/files/')
698 | def uploa(filename):
699 | # This sets the directory to your app's root directory
700 | directory = current_app.root_path
701 | return send_from_directory(directory, filename)
702 |
703 |
704 | def allowed_fil(filename):
705 | return '.' in filename and filename.rsplit('.', 1)[1].lower() in {'zip', 'pdb'}
706 | def convert_sdf_to_pdbqt(sdf_path, output_directory):
707 | # Function to convert SDF files in a specified directory to PDBQT format
708 | for root, dirs, files in os.walk(output_directory):
709 | for file in files:
710 | if file.endswith(".sdf"): # Check for .sdf files
711 | sdf_path = os.path.join(root, file)
712 | pdbqt_filename = file.replace('.sdf', '.pdbqt')
713 | pdbqt_path = os.path.join(root, pdbqt_filename)
714 | # Prepare the obabel command
715 | obabel_command = [
716 | 'obabel', sdf_path, '-O', pdbqt_path,
717 | '--gen3d', '-h' # The -h flag adds hydrogens
718 | ]
719 | # Run the obabel command
720 | try:
721 | subprocess.run(obabel_command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
722 | print(f"Conversion successful for {file}")
723 | except subprocess.CalledProcessError as e:
724 | print(f"An error occurred while converting {file}: {e.stderr.decode()}")
725 |
726 | def convert_protein(protein_pdb_path, protein_pdbqt_path):
727 | # Function to convert a PDB file to PDBQT
728 | obabel_command = [
729 | 'obabel', protein_pdb_path, '-xr', '-O', protein_pdbqt_path # The -xr flag removes residues not recognized by AutoDock
730 | ]
731 | try:
732 | subprocess.run(obabel_command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
733 | print(f"Conversion successful for {protein_pdb_path}")
734 | except subprocess.CalledProcessError as e:
735 | error_message = e.stderr.decode() if e.stderr else 'An error occurred.'
736 | print(f"An error occurred while converting {protein_pdb_path}: {error_message}")
737 |
738 |
739 | def clear_workspace(workspace_path):
740 | if os.path.exists(workspace_path):
741 | shutil.rmtree(workspace_path)
742 | os.makedirs(workspace_path)
743 |
744 |
745 | @app.route('/upload', methods=['POST'])
746 | def upload_files():
747 | # Generate a unique job ID for this particular user's session or job
748 | job_id = uuid.uuid4().hex
749 | job_workspace = os.path.join(app.config['UPLOAD_FOLDER'], job_id)
750 | job_results_dir = os.path.join(app.config['DOCKING_RESULTS_DIR'], job_id)
751 |
752 | # Create directories for the job
753 | clear_workspace(job_workspace) # Clear previous data and create new workspace
754 | clear_workspace(job_results_dir) # Clear previous results and create new results directory
755 |
756 | # Save uploaded protein and ligand files
757 | protein_file = request.files.get('protein_file')
758 | ligand_zip = request.files.get('ligand_zip')
759 |
760 | if protein_file and allowed_fil(protein_file.filename) and ligand_zip and allowed_fil(ligand_zip.filename):
761 | protein_filename = secure_filename(protein_file.filename)
762 | ligand_zip_filename = secure_filename(ligand_zip.filename)
763 |
764 | protein_file_path = os.path.join(job_workspace, protein_filename)
765 | ligand_zip_path = os.path.join(job_workspace, ligand_zip_filename)
766 |
767 | protein_file.save(protein_file_path)
768 | ligand_zip.save(ligand_zip_path)
769 |
770 | # Unzip ligands and start conversion
771 | output_directory_path = os.path.join(job_workspace, 'refined_ligands')
772 | Path(output_directory_path).mkdir(parents=True, exist_ok=True)
773 |
774 | with zipfile.ZipFile(ligand_zip_path, 'r') as zip_ref:
775 | zip_ref.extractall(output_directory_path)
776 |
777 | # Convert and dock
778 | convert_sdf_to_pdbqt(sdf_path=ligand_zip_path, output_directory=output_directory_path)
779 | protein_pdbqt_path = protein_file_path.replace('.pdb', '.pdbqt')
780 | convert_protein(protein_file_path, protein_pdbqt_path)
781 | run_docking(protein_pdbqt_path, output_directory_path, job_results_dir)
782 |
783 | return jsonify({'job_id': job_id, 'message': 'Files uploaded, conversion started, and docking initiated!'})
784 | else:
785 | return jsonify({'error': 'Invalid file type or missing files.'}), 400
786 |
787 |
788 | def run_docking(protein_pdbqt, ligand_directory_path, results_directory_path):
789 | print("Starting the docking process...") # Debug print
790 | for ligand_file in Path(ligand_directory_path).glob('*.pdbqt'):
791 | ligand_pdbqt = str(ligand_file)
792 | result_file_path = os.path.join(results_directory_path, ligand_file.stem + '_docked.pdbqt')
793 | # Extract docking parameters from the form
794 | center_x = request.form.get('center_x', type=float)
795 | center_y = request.form.get('center_y', type=float)
796 | center_z = request.form.get('center_z', type=float)
797 | size_x = request.form.get('size_x', type=float)
798 | size_y = request.form.get('size_y', type=float)
799 | size_z = request.form.get('size_z', type=float)
800 | exhaustiveness = request.form.get('exhaustiveness', type=int)
801 | num_modes = request.form.get('num_modes', type=int)
802 | energy_range = request.form.get('energy_range', type=int)
803 |
804 | # Configuration text for docking
805 | # Use these parameters in the docking configuration
806 | config_text = f"""receptor = {protein_pdbqt}
807 | ligand = {ligand_pdbqt}
808 |
809 | center_x = {center_x}
810 | center_y = {center_y}
811 | center_z = {center_z}
812 | size_x = {size_x}
813 | size_y = {size_y}
814 | size_z = {size_z}
815 |
816 | out = {result_file_path}
817 | exhaustiveness = {exhaustiveness}
818 | num_modes = {num_modes}
819 | energy_range = {energy_range}
820 | """
821 | # Write configuration to a file
822 | config_file_path = os.path.join(results_directory_path, ligand_file.stem + '_config.txt')
823 | with open(config_file_path, 'w') as config_file:
824 | config_file.write(config_text)
825 |
826 | # Run Vina with output capture
827 | vina_command = ['vina', '--config', config_file_path]
828 | try:
829 | result = subprocess.run(vina_command, capture_output=True, text=True)
830 | if result.returncode != 0: # Check if the command was not successful
831 | print(f"Error in docking: {result.stderr}") # Log any errors
832 | else:
833 | print(f"Docking completed for {ligand_file.stem}. Output:\n{result.stdout}") # Log the success output
834 | except Exception as e:
835 | print(f"An exception occurred: {e}") # Log any exceptions
836 | finally:
837 | # Clean up the config file after docking
838 | os.remove(config_file_path)
839 | # Initialize an empty list to collect docking data
840 | docking_data = []
841 | for file_name in Path(results_directory_path).glob('*_docked.pdbqt'):
842 | with open(file_name, 'r') as file:
843 | lines = file.readlines()
844 | # Extract data for all poses
845 | for line in lines:
846 | if line.startswith("REMARK VINA RESULT:"):
847 | # Parse out the binding affinity and RMSD
848 | parts = line.split()
849 | binding_affinity = float(parts[3]) # The fourth item on this line is the affinity
850 | rmsd_lb = float(parts[4]) # RMSD lower bound
851 | rmsd_ub = float(parts[5]) # RMSD upper bound
852 | # Store in the list with the 'file_name' key
853 | docking_data.append({
854 | 'file_name': os.path.basename(file_name), # Use basename to get the file name only
855 | 'binding_affinity': binding_affinity,
856 | 'rmsd_lb': rmsd_lb,
857 | 'rmsd_ub': rmsd_ub
858 | })
859 |
860 | # Check if docking data was collected
861 | if docking_data:
862 | # Convert list to DataFrame
863 | df = pd.DataFrame(docking_data)
864 | df_second_poses = df.groupby('file_name').nth(1) # This selects the second pose for each ligand
865 | df_second_poses['final_rmsd'] = df_second_poses['rmsd_ub'] - df_second_poses['rmsd_lb']
866 | df_best_poses = df_second_poses
867 | print(df_best_poses)
868 | csv_file_path = os.path.join(results_directory_path, 'docking_results.csv')
869 | df_best_poses.to_csv(csv_file_path, index=False)
870 | else:
871 | print("No docking data to process.")
872 |
873 | def validate_docking_output(docked_file_path):
874 | if os.path.exists(docked_file_path) and os.path.getsize(docked_file_path) > 0:
875 | with open(docked_file_path, 'r') as file:
876 | for i in range(10):
877 | line = file.readline()
878 | if not line:
879 | break
880 | print(line.strip()) # Process line or check if it's as expected
881 | else:
882 | print(f"Docked file {docked_file_path} not found or is empty.")
883 | @app.route('/docking', methods=['GET'])
884 | def docking():
885 | protein_file_path = request.args.get('protein_file_path', type=str)
886 | protein_pdbqt_path = os.path.join(app.config['UPLOADED_FILES_DIR'], protein_file_path)
887 | ligand_directory_path = os.path.join(app.config['GENERATED_FILES_DIR'], 'refined_ligands')
888 | results_directory_path = os.path.join(app.config['DOCKING_RESULTS_DIR'])
889 |
890 | run_docking(protein_pdbqt_path, ligand_directory_path, results_directory_path)
891 | return jsonify({'message': 'Docking completed!'})
892 |
893 | @app.route('/list_docking_results')
894 | def list_docking_results():
895 | results_files = Path(app.config['DOCKING_RESULTS_DIR']).glob('*_docked.pdbqt')
896 | results_list = [str(result) for result in results_files if result.is_file() and result.stat().st_size > 0]
897 | return jsonify(results_list)
898 | @app.route('/results/')
899 | def download_results(filename):
900 | results_directory_path = os.path.join(app.config['DOCKING_RESULTS_DIR'])
901 | return send_from_directory(directory=results_directory_path, filename=filename, as_attachment=True)
902 | @app.route('/analyze_results/', methods=['GET'])
903 | def analyze_results(job_id):
904 | # Directory where the results are stored
905 | results_directory = os.path.join(app.config['DOCKING_RESULTS_DIR'], job_id)
906 | filepath = os.path.join(results_directory, 'docking_results.csv')
907 |
908 | if os.path.isfile(filepath) and os.path.getsize(filepath) > 0:
909 | return send_file(filepath, as_attachment=True) # Send the file for download
910 | else:
911 | return jsonify({'message': 'Results not ready'}), 202
912 |
913 |
914 | @app.route('/chart_data/') # URL pattern includes job_id
915 | def chart_data(job_id):
916 | # Construct the file path using the job_id provided in the URL
917 | job_results_dir = os.path.join(app.config['DOCKING_RESULTS_DIR'], job_id)
918 | filepath = os.path.join(job_results_dir, 'docking_results.csv')
919 |
920 | if os.path.isfile(filepath):
921 | df = pd.read_csv(filepath)
922 | # Create a barplot of binding affinities
923 | binding_affinities = df['binding_affinity'].tolist()
924 | file_names = df['file_name'].tolist()
925 | chart_data = {
926 | 'labels': file_names,
927 | 'datasets': [{
928 | 'label': 'Binding Affinity',
929 | 'data': binding_affinities,
930 | 'backgroundColor': 'rgba(0, 123, 255, 0.5)',
931 | 'borderColor': 'rgba(0, 123, 255, 1)',
932 | 'borderWidth': 1
933 | }]
934 | }
935 | return jsonify(chart_data)
936 | else:
937 | return jsonify({'message': 'Results not ready for job ' + job_id}), 202
938 |
939 |
940 | @app.route('/download_complexes/')
941 | def download_complexes(job_id):
942 | job_results_dir = os.path.join(app.config['DOCKING_RESULTS_DIR'], job_id)
943 |
944 | # Check if the job results directory exists
945 | if not os.path.isdir(job_results_dir):
946 | return abort(404, description="Job results not found.")
947 |
948 | # Create a BytesIO object to write the zip file in memory
949 | zip_in_memory = BytesIO()
950 | with zipfile.ZipFile(zip_in_memory, 'w', zipfile.ZIP_DEFLATED) as zipf:
951 | for root, dirs, files in os.walk(job_results_dir):
952 | for file in files:
953 | file_path = os.path.join(root, file)
954 | zipf.write(file_path, os.path.relpath(file_path, job_results_dir))
955 | zip_in_memory.seek(0)
956 | zip_filename = f'{job_id}_results.zip'
957 | return send_file(zip_in_memory, download_name=zip_filename, as_attachment=True, mimetype='application/zip')
958 |
959 |
960 | if __name__ == "__main__":
961 | if not os.path.exists(app.config['UPLOADED_FILES_DIR']):
962 | os.makedirs(app.config['UPLOADED_FILES_DIR'])
963 | if not os.path.exists(app.config['GENERATED_FILES_DIR']):
964 | os.makedirs(app.config['GENERATED_FILES_DIR'])
965 | if not os.path.exists(app.config['generated_files_dir']):
966 | os.makedirs(app.config['generated_files_dir'])
967 | if not os.path.exists(app.config['uploaded_files_dir']):
968 | os.makedirs(app.config['uploaded_files_dir'])
969 | if not os.path.exists(app.config['UPLOAD_FOLDER']):
970 | os.makedirs(app.config['UPLOAD_FOLDER'])
971 | if not os.path.exists(app.config['DOCKING_RESULTS_DIR']):
972 | os.makedirs(app.config['DOCKING_RESULTS_DIR'])
973 | app.run(debug=True)
--------------------------------------------------------------------------------