File size: 10,670 Bytes
96272bc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 | #!/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()
|