| |
|
|
| import sys |
| import os |
| import sqlite3 |
| import shutil |
| import tempfile |
| from pprint import pprint |
| import pandas as pd |
| import numpy as np |
| import re |
| import argparse |
| import datetime |
| import sys |
| import collections |
| import threading |
|
|
| import flex_ddg_db3 |
|
|
| rosetta_output_file_name = 'rosetta.out' |
| output_database_name = 'ddG.db3' |
| script_output_folder = 'analysis_output' |
|
|
| |
| |
| |
| default_trajectory_stride = 5 |
|
|
| zemu_gam_params = { |
| 'fa_sol' : (6.940, -6.722), |
| 'hbond_sc' : (1.902, -1.999), |
| 'hbond_bb_sc' : (0.063, 0.452), |
| 'fa_rep' : (1.659, -0.836), |
| 'fa_elec' : (0.697, -0.122), |
| 'hbond_lr_bb' : (2.738, -1.179), |
| 'fa_atr' : (2.313, -1.649), |
| } |
|
|
| def gam_function(x, score_term = None ): |
| return -1.0 * np.exp( zemu_gam_params[score_term][0] ) + 2.0 * np.exp( zemu_gam_params[score_term][0] ) / ( 1.0 + np.exp( -1.0 * x * np.exp( zemu_gam_params[score_term][1] ) ) ) |
|
|
| def apply_zemu_gam(scores): |
| new_columns = list(scores.columns) |
| new_columns.remove('total_score') |
| scores = scores.copy()[ new_columns ] |
| for score_term in zemu_gam_params: |
| assert( score_term in scores.columns ) |
| scores[score_term] = scores[score_term].apply( gam_function, score_term = score_term ) |
| scores[ 'total_score' ] = scores[ list(zemu_gam_params.keys()) ].sum( axis = 1 ) |
| scores[ 'score_function_name' ] = scores[ 'score_function_name' ] + '-gam' |
| return scores |
|
|
| def rosetta_output_succeeded( potential_struct_dir ): |
| path_to_rosetta_output = os.path.join( potential_struct_dir, rosetta_output_file_name ) |
| if not os.path.isfile(path_to_rosetta_output): |
| return False |
|
|
| db3_file = os.path.join( potential_struct_dir, output_database_name ) |
| if not os.path.isfile( db3_file ): |
| return False |
|
|
| success_line_found = False |
| no_more_batches_line_found = False |
| with open( path_to_rosetta_output, 'r' ) as f: |
| for line in f: |
| if line.startswith( 'protocols.jd2.JobDistributor' ) and 'reported success in' in line: |
| success_line_found = True |
| if line.startswith( 'protocols.jd2.JobDistributor' ) and 'no more batches to process' in line: |
| no_more_batches_line_found = True |
|
|
| return no_more_batches_line_found and success_line_found |
|
|
| def find_finished_jobs( output_folder ): |
| return_dict = {} |
| job_dirs = [ os.path.abspath(os.path.join(output_folder, d)) for d in os.listdir(output_folder) if os.path.isdir( os.path.join(output_folder, d) )] |
| for job_dir in job_dirs: |
| completed_struct_dirs = [] |
| for potential_struct_dir in sorted([ os.path.abspath(os.path.join(job_dir, d)) for d in os.listdir(job_dir) if os.path.isdir( os.path.join(job_dir, d) )]): |
| if rosetta_output_succeeded( potential_struct_dir ): |
| completed_struct_dirs.append( potential_struct_dir ) |
| return_dict[job_dir] = completed_struct_dirs |
|
|
| return return_dict |
|
|
| def get_scores_from_db3_file(db3_file, struct_number, case_name, trajectory_stride): |
| conn = sqlite3.connect(db3_file) |
| conn.row_factory = sqlite3.Row |
| c = conn.cursor() |
|
|
| num_batches = c.execute('SELECT max(batch_id) from batches').fetchone()[0] |
|
|
| scores = pd.read_sql_query(''' |
| SELECT batches.name, structure_scores.struct_id, score_types.score_type_name, structure_scores.score_value, score_function_method_options.score_function_name from structure_scores |
| INNER JOIN batches ON batches.batch_id=structure_scores.batch_id |
| INNER JOIN score_function_method_options ON score_function_method_options.batch_id=batches.batch_id |
| INNER JOIN score_types ON score_types.batch_id=structure_scores.batch_id AND score_types.score_type_id=structure_scores.score_type_id |
| ''', conn) |
|
|
| def renumber_struct_id( struct_id ): |
| return trajectory_stride * ( 1 + (int(struct_id-1) // num_batches) ) |
|
|
| scores['struct_id'] = scores['struct_id'].apply( renumber_struct_id ) |
| scores['name'] = scores['name'].apply( lambda x: x[:-9] if x.endswith('_dbreport') else x ) |
| scores = scores.pivot_table( index = ['name', 'struct_id', 'score_function_name'], columns = 'score_type_name', values = 'score_value' ).reset_index() |
| scores.rename( columns = { |
| 'name' : 'state', |
| 'struct_id' : 'backrub_steps', |
| }, inplace=True) |
| scores['struct_num'] = struct_number |
| scores['case_name'] = case_name |
|
|
| conn.close() |
|
|
| return scores |
|
|
| def get_per_chain_scores_from_db3_file(db3_file, struct_number, case_name, trajectory_stride): |
| '''Read the per-chain intramolecular energies written by the per-chain protocol variant |
| (see per_chain_protocol.py). Returns None if the run did not report them. |
| |
| Only the unbound states are meaningful here: the chains are 1000 A apart, so there are no |
| cross-chain pair energies and each value is exactly that chain's intramolecular energy. On |
| the bound states the value additionally carries roughly half the interface energy, because |
| Rosetta splits each two-body term between its two residues. |
| |
| Note that chain IDs come back lowercased, because Rosetta lowercases database table names. |
| Chain "A" appears here as "a". per_chain_protocol.py refuses to set up a run whose chain IDs |
| differ only by case, so this stays unambiguous. |
| ''' |
| conn = sqlite3.connect(db3_file) |
| conn.row_factory = sqlite3.Row |
| c = conn.cursor() |
|
|
| chain_tables = [ row[0] for row in c.execute( |
| "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'chain\\_%\\_energy' ESCAPE '\\'" |
| ).fetchall() ] |
| if len(chain_tables) == 0: |
| conn.close() |
| return None |
|
|
| num_batches = c.execute('SELECT max(batch_id) from batches').fetchone()[0] |
|
|
| dfs = [] |
| for table in chain_tables: |
| chain = table[len('chain_'):-len('_energy')] |
| df = pd.read_sql_query(''' |
| SELECT batches.name, %s.struct_id, %s.total_energy from %s |
| INNER JOIN structures ON structures.struct_id=%s.struct_id |
| INNER JOIN batches ON batches.batch_id=structures.batch_id |
| ''' % (table, table, table, table), conn) |
| df['chain'] = chain |
| dfs.append(df) |
| conn.close() |
|
|
| scores = pd.concat( dfs ) |
| scores['struct_id'] = scores['struct_id'].apply( |
| lambda struct_id: trajectory_stride * ( 1 + (int(struct_id-1) // num_batches) ) ) |
| scores['name'] = scores['name'].apply( lambda x: x[:-9] if x.endswith('_dbreport') else x ) |
| scores.rename( columns = { |
| 'name' : 'state', |
| 'struct_id' : 'backrub_steps', |
| 'total_energy' : 'intra_energy', |
| }, inplace=True) |
| scores['struct_num'] = struct_number |
| scores['case_name'] = case_name |
|
|
| return scores |
|
|
| def calc_per_chain_ddg( scores ): |
| '''Per-chain intramolecular ddG, read off the unbound states and averaged over nstruct.''' |
| unbound = scores.loc[ scores['state'].isin(['unbound_wt', 'unbound_mut']) ].copy() |
| if len(unbound) == 0: |
| return None |
|
|
| wide = unbound.pivot_table( |
| index = ['case_name', 'chain', 'backrub_steps', 'struct_num'], |
| columns = 'state', values = 'intra_energy' ).reset_index() |
| if 'unbound_wt' not in wide.columns or 'unbound_mut' not in wide.columns: |
| return None |
| wide['ddG'] = wide['unbound_mut'] - wide['unbound_wt'] |
|
|
| summary = wide.groupby( ['case_name', 'chain', 'backrub_steps'] ).agg( |
| nstruct = ('ddG', 'size'), |
| wt_intra = ('unbound_wt', 'mean'), |
| mut_intra = ('unbound_mut', 'mean'), |
| ddG = ('ddG', 'mean'), |
| ddG_sd = ('ddG', 'std'), |
| ).reset_index() |
| summary['ddG_sem'] = summary['ddG_sd'] / np.sqrt( summary['nstruct'] ) |
| return summary.round(decimals=5) |
|
|
| def resolve_trajectory_stride( db3_file, stride_override = None ): |
| '''Stride to label this database's checkpoints with, preferring what the run recorded.''' |
| if stride_override is not None: |
| return stride_override |
|
|
| stride = flex_ddg_db3.trajectory_stride_from_db3( db3_file ) |
| if stride is not None: |
| return stride |
|
|
| print( 'WARNING: %s does not record backrub_trajectory_stride; assuming %d.' % ( |
| db3_file, default_trajectory_stride ) ) |
| print( ' If the run used a different stride, pass --stride to label the' ) |
| print( ' checkpoints correctly. This affects labels only, not any energy.' ) |
| return default_trajectory_stride |
|
|
| def process_finished_struct( output_path, case_name, stride_override = None ): |
| db3_file = os.path.join( output_path, output_database_name ) |
| assert( os.path.isfile( db3_file ) ) |
| struct_number = int( os.path.basename(output_path) ) |
| trajectory_stride = resolve_trajectory_stride( db3_file, stride_override ) |
| scores_df = get_scores_from_db3_file( db3_file, struct_number, case_name, trajectory_stride ) |
| per_chain_df = get_per_chain_scores_from_db3_file( db3_file, struct_number, case_name, trajectory_stride ) |
|
|
| return scores_df, per_chain_df |
|
|
| def calc_ddg( scores ): |
| total_structs = np.max( scores['struct_num'] ) |
|
|
| nstructs_to_analyze = set([total_structs]) |
| for x in range(10, total_structs): |
| if x % 10 == 0: |
| nstructs_to_analyze.add(x) |
| nstructs_to_analyze = sorted(nstructs_to_analyze) |
|
|
| all_ddg_scores = [] |
| for nstructs in nstructs_to_analyze: |
| ddg_scores = scores.loc[ ((scores['state'] == 'unbound_mut') | (scores['state'] == 'bound_wt')) & (scores['struct_num'] <= nstructs) ].copy() |
| for column in ddg_scores.columns: |
| if column not in ['state', 'case_name', 'backrub_steps', 'struct_num', 'score_function_name']: |
| ddg_scores.loc[:,column] *= -1.0 |
| ddg_scores = pd.concat( [ ddg_scores, scores.loc[ ((scores['state'] == 'unbound_wt') | (scores['state'] == 'bound_mut')) & (scores['struct_num'] <= nstructs) ].copy() ] ) |
| ddg_scores = ddg_scores.groupby( ['case_name', 'backrub_steps', 'struct_num', 'score_function_name'] ).sum( numeric_only = True ).reset_index() |
|
|
| if nstructs == total_structs: |
| struct_scores = ddg_scores.copy() |
|
|
| ddg_scores = ddg_scores.groupby( ['case_name', 'backrub_steps', 'score_function_name'] ).mean( numeric_only = True ).round(decimals=5).reset_index() |
| new_columns = list(ddg_scores.columns.values) |
| new_columns.remove( 'struct_num' ) |
| ddg_scores = ddg_scores[new_columns] |
| ddg_scores[ 'scored_state' ] = 'ddG' |
| ddg_scores[ 'nstruct' ] = nstructs |
| all_ddg_scores.append(ddg_scores) |
|
|
| return (pd.concat(all_ddg_scores), struct_scores) |
|
|
| def calc_dgs( scores ): |
| l = [] |
|
|
| total_structs = np.max( scores['struct_num'] ) |
|
|
| nstructs_to_analyze = set([total_structs]) |
| for x in range(10, total_structs): |
| if x % 10 == 0: |
| nstructs_to_analyze.add(x) |
| nstructs_to_analyze = sorted(nstructs_to_analyze) |
|
|
| for state in ['mut', 'wt']: |
| for nstructs in nstructs_to_analyze: |
| dg_scores = scores.loc[ (scores['state'].str.endswith(state)) & (scores['state'].str.startswith('unbound')) & (scores['struct_num'] <= nstructs) ].copy() |
| for column in dg_scores.columns: |
| if column not in ['state', 'case_name', 'backrub_steps', 'struct_num', 'score_function_name']: |
| dg_scores.loc[:,column] *= -1.0 |
| dg_scores = pd.concat( [ dg_scores, scores.loc[ (scores['state'].str.endswith(state)) & (scores['state'].str.startswith('bound')) & (scores['struct_num'] <= nstructs) ].copy() ] ) |
| dg_scores = dg_scores.groupby( ['case_name', 'backrub_steps', 'struct_num', 'score_function_name'] ).sum( numeric_only = True ).reset_index() |
| dg_scores = dg_scores.groupby( ['case_name', 'backrub_steps', 'score_function_name'] ).mean( numeric_only = True ).round(decimals=5).reset_index() |
| new_columns = list(dg_scores.columns.values) |
| new_columns.remove( 'struct_num' ) |
| dg_scores = dg_scores[new_columns] |
| dg_scores[ 'scored_state' ] = state + '_dG' |
| dg_scores[ 'nstruct' ] = nstructs |
| l.append( dg_scores ) |
| return l |
|
|
| def analyze_output_folder( output_folder, stride_override = None ): |
| |
| finished_jobs = find_finished_jobs( output_folder ) |
| if len(finished_jobs) == 0: |
| print( 'No finished jobs found' ) |
| return |
|
|
| ddg_scores_dfs = [] |
| struct_scores_dfs = [] |
| per_chain_dfs = [] |
| for finished_job, finished_structs in finished_jobs.items(): |
| inner_scores_list = [] |
| inner_per_chain_list = [] |
| for finished_struct in finished_structs: |
| inner_scores, inner_per_chain = process_finished_struct( finished_struct, os.path.basename(finished_job), stride_override ) |
| inner_scores_list.append( inner_scores ) |
| if inner_per_chain is not None: |
| inner_per_chain_list.append( inner_per_chain ) |
| scores = pd.concat( inner_scores_list ) |
| if len(inner_per_chain_list) > 0: |
| per_chain_summary = calc_per_chain_ddg( pd.concat( inner_per_chain_list ) ) |
| if per_chain_summary is not None: |
| per_chain_dfs.append( per_chain_summary ) |
| ddg_scores, struct_scores = calc_ddg( scores ) |
| struct_scores_dfs.append( struct_scores ) |
| ddg_scores_dfs.append( ddg_scores ) |
| ddg_scores_dfs.append( apply_zemu_gam(ddg_scores) ) |
| ddg_scores_dfs.extend( calc_dgs( scores ) ) |
|
|
| if not os.path.isdir(script_output_folder): |
| os.makedirs(script_output_folder) |
| basename = os.path.basename(output_folder) |
|
|
| pd.concat( struct_scores_dfs ).to_csv( os.path.join(script_output_folder, basename + '-struct_scores_results.csv' ) ) |
|
|
| df = pd.concat( ddg_scores_dfs ) |
| df.to_csv( os.path.join(script_output_folder, basename + '-results.csv') ) |
|
|
| display_columns = ['backrub_steps', 'case_name', 'nstruct', 'score_function_name', 'scored_state', 'total_score'] |
| for score_type in ['mut_dG', 'wt_dG', 'ddG']: |
| print( score_type ) |
| print( df.loc[ df['scored_state'] == score_type ][display_columns].head( n = 20 ) ) |
| print( '' ) |
|
|
| if len(per_chain_dfs) > 0: |
| per_chain = pd.concat( per_chain_dfs ) |
| per_chain.to_csv( os.path.join(script_output_folder, basename + '-per_chain_results.csv'), index = False ) |
| print( 'per-chain intramolecular ddG (from the unbound states)' ) |
| print( per_chain.head( n = 40 ).to_string(index = False) ) |
| print( '' ) |
| print( 'NOTE: this is the intramolecular strain difference in the *bound* backbone' ) |
| print( ' conformation, not a folding ddG -- the unbound state is never relaxed.' ) |
| print( ' A chain you did not mutate should come out at ~0 +/- ddG_sem; if it does' ) |
| print( ' not, nstruct is too low to average out the whole-pose minimization noise.' ) |
| print( '' ) |
|
|
| if __name__ == '__main__': |
| parser = argparse.ArgumentParser( |
| description = 'Analyze one or more flex ddG output folders (e.g. "output").' ) |
| parser.add_argument( 'output_folders', nargs = '+', help = 'flex ddG output folder(s)' ) |
| parser.add_argument( '--stride', type = int, default = None, |
| help = 'override backrub_trajectory_stride instead of reading it from' |
| ' each ddG.db3. Affects checkpoint labels only, not any energy.' ) |
| parsed_args = parser.parse_args() |
|
|
| for folder_to_analyze in parsed_args.output_folders: |
| if os.path.isdir( folder_to_analyze ): |
| analyze_output_folder( folder_to_analyze, parsed_args.stride ) |
| else: |
| print( 'ERROR: %s is not a valid directory' % folder_to_analyze ) |
|
|