Skip to content
Merged
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
5 changes: 3 additions & 2 deletions src/geckopy/adapter/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,9 @@ class OkpParams(BaseModel):
method: str = Field(
default="CataPro",
description=(
"Predictor method: CataPro, CatPred, DLKcat, EITLEM, "
"KinForm-H, KinForm-L, UniKP (see GET /api/v1/methods/)."
"Predictor method: CataPro, CatPred, CatRange, DLKcat, EITLEM, "
"KinForm-H, KinForm-L, OmniESI, OmniESI-O2DENet, TurNuP, UniKP "
"(see GET /api/v1/methods/)."
),
)
handle_long_sequences: str = Field(
Expand Down
42 changes: 37 additions & 5 deletions src/geckopy/gather_kcats/open_kinetics_predictor/parse_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,34 @@

logger = logging.getLogger(__name__)

_KCAT_COLUMN_SUFFIX = "kcat (1/s)"
_RANGE_RE = re.compile(
r"^\s*([0-9.eE+-]+)\s+to\s+([0-9.eE+-]+)\s*(?:s\^-1)?\s*$"
)


def _kcat_value(text: str) -> float | None:
"""A prediction as a number in 1/s.

Plain numbers are returned as they are. CatRange predicts a class,
which OKP returns as its range (``"10 to 100 s^-1"``); it becomes the
geometric midpoint, or half the upper bound for the class starting at
0, the values OKP returned for these classes before. Anything else is
None.
"""
try:
return float(text)
except (TypeError, ValueError):
pass
match = _RANGE_RE.match(text or "")
if match is None:
return None
try:
low, high = float(match.group(1)), float(match.group(2))
except ValueError:
return None
return (low * high) ** 0.5 if low > 0 else high / 2

_OUTPUT_COLUMNS = [
"rxn_id", "source", "eccode", "substrates", "genes",
"kcat", "wildcard_level", "origin",
Expand All @@ -44,9 +72,10 @@ def parse_okp_output(
``ec.rxn_enz_mat`` and per-metabolite SMILES populated.
result
Either the CSV text or a path to the result file. Expected
columns include ``kcat (1/s)``, ``Source kcat``,
``Protein Sequence`` and ``Substrate`` (extra similarity
columns are ignored).
columns include ``kcat (1/s)`` (CatRange: ``Predicted kcat
range: kcat (1/s)``, a number or a class range such as
``10 to 100 s^-1``), ``Source kcat``, ``Protein Sequence`` and
``Substrate`` (extra similarity columns are ignored).

Returns
-------
Expand All @@ -71,15 +100,18 @@ def parse_okp_output(
df = pd.read_csv(io.StringIO(text), dtype=str, keep_default_na=False)
df.columns = [c.strip() for c in df.columns]

required = ["kcat (1/s)", "Source kcat", "Protein Sequence", "Substrate"]
kcat_columns = [c for c in df.columns if c.endswith(_KCAT_COLUMN_SUFFIX)]
required = ["Source kcat", "Protein Sequence", "Substrate"]
missing = [c for c in required if c not in df.columns]
if len(kcat_columns) != 1:
missing.insert(0, _KCAT_COLUMN_SUFFIX)
if missing:
raise ValueError(
f"OKP result is missing expected column(s): {missing}. "
f"Found: {list(df.columns)}"
)

numeric_kcat = pd.to_numeric(df["kcat (1/s)"], errors="coerce")
numeric_kcat = pd.to_numeric(df[kcat_columns[0]].map(_kcat_value), errors="coerce")
valid_mask = numeric_kcat.notna()
if not valid_mask.any():
raise ValueError(
Expand Down
31 changes: 31 additions & 0 deletions tests/test_open_kinetics_predictor.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,37 @@ def test_parse_no_numeric_kcat_raises():
parse_okp_output(_ec_model(), csv_text)


def test_parse_catrange_class_ranges():
"""CatRange's column holds class ranges; each becomes its geometric
midpoint, as OKP returned it before."""
csv_text = (
"Predicted kcat range: kcat (1/s),Source kcat,Extra Info kcat,Protein Sequence,Substrate\n"
"10 to 100 s^-1,Prediction from CatRange,Predicted range: 10 to 100 s^-1,S1,CCO\n"
)
df = parse_okp_output(_ec_model(), csv_text)
assert not df.empty
assert all(df["source"] == "OKP-CatRange")
assert df["kcat"].iloc[0] == pytest.approx(10 ** 1.5)


@pytest.mark.parametrize("text, expected", [
("12.5", 12.5),
("10 to 100 s^-1", 10 ** 1.5),
("0 to 1e-08 s^-1", 5e-9),
("1000 to 1e+08 s^-1", 10 ** 5.5),
("", None),
("NA", None),
])
def test_kcat_value(text, expected):
from geckopy.gather_kcats.open_kinetics_predictor.parse_output import _kcat_value

value = _kcat_value(text)
if expected is None:
assert value is None or value != value
else:
assert value == pytest.approx(expected)


def test_parse_missing_column_raises():
csv_text = "kcat (1/s),Protein Sequence\n1.0,S1\n"
with pytest.raises(ValueError, match="missing expected column"):
Expand Down
Loading