From c746d425b9f9059c9239ffa3eaa2503f69d9e331 Mon Sep 17 00:00:00 2001 From: Tim Dudgeon Date: Wed, 10 Jun 2026 09:49:48 +0100 Subject: [PATCH 1/3] Add pose IDs to mmCIF keywords via $PoseID token substitution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reads the Fragalysis download metadata.csv (column 1 = pose code, column 3 = crystal name) to build a crystal→pose-codes mapping, then substitutes the $PoseID token wherever it appears in the mmcif-gen template output (handles both loop items and key-value pairs). Closes #81. Co-Authored-By: Claude Sonnet 4.6 --- src/pdbdepo/pdb_deposition.py | 42 +++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/pdbdepo/pdb_deposition.py b/src/pdbdepo/pdb_deposition.py index a2a33c7..e78aeab 100644 --- a/src/pdbdepo/pdb_deposition.py +++ b/src/pdbdepo/pdb_deposition.py @@ -101,6 +101,7 @@ def process_input( mmcifgen_block, output_dir: Path, cmpd_codes_dict: dict = {}, + pose_ids_dict: dict = {}, debug=False, ): software_templates = read_software_templates() @@ -276,6 +277,10 @@ def process_input( mmcifgen_refine_values, ) + # build the comma-separated pose ID string for this crystal + pose_ids = pose_ids_dict.get(xtal_name, []) + pose_id_str = ', '.join(pose_ids) + # add in the common metadata (generated by mmcif-gen) for item in mmcifgen_block: to_add = True @@ -287,6 +292,7 @@ def process_input( template = item.loop.values[1] template = template.replace('$CompoundCode', cmpd_code) template = template.replace('$CrystalName', xtal_name) + template = template.replace('$PoseID', pose_id_str) cmpd_codes = cmpd_codes_dict.get(xtal_name) if cmpd_codes: for i, cmpd_code in enumerate(cmpd_codes): @@ -296,6 +302,19 @@ def process_input( template = template.replace('$ExternalCode' + str(i), '') new_loop = structure_cif_block0.init_loop('', item.loop.tags) new_loop.add_row([item.loop.values[0], template]) + elif any('$PoseID' in v for v in item.loop.values): + to_add = False + ncols = len(item.loop.tags) + nrows = len(item.loop.values) // ncols + new_loop = structure_cif_block0.init_loop('', item.loop.tags) + for r in range(nrows): + row = [ + item.loop.values[r * ncols + c].replace('$PoseID', pose_id_str) for c in range(ncols) + ] + new_loop.add_row(row) + elif item.pair is not None and '$PoseID' in item.pair[1]: + structure_cif_block0.set_pair(item.pair[0], item.pair[1].replace('$PoseID', pose_id_str)) + to_add = False if to_add: # otherwise add the item as it is added_item = structure_cif_block0.add_item(item) @@ -1060,12 +1079,34 @@ def read_cmpd_codes(filename): return d +def read_fragalysis_csv(filename): + """Read Fragalysis metadata CSV and return a dict mapping crystal name -> list of pose codes. + + The CSV has pose code in column 1 (index 0) and crystal/experiment code in column 3 (index 2). + """ + d = {} + if filename: + with open(filename, 'rt') as f: + f.readline() # skip header + for line in f: + tokens = line.split(',') + if len(tokens) >= 3: + pose_code = tokens[0].strip() + crystal_name = tokens[2].strip() + if crystal_name not in d: + d[crystal_name] = [] + d[crystal_name].append(pose_code) + info('read pose IDs for', len(d), 'crystals') + return d + + def run(collator_path, metadata_csv, compound_codes_csv=None, fragalysis_csv=None, debug=False): info('run on ' + str(datetime.datetime.now())) info('using RDKit version ' + rdBase.rdkitVersion) # info('using InCHI version ' + Chem.GetInchiVersion()) cmpd_codes_dict = read_cmpd_codes(compound_codes_csv) + pose_ids_dict = read_fragalysis_csv(fragalysis_csv) output_dir_p = collator_path / 'pdb_depo_files' @@ -1101,6 +1142,7 @@ def run(collator_path, metadata_csv, compound_codes_csv=None, fragalysis_csv=Non meta_mmcifgen, output_dir_p, cmpd_codes_dict=cmpd_codes_dict, + pose_ids_dict=pose_ids_dict, debug=debug, ) From 198d29bd777d3e33bea1e3171ac2e0dce7a4a73e Mon Sep 17 00:00:00 2001 From: Tim Dudgeon Date: Wed, 10 Jun 2026 09:54:17 +0100 Subject: [PATCH 2/3] Rename loop row variable to avoid shadowing soakdb row variable Co-Authored-By: Claude Sonnet 4.6 --- src/pdbdepo/pdb_deposition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pdbdepo/pdb_deposition.py b/src/pdbdepo/pdb_deposition.py index e78aeab..f4f1d52 100644 --- a/src/pdbdepo/pdb_deposition.py +++ b/src/pdbdepo/pdb_deposition.py @@ -308,10 +308,10 @@ def process_input( nrows = len(item.loop.values) // ncols new_loop = structure_cif_block0.init_loop('', item.loop.tags) for r in range(nrows): - row = [ + loop_row = [ item.loop.values[r * ncols + c].replace('$PoseID', pose_id_str) for c in range(ncols) ] - new_loop.add_row(row) + new_loop.add_row(loop_row) elif item.pair is not None and '$PoseID' in item.pair[1]: structure_cif_block0.set_pair(item.pair[0], item.pair[1].replace('$PoseID', pose_id_str)) to_add = False From 702c32db96c350871fbba1c14fdf0d4b3cae3e64 Mon Sep 17 00:00:00 2001 From: Tim Dudgeon Date: Wed, 10 Jun 2026 10:00:40 +0100 Subject: [PATCH 3/3] Fix fragalysis CSV parsing to handle quoted fields The CSV uses double-quoted fields so simple split(',') left quotes in the parsed values, preventing dict lookups from matching crystal names. Switch to csv.reader which strips the quotes correctly. Co-Authored-By: Claude Sonnet 4.6 --- src/pdbdepo/pdb_deposition.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pdbdepo/pdb_deposition.py b/src/pdbdepo/pdb_deposition.py index f4f1d52..e0e7c3b 100644 --- a/src/pdbdepo/pdb_deposition.py +++ b/src/pdbdepo/pdb_deposition.py @@ -13,6 +13,7 @@ import argparse import bz2 +import csv import datetime import glob import re @@ -1086,10 +1087,10 @@ def read_fragalysis_csv(filename): """ d = {} if filename: - with open(filename, 'rt') as f: - f.readline() # skip header - for line in f: - tokens = line.split(',') + with open(filename, 'rt', newline='') as f: + reader = csv.reader(f) + next(reader) # skip header + for tokens in reader: if len(tokens) >= 3: pose_code = tokens[0].strip() crystal_name = tokens[2].strip()