Download scripts/reprocess_per_chain.py from OneScience-Group/flex_ddG_tutorial: direct link, hf CLI and curl.
- Browser
- Download file 10.7 kB
-
https://huggingface.co/OneScience-Group/flex_ddG_tutorial/resolve/main/scripts/reprocess_per_chain.py
- Command line
-
hf download hf://OneScience-Group/flex_ddG_tutorial/scripts/reprocess_per_chain.py
-
curl -L -o reprocess_per_chain.py https://huggingface.co/OneScience-Group/flex_ddG_tutorial/resolve/main/scripts/reprocess_per_chain.py
10.7 kB
| #!/usr/bin/env python3 | |
| """ | |
| Recover per-chain intramolecular energies from an EXISTING flex ddG run. | |
| No re-sampling is required. The backrub trajectory is the expensive part and you already ran | |
| it: struct.db3 holds the coordinates of every backrub, wild type minimized and mutant minimized | |
| pose. This script reads those poses back into Rosetta, isolates one chain at a time, rescores, | |
| and reports per-chain ddG. Verified to reproduce the in-protocol per-chain metric (see | |
| per_chain_protocol.py) to nine decimal places. | |
| Why the chains have to be physically isolated rather than just selected: struct.db3 stores the | |
| *bound* poses. A Chain residue selector scoped over a bound pose still picks up cross-chain pair | |
| energies, because Rosetta's residue_total_energies splits each two-body term between its two | |
| residues, so roughly half the interface energy leaks into each chain. Deleting the other chains | |
| reproduces the separated unbound state exactly, since intra-chain energy is invariant under the | |
| rigid-body translation InterfaceDdGMover uses to unbind. | |
| What this does and does not give you | |
| ------------------------------------ | |
| It gives you the intramolecular energy difference between the mutant and wild type in the | |
| *bound* backbone conformation, per chain. That is a strain term. It is NOT a folding ddG of the | |
| free monomer: the unbound state is never relaxed here, in this script or in flex ddG itself. For | |
| a true monomer stability ddG use a dedicated protocol such as cartesian_ddg on the isolated chain. | |
| Built-in control: a chain you did not mutate should come out at 0 within its SEM. If it does not, | |
| nstruct is too low to average out the whole-pose minimization noise, and the mutated chain's | |
| number is not trustworthy either. | |
| Usage | |
| ----- | |
| python3 reprocess_per_chain.py <output_folder> [--stride N] [--chains A,B] [--csv out.csv] | |
| The backrub_trajectory_stride is read back out of each struct.db3, so --stride is only needed | |
| for databases that do not record it. It affects the checkpoint labels, not any energy. | |
| """ | |
| import argparse | |
| import glob | |
| import os | |
| import re | |
| import sqlite3 | |
| import subprocess | |
| import sys | |
| import numpy as np | |
| import pandas as pd | |
| import flex_ddg_db3 | |
| rosetta_scripts_path = os.path.expanduser('~/rosetta/source/bin/rosetta_scripts') | |
| # Must match the score function the original run used. | |
| rosetta_flags = [ | |
| '-restore_talaris_behavior', | |
| '-in:file:fullatom', | |
| '-out:nooutput', | |
| ] | |
| struct_db3_name = 'struct.db3' | |
| per_chain_db3_name = 'per_chain.db3' | |
| # flex ddG writes three poses per checkpoint, in this order. | |
| pose_order = ['backrub', 'wt', 'mut'] | |
| def chains_in_struct_db3(struct_db3): | |
| conn = sqlite3.connect(struct_db3) | |
| try: | |
| chains = [row[0] for row in conn.execute( | |
| 'SELECT DISTINCT chain_id FROM residue_pdb_identification ORDER BY chain_id')] | |
| finally: | |
| conn.close() | |
| return [c for c in chains if c and c.strip()] | |
| def write_rescore_protocol(chains, out_path, scorefxn='fa_talaris2014'): | |
| """Emit a RosettaScripts protocol that isolates and rescores each chain in turn.""" | |
| lowered = [c.lower() for c in chains] | |
| if len(set(lowered)) != len(lowered): | |
| raise ValueError('Chain IDs differ only by case, which collides in batch names: %s' % chains) | |
| selectors = '\n'.join( | |
| ' <Chain name="chain_%s" chains="%s"/>\n' | |
| ' <Not name="not_chain_%s" selector="chain_%s"/>' % (c, c, c, c) for c in chains) | |
| movers = '\n'.join( | |
| ' <DeleteRegionMover name="isolate_chain_%s" residue_selector="not_chain_%s"/>\n' | |
| ' <ReportToDB name="chain_%s_report" batch_description="per_chain" database_name="%s">\n' | |
| ' <ScoreTypeFeatures/>\n' | |
| ' <ScoreFunctionFeatures scorefxn="%s"/>\n' | |
| ' <StructureScoresFeatures scorefxn="%s"/>\n' | |
| ' </ReportToDB>' % (c, c, c, per_chain_db3_name, scorefxn, scorefxn) for c in chains) | |
| steps = [' <Add mover_name="save_full"/>'] | |
| for i, c in enumerate(chains): | |
| if i > 0: | |
| steps.append(' <Add mover_name="restore_full"/>') | |
| steps.append(' <Add mover_name="isolate_chain_%s"/>' % c) | |
| steps.append(' <Add mover_name="chain_%s_report"/>' % c) | |
| xml = '''<ROSETTASCRIPTS> | |
| <SCOREFXNS> | |
| <ScoreFunction name="%s" weights="talaris2014"/> | |
| </SCOREFXNS> | |
| <RESIDUE_SELECTORS> | |
| %s | |
| </RESIDUE_SELECTORS> | |
| <MOVERS> | |
| <SavePoseMover name="save_full" reference_name="full_pose" restore_pose="0"/> | |
| <SavePoseMover name="restore_full" reference_name="full_pose" restore_pose="1"/> | |
| %s | |
| </MOVERS> | |
| <PROTOCOLS> | |
| %s | |
| </PROTOCOLS> | |
| <OUTPUT /> | |
| </ROSETTASCRIPTS> | |
| ''' % (scorefxn, selectors, movers, '\n'.join(steps)) | |
| with open(out_path, 'w') as f: | |
| f.write(xml) | |
| return out_path | |
| def rescore_one(struct_db3, protocol_path): | |
| """Run the isolate-and-rescore protocol on one struct.db3, writing per_chain.db3 beside it.""" | |
| working_dir = os.path.dirname(os.path.abspath(struct_db3)) | |
| out_db3 = os.path.join(working_dir, per_chain_db3_name) | |
| if os.path.isfile(out_db3): | |
| os.remove(out_db3) | |
| args = [ | |
| os.path.abspath(rosetta_scripts_path), | |
| '-inout:dbms:database_name', struct_db3_name, | |
| '-in:use_database', | |
| '-parser:protocol', os.path.abspath(protocol_path), | |
| ] + rosetta_flags | |
| log_path = os.path.join(working_dir, 'per_chain_rescore.log') | |
| with open(log_path, 'w') as log: | |
| proc = subprocess.Popen(args, stdout=log, stderr=subprocess.STDOUT, cwd=working_dir) | |
| returncode = proc.wait() | |
| if returncode != 0 or not os.path.isfile(out_db3): | |
| raise RuntimeError('Rescoring failed for %s -- see %s' % (struct_db3, log_path)) | |
| return out_db3 | |
| def read_per_chain_db3(per_chain_db3, struct_number, case_name, stride): | |
| conn = sqlite3.connect(per_chain_db3) | |
| df = pd.read_sql_query(''' | |
| SELECT batches.name AS batch, structures.tag AS tag, structure_scores.score_value AS energy | |
| FROM structure_scores | |
| INNER JOIN structures ON structures.struct_id=structure_scores.struct_id | |
| INNER JOIN batches ON batches.batch_id=structure_scores.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 | |
| WHERE score_types.score_type_name="total_score" | |
| ''', conn) | |
| conn.close() | |
| # batch name is "chain_<X>_report"; tag is "<original struct.db3 struct_id>_0001" | |
| df['chain'] = df['batch'].apply(lambda b: b[len('chain_'):-len('_report')]) | |
| original_id = df['tag'].apply(lambda t: int(re.match(r'(\d+)', t).group(1))) | |
| df['pose'] = original_id.apply(lambda i: pose_order[(i - 1) % len(pose_order)]) | |
| df['backrub_steps'] = original_id.apply(lambda i: stride * (((i - 1) // len(pose_order)) + 1)) | |
| df['struct_num'] = struct_number | |
| df['case_name'] = case_name | |
| return df[['case_name', 'struct_num', 'backrub_steps', 'chain', 'pose', 'energy']] | |
| def main(): | |
| parser = argparse.ArgumentParser(description=__doc__, | |
| formatter_class=argparse.RawDescriptionHelpFormatter) | |
| parser.add_argument('output_folder', help='flex ddG output folder (e.g. "output")') | |
| parser.add_argument('--stride', type=int, default=None, | |
| help='override backrub_trajectory_stride instead of reading it from each ' | |
| 'struct.db3. Affects checkpoint labels only, not any energy.') | |
| parser.add_argument('--chains', default=None, | |
| help='comma-separated chains (default: auto-detect from struct.db3)') | |
| parser.add_argument('--csv', default=None, help='write the full per-structure table here') | |
| parser.add_argument('--reuse', action='store_true', | |
| help='skip Rosetta where per_chain.db3 already exists') | |
| args = parser.parse_args() | |
| if not os.path.isfile(rosetta_scripts_path): | |
| sys.exit('ERROR: set rosetta_scripts_path to your compiled rosetta_scripts binary') | |
| struct_db3s = sorted(glob.glob(os.path.join(args.output_folder, '*', '*', struct_db3_name))) | |
| if not struct_db3s: | |
| sys.exit('ERROR: no %s found under %s' % (struct_db3_name, args.output_folder)) | |
| print('Found %d %s files' % (len(struct_db3s), struct_db3_name)) | |
| chains = args.chains.split(',') if args.chains else chains_in_struct_db3(struct_db3s[0]) | |
| print('Chains: %s' % ', '.join(chains)) | |
| protocol_path = os.path.join(args.output_folder, 'per_chain_rescore.generated.xml') | |
| write_rescore_protocol(chains, protocol_path) | |
| print('Wrote protocol %s\n' % protocol_path) | |
| frames = [] | |
| for i, struct_db3 in enumerate(struct_db3s, start=1): | |
| struct_dir = os.path.dirname(struct_db3) | |
| case_name = os.path.basename(os.path.dirname(struct_dir)) | |
| struct_number = os.path.basename(struct_dir) | |
| out_db3 = os.path.join(struct_dir, per_chain_db3_name) | |
| if args.reuse and os.path.isfile(out_db3): | |
| print(' [%d/%d] %s (reusing)' % (i, len(struct_db3s), struct_dir)) | |
| else: | |
| print(' [%d/%d] %s' % (i, len(struct_db3s), struct_dir)) | |
| out_db3 = rescore_one(struct_db3, protocol_path) | |
| stride = args.stride | |
| if stride is None: | |
| stride = flex_ddg_db3.trajectory_stride_from_db3(struct_db3) | |
| if stride is None: | |
| stride = 5 | |
| print(' WARNING: %s does not record backrub_trajectory_stride; assuming %d. ' | |
| 'Pass --stride to label the checkpoints correctly.' % (struct_db3, stride)) | |
| frames.append(read_per_chain_db3(out_db3, struct_number, case_name, stride)) | |
| per_structure = pd.concat(frames) | |
| wide = per_structure[per_structure['pose'].isin(['wt', 'mut'])].pivot_table( | |
| index=['case_name', 'chain', 'backrub_steps', 'struct_num'], | |
| columns='pose', values='energy').reset_index() | |
| wide['ddG'] = wide['mut'] - wide['wt'] | |
| if args.csv: | |
| wide.to_csv(args.csv, index=False) | |
| print('\nWrote %s' % args.csv) | |
| summary = wide.groupby(['case_name', 'chain', 'backrub_steps']).agg( | |
| nstruct=('ddG', 'size'), | |
| wt_intra=('wt', 'mean'), | |
| mut_intra=('mut', 'mean'), | |
| ddG=('ddG', 'mean'), | |
| ddG_sd=('ddG', 'std'), | |
| ).reset_index() | |
| summary['ddG_sem'] = summary['ddG_sd'] / np.sqrt(summary['nstruct']) | |
| print('\n=== per-chain intramolecular ddG ===') | |
| print(summary.round(4).to_string(index=False)) | |
| print('\nA chain you did NOT mutate should read ~0 within ddG_sem.') | |
| print('This is bound-conformation strain, not a folding ddG (see the module docstring).') | |
| if __name__ == '__main__': | |
| main() | |