From 53a252cb0c9d2f69264bf6f8bfa585647ede8eae Mon Sep 17 00:00:00 2001 From: Martin Noble Date: Tue, 16 Jun 2026 13:50:07 +0100 Subject: [PATCH] to_mmcif: quote empty pdbx_strand_id in _struct_ref_seq When a structure has a blank chain name (e.g. a PDB file with no chain ID) and a DBREF, the _struct_ref_seq.pdbx_strand_id value is empty. It was added to the loop raw, so the writer emitted it as a zero-width token: the data row then had one value fewer than the loop has tags, making the whole mmCIF file unreadable (gemmi's own reader rejects it with "Wrong number of values in loop _struct_ref_seq", and strict third-party readers such as MMDB/clipper abort on it). Every sibling field in this row is already passed through a helper (string_or_qmark, string_or_dot, .str(), qchain); pdbx_strand_id was the only raw one. Wrap it in qchain(), matching the adjacent _struct_ref row's qchain(ent.name) and qchain's documented purpose (chain names, empty -> ''). The empty value now serialises as '' and the loop round-trips. Added a regression test that blanks a chain name on 1pfe and checks the written _struct_ref_seq re-reads cleanly. --- src/to_mmcif.cpp | 2 +- tests/test_mol.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/to_mmcif.cpp b/src/to_mmcif.cpp index a4f769e1..64e13eb0 100644 --- a/src/to_mmcif.cpp +++ b/src/to_mmcif.cpp @@ -721,7 +721,7 @@ void update_mmcif_block(const Structure& st, cif::Block& block, MmcifOutputGroup } seq_loop.add_row({std::to_string(++counter2), std::to_string(counter), - strand_id->second, // pdbx_strand_id + qchain(strand_id->second), // pdbx_strand_id id, label_begin.str(), label_end.str(), diff --git a/tests/test_mol.py b/tests/test_mol.py index 10f00577..064b6bdd 100644 --- a/tests/test_mol.py +++ b/tests/test_mol.py @@ -853,6 +853,23 @@ def test_remove2(self): ref_seq = doc[0].get_mmcif_category('_struct_ref_seq') self.assertEqual(ref_seq['pdbx_strand_id'], ['B']) + def test_empty_strand_id_struct_ref_seq(self): + # A blank chain name (e.g. a PDB file with no chain ID) gives an empty + # _struct_ref_seq.pdbx_strand_id. It must be written as a quoted empty + # string (''), not dropped: dropping it leaves the loop row one value + # short, which makes the whole file unreadable. + st = gemmi.read_structure(full_path('1pfe.cif.gz')) + for chain in st[0]: + if chain.name == 'B': + chain.name = '' + doc = st.make_mmcif_document() + strand_ids = doc[0].find_values('_struct_ref_seq.pdbx_strand_id') + self.assertEqual([gemmi.cif.as_string(v) for v in strand_ids], ['A', '']) + # Round-trip: writing then re-reading must not raise on a ragged loop. + reparsed = gemmi.cif.read_string(doc.as_string()) + self.assertEqual(reparsed[0].find_values('_struct_ref_seq.pdbx_strand_id') + .str(1), '') + def test_first_conformer(self): model = gemmi.read_structure(full_path('1pfe.cif.gz'))[0] b = model['B']