| |
| """ |
| Epitope Prediction Web Tool |
| Similar to BepiPred-2.0 for B-cell and T-cell epitope prediction |
| """ |
|
|
| import os |
| import json |
| import uuid |
| import logging |
| from datetime import datetime |
| from flask import Flask, render_template, request, jsonify, send_file, flash, redirect, url_for |
| from werkzeug.utils import secure_filename |
| import pandas as pd |
| import numpy as np |
| from io import StringIO, BytesIO |
| import tempfile |
|
|
| |
| from model_predictor import EpitopePredictor |
|
|
| |
| logging.basicConfig( |
| level=logging.INFO, |
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', |
| handlers=[ |
| logging.FileHandler('epitope_prediction.log'), |
| logging.StreamHandler() |
| ] |
| ) |
| logger = logging.getLogger(__name__) |
|
|
| app = Flask(__name__) |
| app.secret_key = 'your-secret-key-change-this-in-production' |
| app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 |
| app.config['UPLOAD_FOLDER'] = 'static/uploads' |
|
|
| |
| predictor = None |
| try: |
| logger.info("Initializing EpitopePredictor...") |
| predictor = EpitopePredictor() |
| logger.info("Model loaded successfully") |
| |
| |
| model_info = predictor.get_model_info() |
| logger.info(f"Model configuration: {model_info}") |
| |
| except Exception as e: |
| logger.error(f"Failed to load model: {e}") |
| predictor = None |
|
|
| |
| ALLOWED_EXTENSIONS = {'txt', 'fasta', 'fa', 'fas'} |
|
|
| def allowed_file(filename): |
| return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS |
|
|
| @app.route('/') |
| def index(): |
| """Main page with sequence input interface""" |
| logger.info("Serving main page") |
| return render_template('index.html') |
|
|
| @app.route('/instructions') |
| def instructions(): |
| """Instructions page""" |
| logger.info("Serving instructions page") |
| return render_template('instructions.html') |
|
|
| @app.route('/about') |
| def about(): |
| """About page with method information""" |
| logger.info("Serving about page") |
| return render_template('about.html') |
|
|
| @app.route('/health') |
| def health_check(): |
| """Health check endpoint for deployment monitoring""" |
| from datetime import datetime |
| return jsonify({ |
| 'status': 'healthy', |
| 'timestamp': datetime.now().isoformat(), |
| 'model_loaded': predictor is not None, |
| 'version': '1.0.0' |
| }) |
|
|
| @app.route('/status') |
| def status(): |
| """System status endpoint for monitoring""" |
| logger.info("Status check requested") |
| |
| status_info = { |
| 'model_loaded': predictor is not None, |
| 'timestamp': datetime.now().isoformat(), |
| 'upload_folder_exists': os.path.exists(app.config['UPLOAD_FOLDER']) |
| } |
| |
| if predictor: |
| try: |
| model_info = predictor.get_model_info() |
| status_info.update({ |
| 'model_info': model_info, |
| 'model_ready': True |
| }) |
| except Exception as e: |
| logger.error(f"Error getting model info: {e}") |
| status_info.update({ |
| 'model_ready': False, |
| 'model_error': str(e) |
| }) |
| else: |
| status_info.update({ |
| 'model_ready': False, |
| 'model_error': 'Model not loaded' |
| }) |
| |
| logger.info(f"Status check result: {status_info}") |
| return jsonify(status_info) |
|
|
| @app.route('/predict', methods=['POST']) |
| def predict(): |
| """Handle sequence prediction requests""" |
| logger.info("Received prediction request") |
| |
| |
| if predictor is None: |
| logger.error("Prediction service is unavailable - model not loaded") |
| flash('Prediction service is currently unavailable. Please check that the model files are properly installed.', 'error') |
| return redirect(url_for('index')) |
|
|
| try: |
| |
| sequence_text = request.form.get('sequence_text', '').strip() |
| uploaded_file = request.files.get('sequence_file') |
| |
| logger.info(f"Input - Text: {len(sequence_text)} chars, File: {'Yes' if uploaded_file and uploaded_file.filename else 'No'}") |
|
|
| sequences = {} |
|
|
| |
| if sequence_text: |
| try: |
| logger.info("Parsing sequence text input") |
| sequences.update(parse_fasta_text(sequence_text)) |
| logger.info(f"Parsed {len(sequences)} sequences from text input") |
| except Exception as e: |
| logger.error(f"Error parsing sequence text: {str(e)}") |
| flash(f'Error parsing sequence text: {str(e)}', 'error') |
| return redirect(url_for('index')) |
|
|
| |
| if uploaded_file and uploaded_file.filename != '' and allowed_file(uploaded_file.filename): |
| try: |
| filename = secure_filename(uploaded_file.filename) |
| file_content = uploaded_file.read().decode('utf-8') |
| logger.info(f"Processing uploaded file: {filename} ({len(file_content)} chars)") |
| |
| file_sequences = parse_fasta_text(file_content) |
| sequences.update(file_sequences) |
| logger.info(f"Parsed {len(file_sequences)} sequences from uploaded file") |
| except UnicodeDecodeError: |
| logger.error("Unicode decode error when reading uploaded file") |
| flash('Error reading file. Please ensure the file is in text format with UTF-8 encoding.', 'error') |
| return redirect(url_for('index')) |
| except Exception as e: |
| logger.error(f"Error processing uploaded file: {str(e)}") |
| flash(f'Error processing uploaded file: {str(e)}', 'error') |
| return redirect(url_for('index')) |
|
|
| if not sequences: |
| logger.warning("No sequences provided in request") |
| flash('Please provide protein sequences in FASTA format.', 'error') |
| return redirect(url_for('index')) |
|
|
| logger.info(f"Total sequences to process: {len(sequences)}") |
|
|
| |
| validation_errors = validate_sequences(sequences) |
| if validation_errors: |
| logger.warning(f"Sequence validation failed: {len(validation_errors)} errors") |
| for error in validation_errors: |
| logger.warning(f"Validation error: {error}") |
| flash(error, 'error') |
| return redirect(url_for('index')) |
| |
| logger.info("Sequence validation passed") |
| |
| |
| job_id = str(uuid.uuid4()) |
| logger.info(f"Generated job ID: {job_id}") |
| |
| |
| results = {} |
| failed_sequences = [] |
|
|
| for seq_name, seq in sequences.items(): |
| try: |
| logger.info(f"Predicting epitopes for sequence: {seq_name} (length: {len(seq)})") |
| |
| |
| b_cell_epitopes, t_cell_epitopes = predictor.predict_epitopes(seq) |
| |
| logger.info(f"Prediction completed for {seq_name}: {len(b_cell_epitopes)} B-cell, {len(t_cell_epitopes)} T-cell epitopes") |
| |
| results[seq_name] = { |
| 'sequence': seq, |
| 'b_cell_epitopes': b_cell_epitopes, |
| 't_cell_epitopes': t_cell_epitopes, |
| 'length': len(seq) |
| } |
| |
| |
| if b_cell_epitopes: |
| logger.info(f"B-cell epitopes for {seq_name}: {[(ep[0], f'{ep[1]:.3f}', ep[2]) for ep in b_cell_epitopes[:3]]}") |
| if t_cell_epitopes: |
| logger.info(f"T-cell epitopes for {seq_name}: {[(ep[0], f'{ep[1]:.3f}', ep[2]) for ep in t_cell_epitopes[:3]]}") |
| |
| except Exception as e: |
| logger.error(f"Prediction failed for sequence {seq_name}: {e}") |
| failed_sequences.append(seq_name) |
|
|
| |
| if not results: |
| logger.error("All predictions failed") |
| flash('Prediction failed for all sequences. Please check your input and try again.', 'error') |
| return redirect(url_for('index')) |
|
|
| logger.info(f"Prediction completed successfully for {len(results)} sequences") |
|
|
| |
| if failed_sequences: |
| logger.warning(f"Prediction failed for {len(failed_sequences)} sequences: {failed_sequences}") |
| flash(f'Prediction failed for {len(failed_sequences)} sequence(s): {", ".join(failed_sequences[:3])}{"..." if len(failed_sequences) > 3 else ""}', 'warning') |
| |
| |
| results_data = { |
| 'job_id': job_id, |
| 'timestamp': datetime.now().isoformat(), |
| 'results': results, |
| 'total_sequences': len(sequences), |
| 'model_info': predictor.get_model_info() if predictor else {} |
| } |
| |
| |
| results_file = os.path.join(app.config['UPLOAD_FOLDER'], f'{job_id}_results.json') |
| with open(results_file, 'w') as f: |
| json.dump(results_data, f, indent=2) |
| |
| logger.info(f"Results saved to: {results_file}") |
| logger.info(f"Prediction request completed successfully for job: {job_id}") |
| |
| return render_template('results.html', |
| job_id=job_id, |
| results=results_data, |
| enumerate=enumerate) |
| |
| except Exception as e: |
| logger.error(f"Unexpected error during prediction: {str(e)}") |
| flash(f'An error occurred during prediction: {str(e)}', 'error') |
| return redirect(url_for('index')) |
|
|
| @app.route('/download/<job_id>/<format>') |
| def download_results(job_id, format): |
| """Download results in specified format""" |
| logger.info(f"Download request - Job ID: {job_id}, Format: {format}") |
| |
| try: |
| results_file = os.path.join(app.config['UPLOAD_FOLDER'], f'{job_id}_results.json') |
| |
| if not os.path.exists(results_file): |
| logger.warning(f"Results file not found: {results_file}") |
| flash('Results not found or expired.', 'error') |
| return redirect(url_for('index')) |
| |
| with open(results_file, 'r') as f: |
| results_data = json.load(f) |
| |
| logger.info(f"Generating {format.upper()} download for job {job_id}") |
| |
| if format.lower() == 'csv': |
| return download_csv(results_data, job_id) |
| elif format.lower() == 'json': |
| return download_json(results_data, job_id) |
| else: |
| logger.warning(f"Invalid download format requested: {format}") |
| flash('Invalid download format.', 'error') |
| return redirect(url_for('index')) |
| |
| except Exception as e: |
| logger.error(f"Error downloading results for job {job_id}: {str(e)}") |
| flash(f'Error downloading results: {str(e)}', 'error') |
| return redirect(url_for('index')) |
|
|
| def parse_fasta_text(text): |
| """Parse FASTA format text and return dictionary of sequences""" |
| logger.debug("Parsing FASTA text input") |
| sequences = {} |
| current_name = None |
| current_seq = [] |
| |
| for line in text.strip().split('\n'): |
| line = line.strip() |
| if line.startswith('>'): |
| if current_name and current_seq: |
| sequences[current_name] = ''.join(current_seq) |
| current_name = line[1:].strip() or f"Sequence_{len(sequences)+1}" |
| current_seq = [] |
| elif line and current_name: |
| |
| clean_seq = ''.join(c.upper() for c in line if c.upper() in 'ACDEFGHIKLMNPQRSTVWY') |
| current_seq.append(clean_seq) |
| |
| if current_name and current_seq: |
| sequences[current_name] = ''.join(current_seq) |
| |
| logger.debug(f"Parsed {len(sequences)} sequences from FASTA text") |
| return sequences |
|
|
| def validate_sequences(sequences): |
| """Validate input sequences""" |
| logger.debug(f"Validating {len(sequences)} sequences") |
| errors = [] |
| |
| if len(sequences) > 50: |
| errors.append("Maximum 50 sequences allowed per submission.") |
| |
| total_length = sum(len(seq) for seq in sequences.values()) |
| if total_length > 300000: |
| errors.append("Total sequence length exceeds 300,000 amino acids.") |
| |
| for name, seq in sequences.items(): |
| if len(seq) < 10: |
| errors.append(f"Sequence '{name}' is too short (minimum 10 amino acids).") |
| elif len(seq) > 6000: |
| errors.append(f"Sequence '{name}' is too long (maximum 6000 amino acids).") |
| |
| |
| valid_aa = set('ACDEFGHIKLMNPQRSTVWY') |
| invalid_chars = set(seq.upper()) - valid_aa |
| if invalid_chars: |
| errors.append(f"Sequence '{name}' contains invalid characters: {', '.join(invalid_chars)}") |
| |
| if errors: |
| logger.warning(f"Sequence validation failed with {len(errors)} errors") |
| else: |
| logger.debug("Sequence validation passed") |
| |
| return errors |
|
|
| def download_csv(results_data, job_id): |
| """Generate CSV download""" |
| logger.info(f"Generating CSV download for job {job_id}") |
| rows = [] |
| |
| for seq_name, data in results_data['results'].items(): |
| |
| for epitope, confidence, pos_range in data['b_cell_epitopes']: |
| rows.append({ |
| 'Sequence_Name': seq_name, |
| 'Epitope_Type': 'B-cell', |
| 'Epitope_Sequence': epitope, |
| 'Confidence': confidence, |
| 'Position_Range': pos_range, |
| 'Full_Sequence': data['sequence'] |
| }) |
| |
| |
| for epitope, confidence, pos_range in data['t_cell_epitopes']: |
| rows.append({ |
| 'Sequence_Name': seq_name, |
| 'Epitope_Type': 'T-cell', |
| 'Epitope_Sequence': epitope, |
| 'Confidence': confidence, |
| 'Position_Range': pos_range, |
| 'Full_Sequence': data['sequence'] |
| }) |
| |
| df = pd.DataFrame(rows) |
| logger.info(f"Generated CSV with {len(rows)} epitope predictions") |
| |
| |
| output = StringIO() |
| df.to_csv(output, index=False) |
| output.seek(0) |
| |
| |
| mem = BytesIO() |
| mem.write(output.getvalue().encode('utf-8')) |
| mem.seek(0) |
| |
| return send_file(mem, |
| as_attachment=True, |
| download_name=f'epitope_predictions_{job_id}.csv', |
| mimetype='text/csv') |
|
|
| def download_json(results_data, job_id): |
| """Generate JSON download""" |
| logger.info(f"Generating JSON download for job {job_id}") |
| mem = BytesIO() |
| mem.write(json.dumps(results_data, indent=2).encode('utf-8')) |
| mem.seek(0) |
| |
| return send_file(mem, |
| as_attachment=True, |
| download_name=f'epitope_predictions_{job_id}.json', |
| mimetype='application/json') |
|
|
| if __name__ == '__main__': |
| |
| os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) |
| logger.info(f"Upload directory created/verified: {app.config['UPLOAD_FOLDER']}") |
| |
| |
| logger.info("Starting EpiPred web application") |
| logger.info(f"Model status: {'Loaded' if predictor else 'Failed to load'}") |
| |
| if predictor: |
| model_info = predictor.get_model_info() |
| logger.info(f"Model ready - Window size: {model_info['window_size']}, Threshold: {model_info['confidence_threshold']}") |
| |
| |
| logger.info("Starting Flask development server on 0.0.0.0:5000") |
| app.run(debug=True, host='0.0.0.0', port=5000) |
|
|