Repository: biopython/biopython

Issue:
how to get label residue numbering instead of author numbering in MMCIFParser?
### Setup

```$ python
Python 2.7.15 (default, May 16 2018, 17:50:09) 
[GCC 8.1.1 20180502 (Red Hat 8.1.1-1)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys; print(sys.version)
2.7.15 (default, May 16 2018, 17:50:09) 
[GCC 8.1.1 20180502 (Red Hat 8.1.1-1)]
>>> import platform; print(platform.python_implementation()); print(platform.platform())
CPython
Linux-4.17.17-300.Lyude.bz1477182.V5.fc28.x86_64-x86_64-with-fedora-28-Twenty_Eight
>>> import Bio; print(Bio.__version__)
1.72
```

### Expected behaviour

I would like to have label sequence numbering instead of author sequence numbering for residues when MMCIFParser is used. I understand that can not be a default, but have not found a way how to achieve that.

### Actual behaviour

Only author sequence numbering for residues is returned.

Would you consider path like this?

Add something like following into `Bio.PDB.StructureBuilder`:

```
SEQ_ID_FIELD = '_atom_site.auth_seq_id'
ASYM_ID_FIELD = '_atom_site.auth_asym_id'
```

so you can easily change that by subclassing from that builder. Then in the `Bio.PDB.MMCIFParser` we would of course need:

```
-seq_id_list = mmcif_dict["_atom_site.auth_seq_id"]
+seq_id_list = mmcif_dict[self._structure_builder.SEQ_ID_FIELD]
```

Also we would need some default in the parser so any existing builders would still work.

Or did I got the whole concept wrong?

I need to use "label" numbering because I'm integrating with some other tool which ises that.


Discussion:
Hello. Please is this a correct course of action?
If you need a quick (and not that robust) mapping between the two numberings you could create a dictionary that converts one to the other:
```python
from Bio.PDB.MMCIF2Dict import MMCIF2Dict

mmcif_dict = MMCIF2Dict(filepath)
auth_to_label = {}

for i, r in enumerate(mmcif_dict['_atom_site.auth_seq_id']):
    k = r + mmcif_dict['_atom_site.auth_asym_id'][i]
    if k not in auth_to_label:
        auth_to_label[k] = mmcif_dict['_atom_site.label_seq_id'][i]
```
and then pass the number and chain through the dictionary to convert it, e.g. "100A" goes to "100".
Another question is how to handle dots in `seq_id_list` :-(

Here https://gemmi.readthedocs.io/en/latest/cif-parser.html it says that:

> The dot and question mark in the example above are two null types. In the CIF spec: ? = unknown and . = not applicable. In mmCIF files . is used for mandatory items, ? for not mandatory.

So should I just `continue` on them later when doing:

```
for i in range(0, len(atom_id_list)):
    [...]
    int_resseq = int(seq_id_list[i])
```
Hello @jgreener64 . this would need to loading/parsing *.fif file twice if I understand it correctly. Also I quite like encapsulation structure returned by parser provides and have quite some code around it. For now I have created a class with parent being `MMCIFParser` from https://github.com/biopython/biopython/blob/master/Bio/PDB/MMCIFParser.py and simply redefined `def _build_structure(...)` with this change:

        ## if auth_seq_id is present, we use this.
        ## Otherwise label_seq_id is used.
        #if "_atom_site.auth_seq_id" in mmcif_dict:
        #    seq_id_list = mmcif_dict["_atom_site.auth_seq_id"]
        #else:
        #    seq_id_list = mmcif_dict["_atom_site.label_seq_id"]
        seq_id_list = mmcif_dict["_atom_site.label_seq_id"]

But I have no clue if that is correct. Also CIF files contain `.` in that list sometime and I do not know how to process that (see mine previous comment).
The `label_seq_id` is only given a number for polymer entities, e.g. proteins, so is assigned a `.` for cofactors and water. This makes sense as these entites have no concept of a sequence, though they do have `auth_seq_id`s as people would give them residue numbers in PDB files. Converting between them is non-trivial.

That code probably does what you want, yes. Though you are going to run into problems with the `.` as you say, as Biopython will want every residue-like object to have a unique residue ID.
I too would like to the option to disable author labeling. No insertion codes and incremental numbering makes it much easier to map to Uniprot, using the sequence ranges that SIFTS provides.

Also, `label_seq_id` is *already* used as a fallback in `MMCIFParser`, so `.` needs to be handled no matter what.

I have made a patch to `MMCIFParser` that adds two `__init__` parameters `auth_residues` and `auth_chains`, `True` by default. If disabled, `label_` records are used for residue ID and chain ID, respectively.

If you would like to have it, I can make a PR.

Hi @sjdv1982, that sounds like a reasonable approach. I'd be happy to review that PR if you find the time to do it.

Resolving patch:
diff --git a/Bio/PDB/MMCIFParser.py b/Bio/PDB/MMCIFParser.py
--- a/Bio/PDB/MMCIFParser.py
+++ b/Bio/PDB/MMCIFParser.py
@@ -20,7 +20,9 @@
 class MMCIFParser:
     """Parse a mmCIF file and return a Structure object."""
 
-    def __init__(self, structure_builder=None, QUIET=False):
+    def __init__(
+        self, structure_builder=None, auth_chains=True, auth_residues=True, QUIET=False
+    ):
         """Create a PDBParser object.
 
         The mmCIF parser calls a number of standard methods in an aggregated
@@ -30,6 +32,14 @@ def __init__(self, structure_builder=None, QUIET=False):
 
         Arguments:
          - structure_builder - an optional user implemented StructureBuilder class.
+         - auth_chains - True by default. If true, use the author chain IDs.
+           If false, use the re-assigned mmCIF chain IDs.
+         - auth_residues - True by default. If true, use the author residue numbering.
+           If false, use the mmCIF "label" residue numbering, which has no insertion
+           codes, and strictly increments residue numbers.
+           NOTE: Non-polymers such as water don't have a "label" residue number,
+           and will be skipped.
+
          - QUIET - Evaluated as a Boolean. If true, warnings issued in constructing
            the SMCRA data will be suppressed. If false (DEFAULT), they will be shown.
            These warnings might be indicative of problems in the mmCIF file!
@@ -43,6 +53,8 @@ def __init__(self, structure_builder=None, QUIET=False):
         # self.trailer = None
         self.line_counter = 0
         self.build_structure = None
+        self.auth_chains = bool(auth_chains)
+        self.auth_residues = bool(auth_residues)
         self.QUIET = bool(QUIET)
 
     # Public methods
@@ -138,7 +150,10 @@ def _build_structure(self, structure_id):
             element_list = mmcif_dict["_atom_site.type_symbol"]
         except KeyError:
             element_list = None
-        chain_id_list = mmcif_dict["_atom_site.auth_asym_id"]
+        if self.auth_chains:
+            chain_id_list = mmcif_dict["_atom_site.auth_asym_id"]
+        else:
+            chain_id_list = mmcif_dict["_atom_site.label_asym_id"]
         x_list = [float(x) for x in mmcif_dict["_atom_site.Cartn_x"]]
         y_list = [float(x) for x in mmcif_dict["_atom_site.Cartn_y"]]
         z_list = [float(x) for x in mmcif_dict["_atom_site.Cartn_z"]]
@@ -166,10 +181,14 @@ def _build_structure(self, structure_id):
         except KeyError:
             # no anisotropic B factors
             aniso_flag = 0
-        # if auth_seq_id is present, we use this.
-        # Otherwise label_seq_id is used.
-        if "_atom_site.auth_seq_id" in mmcif_dict:
-            seq_id_list = mmcif_dict["_atom_site.auth_seq_id"]
+
+        if self.auth_residues:
+            # if auth_seq_id is present, we use this.
+            # Otherwise label_seq_id is used.
+            if "_atom_site.auth_seq_id" in mmcif_dict:
+                seq_id_list = mmcif_dict["_atom_site.auth_seq_id"]
+            else:
+                seq_id_list = mmcif_dict["_atom_site.label_seq_id"]
         else:
             seq_id_list = mmcif_dict["_atom_site.label_seq_id"]
         # Now loop over atoms and build the structure
@@ -209,7 +228,23 @@ def _build_structure(self, structure_id):
             altloc = alt_list[i]
             if altloc in _unassigned:
                 altloc = " "
-            int_resseq = int(seq_id_list[i])
+            resseq = seq_id_list[i]
+            if resseq == ".":
+                # Non-existing residue ID
+                try:
+                    msg_resseq = mmcif_dict["_atom_site.auth_seq_id"][i]
+                    msg = "Non-existing residue ID in chain '{}', residue '{}'".format(
+                        chainid, msg_resseq
+                    )
+                except (KeyError, IndexError):
+                    msg = "Non-existing residue ID in chain '{}'".format(chainid)
+                warnings.warn(
+                    "PDBConstructionWarning: ",
+                    msg,
+                    PDBConstructionWarning,
+                )
+                continue
+            int_resseq = int(resseq)
             icode = icode_list[i]
             if icode in _unassigned:
                 icode = " "
@@ -305,7 +340,9 @@ def _build_structure(self, structure_id):
 class FastMMCIFParser:
     """Parse an MMCIF file and return a Structure object."""
 
-    def __init__(self, structure_builder=None, QUIET=False):
+    def __init__(
+        self, structure_builder=None, auth_chains=True, auth_residues=True, QUIET=False
+    ):
         """Create a FastMMCIFParser object.
 
         The mmCIF parser calls a number of standard methods in an aggregated
@@ -319,6 +356,14 @@ def __init__(self, structure_builder=None, QUIET=False):
 
         Arguments:
          - structure_builder - an optional user implemented StructureBuilder class.
+         - auth_chains - True by default. If true, use the author chain IDs.
+           If false, use the re-assigned mmCIF chain IDs.
+         - auth_residues - True by default. If true, use the author residue numbering.
+           If false, use the mmCIF "label" residue numbering, which has no insertion
+           codes, and strictly increments residue numbers.
+           NOTE: Non-polymers such as water don't have a "label" residue number,
+           and will be skipped.
+
          - QUIET - Evaluated as a Boolean. If true, warnings issued in constructing
            the SMCRA data will be suppressed. If false (DEFAULT), they will be shown.
            These warnings might be indicative of problems in the mmCIF file!
@@ -331,6 +376,8 @@ def __init__(self, structure_builder=None, QUIET=False):
 
         self.line_counter = 0
         self.build_structure = None
+        self.auth_chains = bool(auth_chains)
+        self.auth_residues = bool(auth_residues)
         self.QUIET = bool(QUIET)
 
     # Public methods
@@ -399,7 +446,10 @@ def _build_structure(self, structure_id, filehandle):
         except KeyError:
             element_list = None
 
-        chain_id_list = mmcif_dict["_atom_site.auth_asym_id"]
+        if self.auth_chains:
+            chain_id_list = mmcif_dict["_atom_site.auth_asym_id"]
+        else:
+            chain_id_list = mmcif_dict["_atom_site.label_asym_id"]
 
         x_list = [float(x) for x in mmcif_dict["_atom_site.Cartn_x"]]
         y_list = [float(x) for x in mmcif_dict["_atom_site.Cartn_y"]]
@@ -431,10 +481,13 @@ def _build_structure(self, structure_id, filehandle):
             # no anisotropic B factors
             aniso_flag = 0
 
-        # if auth_seq_id is present, we use this.
-        # Otherwise label_seq_id is used.
-        if "_atom_site.auth_seq_id" in mmcif_dict:
-            seq_id_list = mmcif_dict["_atom_site.auth_seq_id"]
+        if self.auth_residues:
+            # if auth_seq_id is present, we use this.
+            # Otherwise label_seq_id is used.
+            if "_atom_site.auth_seq_id" in mmcif_dict:
+                seq_id_list = mmcif_dict["_atom_site.auth_seq_id"]
+            else:
+                seq_id_list = mmcif_dict["_atom_site.label_seq_id"]
         else:
             seq_id_list = mmcif_dict["_atom_site.label_seq_id"]
 
@@ -466,7 +519,23 @@ def _build_structure(self, structure_id, filehandle):
             altloc = alt_list[i]
             if altloc in _unassigned:
                 altloc = " "
-            int_resseq = int(seq_id_list[i])
+            resseq = seq_id_list[i]
+            if resseq == ".":
+                # Non-existing residue ID
+                try:
+                    msg_resseq = mmcif_dict["_atom_site.auth_seq_id"][i]
+                    msg = "Non-existing residue ID in chain '{}', residue '{}'".format(
+                        chainid, msg_resseq
+                    )
+                except (KeyError, IndexError):
+                    msg = "Non-existing residue ID in chain '{}'".format(chainid)
+                warnings.warn(
+                    "PDBConstructionWarning: ",
+                    msg,
+                    PDBConstructionWarning,
+                )
+                continue
+            int_resseq = int(resseq)
             icode = icode_list[i]
             if icode in _unassigned:
                 icode = " "


Test patch: