Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions biopandas/mol2/pandas_mol2.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@

COLUMN_TYPES = (int, str, float, float, float, str, int, str, float)

BOND_COLUMN_NAMES = (
"bond_id",
"origin_atom_id",
"target_atom_id",
"bond_type",
"status_bits",
)

BOND_COLUMN_TYPES = ("int64", "int64", "int64", str, str)


class PandasMol2(object):
"""
Expand All @@ -35,6 +45,10 @@ class PandasMol2(object):
df : pandas.DataFrame
DataFrame of a Mol2's ATOM section

df_bonds : pandas.DataFrame or None
DataFrame of a Mol2's BOND section. `None` until
`parse_bonds()` has been called.

mol2_text : str
Mol2 file contents in string format

Expand All @@ -48,6 +62,7 @@ class PandasMol2(object):

def __init__(self):
self._df = None
self._df_bonds = None
self.mol2_text = ""
self.header = ""
self.code = ""
Expand All @@ -68,6 +83,23 @@ def df(self, value):
)
# self._df = value

@property
def df_bonds(self):
"""Accesses the pandas DataFrame of the BOND section.

`None` until `parse_bonds()` has been called.
"""
return self._df_bonds

@df_bonds.setter
def df_bonds(self, value):
"""Assign a new value to the BOND section DataFrame"""
raise AttributeError(
"Please use `PandasMol2._df_bonds = ... ` instead\n"
"of `PandasMol2.df_bonds = ... ` if you are sure that\n"
"you want to overwrite the `df_bonds` attribute."
)

def _load_mol2(self, mol2_lines, mol2_code, columns):
"""Load mol2 contents into assert_raise_message instance"""
if columns is None:
Expand All @@ -88,6 +120,38 @@ def _load_mol2(self, mol2_lines, mol2_code, columns):
self.code = mol2_code.decode()

self._df = self._construct_df(mol2_lines, col_names, col_types)
# Bonds from a previously loaded molecule no longer apply.
self._df_bonds = None

def parse_bonds(self):
"""Parses the BOND section of the loaded Mol2 into a DataFrame

Bonds are not parsed by `read_mol2` or `read_mol2_from_list`,
since many analyses don't need them. Call this method after
reading a molecule to parse them. The result is also stored
in `PandasMol2.df_bonds`.

Returns
---------
pandas.DataFrame
DataFrame with one row per bond and the columns
'bond_id', 'origin_atom_id', 'target_atom_id' (int64),
'bond_type' and 'status_bits' (str). 'bond_type' is kept as
a string since Tripos bond types include non-numeric codes
('am', 'ar', 'du', 'un', 'nc'). 'status_bits' is the empty
string for bonds that don't specify it. A molecule without
a BOND section gives an empty DataFrame with these columns.

"""
if self._df is None:
raise AttributeError(
"No Mol2 structure loaded. Please read a Mol2 file "
"via `read_mol2` or `read_mol2_from_list` first."
)
self._df_bonds = self._bondsection_to_pandas(
self._get_bondsection(self.mol2_text.splitlines())
)
return self._df_bonds

def read_mol2(self, path, columns=None):
"""Reads Mol2 files (unzipped or gzipped) from local drive
Expand Down Expand Up @@ -207,6 +271,47 @@ def _atomsection_to_pandas(mol2_atom_lst, col_names, col_types):

return df

@staticmethod
def _get_bondsection(mol2_lst):
"""Returns the lines of the BOND section from a mol2 provided as a
list of strings, without blank and comment lines. Returns an empty
list if there is no BOND section."""
bond_lines = []
started = False
for s in mol2_lst:
if s.startswith("@<TRIPOS>"):
if started:
break
started = s.startswith("@<TRIPOS>BOND")
elif started:
s = s.strip()
if s and not s.startswith("#"):
bond_lines.append(s)
return bond_lines

@staticmethod
def _bondsection_to_pandas(mol2_bond_lst):
rows = []
for line in mol2_bond_lst:
fields = line.split()
try:
if len(fields) < 4:
raise ValueError
ids = [int(f) for f in fields[:3]]
except ValueError:
raise ValueError(
"Could not parse BOND record %r. Expected: bond_id "
"origin_atom_id target_atom_id (integers), bond_type "
"and optional status_bits" % line
) from None
rows.append(ids + [fields[3], " ".join(fields[4:])])

df = pd.DataFrame(rows, columns=list(BOND_COLUMN_NAMES))
for name, dtype in zip(BOND_COLUMN_NAMES, BOND_COLUMN_TYPES):
df[name] = df[name].astype(dtype)

return df

@staticmethod
def rmsd(df1, df2, heavy_only=True):
"""Compute the Root Mean Square Deviation between molecules
Expand Down
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The CHANGELOG for the current development version is available at

### Unreleased

- Add `PandasMol2.parse_bonds()` to parse the Mol2 BOND section into a DataFrame (`bond_id`, `origin_atom_id`, `target_atom_id`, `bond_type`, `status_bits`), also stored in the new `PandasMol2.df_bonds` attribute. Bonds are parsed on request only, so `read_mol2` is unchanged. Addresses issue [#54](https://github.com/BioPandas/biopandas/issues/54).

- Add `PandasPdb.to_fasta()` to export observed ATOM protein sequences by chain from an explicitly selected model, with configurable wrapping and optional file output.

- Fix: parse the PDB v3.3 atomic charge notation (columns 79-80, e.g. `2+`, `1-`) as integer charges instead of floats, and write it back in spec-compliant notation. Also restores consistent integer dtypes between the PDB and MMTF readers on Windows. Addresses issue [#151](https://github.com/BioPandas/biopandas/issues/151). PR [#160](https://github.com/BioPandas/biopandas/pull/160)
Expand Down
186 changes: 186 additions & 0 deletions docs/tutorials/Working_with_MOL2_Structures_in_DataFrames.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,192 @@
" Cu copper\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Parsing the BOND Section\n",
"\n",
"By default, `read_mol2` only parses the ATOM section, since many analyses don't need the bonds. To also get the bonds between the atoms, call `parse_bonds()` after reading a molecule. It returns a DataFrame of the BOND section, which is also stored in the `df_bonds` attribute:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>bond_id</th>\n",
" <th>origin_atom_id</th>\n",
" <th>target_atom_id</th>\n",
" <th>bond_type</th>\n",
" <th>status_bits</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>1</td>\n",
" <td>1</td>\n",
" <td>2</td>\n",
" <td>2</td>\n",
" <td></td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>2</td>\n",
" <td>1</td>\n",
" <td>3</td>\n",
" <td>1</td>\n",
" <td></td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>3</td>\n",
" <td>2</td>\n",
" <td>11</td>\n",
" <td>1</td>\n",
" <td></td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>4</td>\n",
" <td>3</td>\n",
" <td>10</td>\n",
" <td>2</td>\n",
" <td></td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>5</td>\n",
" <td>3</td>\n",
" <td>12</td>\n",
" <td>1</td>\n",
" <td></td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" bond_id origin_atom_id target_atom_id bond_type status_bits\n",
"0 1 1 2 2 \n",
"1 2 1 3 1 \n",
"2 3 2 11 1 \n",
"3 4 3 10 2 \n",
"4 5 3 12 1 "
]
},
"execution_count": null,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"pmol = PandasMol2().read_mol2('./data/1b5e_1.mol2')\n",
"pmol.parse_bonds()\n",
"pmol.df_bonds.head()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `origin_atom_id` and `target_atom_id` columns refer to the `atom_id` column of `pmol.df`. The `bond_type` column holds the Tripos bond type as a string, since besides `'1'`, `'2'` and `'3'` (single, double and triple bonds) it can be `'am'` (amide), `'ar'` (aromatic), `'du'` (dummy), `'un'` (unknown) or `'nc'` (not connected). `status_bits` is the empty string for bonds that don't specify it.\n",
"\n",
"Since the bonds are a regular DataFrame, they can be combined with the atoms using pandas, for example to list the atom names of the amide bonds:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>bond_id</th>\n",
" <th>origin_atom_id</th>\n",
" <th>target_atom_id</th>\n",
" <th>bond_type</th>\n",
" <th>status_bits</th>\n",
" <th>origin_atom_name</th>\n",
" <th>target_atom_name</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>7</td>\n",
" <td>4</td>\n",
" <td>11</td>\n",
" <td>am</td>\n",
" <td></td>\n",
" <td>C4</td>\n",
" <td>N2</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" bond_id origin_atom_id ... origin_atom_name target_atom_name\n",
"6 7 4 ... C4 N2\n",
"\n",
"[1 rows x 7 columns]"
]
},
"execution_count": null,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"atom_names = pmol.df.set_index('atom_id')['atom_name']\n",
"amide_bonds = pmol.df_bonds[pmol.df_bonds['bond_type'] == 'am'].copy()\n",
"amide_bonds['origin_atom_name'] = amide_bonds['origin_atom_id'].map(atom_names)\n",
"amide_bonds['target_atom_name'] = amide_bonds['target_atom_id'].map(atom_names)\n",
"amide_bonds"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
Loading