Repository: biopython/biopython

Issue:
Prevent PDBIO from writing large structures not supported in PDB format
The PDB format specification imposes several limits on the number of atoms, residues, and chains that can be written to file. This is one of the reasons behind the move to the mmCIF format as the default structural data storage format. 

Since we have the ability to read both PDB and mmCIF structures, but only to write PDB files, there should be a check to prevent writing very large structures that break the format specifications: > 99.999 atoms, >9999 residues, >1 character chains, etc.


Discussion:
Good idea.
This is still an issue in Biopython 1.79. I have an input PDB file with >100k atoms (some of them with non-unique numbers). After reading it in via `Bio.PDB.PDBParser` and writing with `PDB.PDBIO` I get a PDB file with 6-digit atom numbers which break the PDB format. It would be great at least see a warning and, even better, having atoms with IDs >=100k somehow fitted into 5-digit field (maybe loosing excess left hand side digits).

**Edit**: Aforementioned problem might be circumvented by using:
```python
io = PDBIO()
io.set_structure(struct)
io.save('out.pdb', preserve_atom_numbering=True)
```
Good point indeed, this was sort of forgotten... thanks!

Resolving patch:
diff --git a/Bio/PDB/PDBIO.py b/Bio/PDB/PDBIO.py
--- a/Bio/PDB/PDBIO.py
+++ b/Bio/PDB/PDBIO.py
@@ -169,10 +169,6 @@ def _get_atom_line(
         else:
             record_type = "ATOM  "
 
-        # Ensure chain id isn't longer than 1 character
-        if len(chain_id) > 1:
-            raise ValueError(f"Chain ID must be of length 1: {chain_id!r} > 1")
-
         # Atom properties
 
         # Check if the atom serial number is an integer
@@ -189,6 +185,11 @@ def _get_atom_line(
                 " preserve_atom_numbering=False"
             )
 
+        if atom_number > 99999:
+            raise ValueError(
+                f"Atom serial number ('{atom_number}') exceeds PDB format limit."
+            )
+
         # Check if the element is valid, unknown (X), or blank
         if atom.element:
             element = atom.element.strip().upper()
@@ -295,7 +296,6 @@ def _get_atom_line(
             return _PQR_ATOM_FORMAT_STRING % args
 
     # Public methods
-
     def save(self, file, select=_select, write_end=True, preserve_atom_numbering=False):
         """Save structure to a file.
 
@@ -349,6 +349,9 @@ def save(self, file, select=_select, write_end=True, preserve_atom_numbering=Fal
                     if not select.accept_chain(chain):
                         continue
                     chain_id = chain.id
+                    if len(chain_id) > 1:
+                        e = f"Chain id ('{chain_id}') exceeds PDB format limit."
+                        raise PDBIOException(e)
 
                     # necessary for TER
                     # do not write TER if no residues were written
@@ -361,33 +364,39 @@ def save(self, file, select=_select, write_end=True, preserve_atom_numbering=Fal
                         hetfield, resseq, icode = residue.id
                         resname = residue.resname
                         segid = residue.segid
+                        resid = residue.id[1]
+                        if resid > 9999:
+                            e = f"Residue number ('{resid}') exceeds PDB format limit."
+                            raise PDBIOException(e)
+
                         for atom in residue.get_unpacked_list():
-                            if select.accept_atom(atom):
-                                chain_residues_written = 1
-                                model_residues_written = 1
-                                if preserve_atom_numbering:
-                                    atom_number = atom.serial_number
-
-                                try:
-                                    s = get_atom_line(
-                                        atom,
-                                        hetfield,
-                                        segid,
-                                        atom_number,
-                                        resname,
-                                        resseq,
-                                        icode,
-                                        chain_id,
-                                    )
-                                except Exception as err:
-                                    # catch and re-raise with more information
-                                    raise PDBIOException(
-                                        f"Error when writing atom {atom.full_id}"
-                                    ) from err
-                                else:
-                                    fhandle.write(s)
-                                    # inconsequential if preserve_atom_numbering is True
-                                    atom_number += 1
+                            if not select.accept_atom(atom):
+                                continue
+                            chain_residues_written = 1
+                            model_residues_written = 1
+                            if preserve_atom_numbering:
+                                atom_number = atom.serial_number
+
+                            try:
+                                s = get_atom_line(
+                                    atom,
+                                    hetfield,
+                                    segid,
+                                    atom_number,
+                                    resname,
+                                    resseq,
+                                    icode,
+                                    chain_id,
+                                )
+                            except Exception as err:
+                                # catch and re-raise with more information
+                                raise PDBIOException(
+                                    f"Error when writing atom {atom.full_id}"
+                                ) from err
+                            else:
+                                fhandle.write(s)
+                                # inconsequential if preserve_atom_numbering is True
+                                atom_number += 1
 
                     if chain_residues_written:
                         fhandle.write(


Test patch:
diff --git a/Tests/test_PDB_PDBIO.py b/Tests/test_PDB_PDBIO.py
--- a/Tests/test_PDB_PDBIO.py
+++ b/Tests/test_PDB_PDBIO.py
@@ -79,6 +79,44 @@ def test_pdbio_write_preserve_numbering(self):
         finally:
             os.remove(filename)
 
+    def test_pdbio_pdb_format_limits(self):
+        """Test raising error when structure cannot meet PDB format limits."""
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore", PDBConstructionWarning)
+            structure = self.parser.get_structure("example", "PDB/1A8O.pdb")
+
+        # Modify structure and check if parser raises an error
+        # Chain id
+        structure[0]["A"].id = "AA"
+        self.io.set_structure(structure)
+        filenumber, filename = tempfile.mkstemp()
+        os.close(filenumber)
+        with self.assertRaises(PDBIOException):
+            self.io.save(filename)
+        structure[0]["AA"].id = "A"
+        os.remove(filename)
+
+        # Residue id
+        het, ori, ins = structure[0]["A"][152].id
+        structure[0]["A"][152].id = (het, 10000, ins)
+        self.io.set_structure(structure)
+        filenumber, filename = tempfile.mkstemp()
+        os.close(filenumber)
+        with self.assertRaises(PDBIOException):
+            self.io.save(filename)
+        structure[0]["A"][10000].id = (het, ori, ins)
+        os.remove(filename)
+
+        # Atom id
+        structure[0]["A"][152]["CA"].serial_number = 1e6
+        self.io.set_structure(structure)
+        filenumber, filename = tempfile.mkstemp()
+        os.close(filenumber)
+        with self.assertRaises(PDBIOException):
+            # perserve_... must be True for exception to trigger
+            self.io.save(filename, preserve_atom_numbering=True)
+        os.remove(filename)
+
     def test_pdbio_write_auto_numbering(self):
         """Test writing PDB and do not preserve atom numbering."""
         self.io.set_structure(self.structure)