Source code for qm_atlas.wrappers.moka.blabber
# usage: ./blabber_sd.bin [options] <filename>
#
# <filename> can be a SD (.sd or .sdf) or MOL2 (.mol2) file
#
# options are:
# -h, --help display this help
# --version show version info
# --input-type=<sd|mol2> input file type (autodetect)
# -p <pH list>, --pH=<pH list> report most abundant species
# at given pH (7.4)
# -n, --neutralize report only the neutral species
# --load-model=<database> use a custom model database for predictions
# -o, --output=FILE output file name (SD only)
# --explicit-h=<mode> select hydrogens to keep on output (pristine)
# <mode> can be one of:
# * none: no hydrogens
# * pristine: only existing hydrogens
# * all: both existing and added hydrogens
# -m, --minimize perform full 3D minimization
# -t <abundance> report all species above the threshold
# -e, --equivalent-protomers generate all equivalent protomers
#
# <pH list> is a comma separated list of the following:
# * single value
# * range ( min-max )
# Examples:
# * single value: --pH=6
# * single range: --pH=6-10
# * multiple values: --pH=6,7,8
# * mixed: --pH=6,7-10
import logging
import pathlib
from collections.abc import Iterator
from pathlib import Path
import numpy as np
from ppqm import chembridge
from ppqm.utils.files import WorkDir
from rdkit.Chem import Mol
from qm_atlas import constants, utils
from qm_atlas.software_environment import SOFTWARE_CONFIG, USE_DEFAULT_MODEL, get_moka_model
from qm_atlas.wrappers.common import run_command
_logger = logging.getLogger(__name__)
BLABBER_CMD = "blabber_sd"
# pH
BLABBER_PH = 7.4
# abundance threshold
BLABBER_THRESHOLD = None
BLABBER_FILENAME = "_tmp_blabber.sdf"
COLUMN_ABUNDANCE = "ABUNDANCE"
[docs]
def get_protonation(
molobj: Mol,
scr: Path = constants.DEFAULT_SCR,
filename: str | Path = BLABBER_FILENAME,
remove_zero_charged: bool = False,
keep_files: bool = False,
**kwargs,
) -> list[Mol]:
"""Enumerate protonation states of *molobj* with MoKa Blabber and return them as molecules."""
filename = scr / filename
sdfstr = chembridge.molobj_to_sdfstr(molobj)
if sdfstr is None:
raise ValueError("Unable to generate SDF")
sdfs = get_protonation_from_sdf(sdfstr, scr=scr, keep_files=keep_files, **kwargs)
molobjs = [chembridge.sdfstr_to_molobj(sdf) for sdf in sdfs]
def _iterate_molobjs():
def _abs_charge(mol):
charges = chembridge.get_atom_charges(mol)
charges = np.array(charges)
charges = np.abs(charges)
return charges.sum()
for molobj in molobjs:
if molobj is None:
continue
value = (
molobj.GetProp(COLUMN_ABUNDANCE)
if molobj.HasProp(COLUMN_ABUNDANCE)
else float("nan")
)
if remove_zero_charged and _abs_charge(molobj) == 0:
continue
if isinstance(value, str) and "No species found" in value:
continue
if isinstance(value, str):
value = value.split("%")
value = value[0]
value = utils.as_float(value)
molobj.SetProp(COLUMN_ABUNDANCE, str(value))
yield molobj
# Filter the protonated molobjs
molobjs = [mol for mol in _iterate_molobjs()]
return molobjs
[docs]
def get_protonation_from_sdf(
sdfstr: str,
cmd: str = BLABBER_CMD,
scr: Path = constants.DEFAULT_SCR,
threshold: int | None = BLABBER_THRESHOLD,
model: Path | None = USE_DEFAULT_MODEL,
ph: float = BLABBER_PH,
use_tempfile: bool = True,
keep_files: bool = False,
) -> list[str]:
"""Run MoKa Blabber on an SDF string and return the protonated species as SDF strings."""
# Resolve model if sentinel value is used
if model is USE_DEFAULT_MODEL:
model = get_moka_model()
if use_tempfile:
temp = WorkDir(dir=scr, prefix="blabber_", keep=keep_files)
scr = pathlib.Path(temp.name)
# NOTE Filenames needs sdf suffix for blabber to read
filename = "_tmp_blabber.sdf"
filename_out = "_tmp_blabber.o.sdf"
# Create inputfile
with open(scr / filename, "w") as f:
f.write(sdfstr)
# Ensure empty output file
with open(scr / filename_out, "w") as f:
pass
args = [f"{cmd}"]
if model is not None:
args += [f"--load-model={model}"]
if ph is not None:
args += [f"-p {ph}"]
if threshold is not None:
args += [f"-t {threshold}"]
args += [f"-o {filename_out}"]
args += [f"{filename}"]
cmd = " ".join(args)
_logger.debug(f"{scr} {cmd}")
software_env_manager = SOFTWARE_CONFIG.get_environment_manager()
env = software_env_manager.get_run_environment("moka")
stdout, stderr = run_command(cmd, env=env, cwd=scr)
with open(scr / filename_out, "r") as f:
lines = f.readlines()
sdfs = generate_sdfs(lines)
sdfs = list(sdfs)
if len(sdfs) == 0:
stderr = stderr.split("\n")
for line in stderr:
line = line.strip()
if line == "":
continue
_logger.error(line)
return sdfs
[docs]
def generate_sdfs(lines: list[str]) -> Iterator[str]:
"""Generate SDF strings from lines of strings"""
# Stream the result in sdfs
sdf = []
for line in lines:
sdf.append(line)
if constants.SDFSEP in line:
sdf = "".join(sdf)
yield sdf
sdf = []
# Collect the last one, if no seperator is found
if len(sdf) > 1:
sdf = "".join(sdf)
yield sdf
return