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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ dependencies = [
"requests",
"scipy",
"jsonschema",
"natsort",
]
requires-python = ">=3.10,<3.14"

Expand Down
76 changes: 54 additions & 22 deletions src/fairmd/lipids/auxiliary/opconvertor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@

import re

from natsort import natsorted

from fairmd.lipids.molecules import Lipid


class NamingRegistry:
"""Registry for naming conventions."""

_registry: dict = {}
_registry: list = []

@classmethod
def _register(cls, name: str, func) -> None:
Expand All @@ -22,18 +24,30 @@ def _register(cls, name: str, func) -> None:
:param name: Name of the fragment.
:param func: Function implementing the convention.
"""
cls._registry[name] = func
cls._registry += [(name, func)]

@classmethod
def apply(cls, opdic: dict):
"""Apply a naming convention functions.
def apply(cls, opdic: dict) -> None:
"""Make whatever required to apply naming conventions to fragmentized dictionary.

:param opdic: Fragmented dictionary.
:return: Function implementing the convention.
"""
if not cls._registry:
cls._initialize()
for frag_name, func in cls._registry.items():
cls._apply_naming(opdic)
cls._apply_sorting(opdic)

@classmethod
def _apply_sorting(cls, opdic: dict) -> None:
"""Sort every fragment list by C atom number."""
for frag_name in opdic:
opdic[frag_name] = natsorted(opdic[frag_name], key=lambda x: x["C"] + "__" + x["H"])

@classmethod
def _apply_naming(cls, opdic: dict) -> None:
"""Apply naming conventions to the fragmented dictionary."""
for frag_func in cls._registry:
frag_name, func = frag_func
if frag_name in opdic:
for i in range(len(opdic[frag_name])):
opdic[frag_name][i] = func(opdic[frag_name][i])
Expand All @@ -45,37 +59,54 @@ def apply(cls, opdic: dict):
# initialize the registry
@classmethod
def _initialize(cls):
def _snX_c_renamer(row: dict) -> dict:
def _snX_c_renamer(row: dict) -> dict: # noqa: N802
match = re.match(r"M_G[12]C([0-9]{1,2})_M", row["C"])
if not match or len(match.groups()) < 1:
raise ValueError(f"Unexpected C format: {row['C']}")
idx = int(match[1])
row["C"] = str(idx - 1)
if match and len(match.groups()) == 1:
idx = int(match[1])
row["C"] = str(idx - 1)
return row

cls._register("sn-1", _snX_c_renamer)
cls._register("sn-2", _snX_c_renamer)

def _gbb_c_renamer(row: dict) -> dict:
match = re.match(r"M_G([1-3])_M", row["C"])
if not match or len(match.groups()) < 1:
raise ValueError(f"Unexpected C format: {row['C']}")
idx = int(match[1])
row["C"] = f"g{idx}"
if match and len(match.groups()) == 1:
idx = int(match[1])
row["C"] = f"g{idx}"
return row

cls._register("glycerol backbone", _gbb_c_renamer)

def _h_renamer(row: dict) -> dict:
match = re.match(r"M_.+H([1-4])", row["H"])
if not match or len(match.groups()) < 1:
raise ValueError(f"Unexpected H format: {row['H']}")
idx = int(match[1])
row["H"] = str(idx)
def _headgroup_c_renamer(row: dict) -> dict:
if row["C"] == "M_G3C4_M":
row["C"] = "α"
elif row["C"] == "M_G3C5_M":
row["C"] = "β"
elif re.match(r"M_G3N6C[1-3]_M", row["C"]):
row["C"] = "γ"
return row

cls._register("headgroup", _headgroup_c_renamer)

def _h_renamer(row: dict) -> dict:
match = re.match(r"M_.+H([1-4])_M", row["H"])
if match and len(match.groups()) == 1:
idx = int(match[1])
row["H"] = str(idx)
return row

cls._register("_all_", _h_renamer)

def _plain_c_renamer(row: dict) -> dict:
match = re.match(r"M_C([0-9]+)_M", row["C"])
if match and len(match.groups()) == 1:
idx = int(match[1])
row["C"] = str(idx)
return row

cls._register("_all_", _plain_c_renamer)


def build_nice_OPdict(src: dict, lipid: Lipid) -> dict:
"""Build nicely formatted OP dictionary from raw OP data.
Expand All @@ -97,7 +128,8 @@ def _fragmentize(src: dict, mdict: dict) -> dict:
for apair, opvals in src.items():
atom_c, atom_h = apair.split(" ")
if atom_c not in mdict:
raise ValueError(f"Atom {atom_c} not found in mapping dictionary.")
msg = f"Atom {atom_c} not found in mapping dictionary."
raise ValueError(msg)
frag_c = mdict[atom_c].get("FRAGMENT", "total")
if frag_c not in r:
r[frag_c] = []
Expand Down
31 changes: 31 additions & 0 deletions tests/test_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@ def test_build_fragmented(self, systems):
assert "sn-1" in rdict
assert "sn-2" in rdict # fragments at the top level

def test_mockdicts(self):
from fairmd.lipids.auxiliary.opconvertor import build_nice_OPdict
from fairmd.lipids.molecules import Lipid

lipid = Lipid("POPE")
lipid.register_mapping()
mock_opdata = {
"M_G1C3_M M_G1C3H2_M": [0.1, 0.01, 0.001],
"M_G1C3_M M_G1C3H1_M": [0.1, 0.01, 0.001],
"M_G1C4_M M_G1C4H1_M": [0.2, 0.02, 0.002],
"M_G1C4_M M_G1C4H2_M": [0.2, 0.02, 0.002],
"M_G1C5_M M_G1C5H1_M": [0.3, 0.03, 0.003],
}
rdict = build_nice_OPdict(mock_opdata, lipid)
check.is_in("sn-1", rdict)
check.is_not_in("sn-2", rdict)
check.equal(len(rdict["sn-1"]), 5)
h_order = [int(x["H"]) for x in rdict["sn-1"]]
check.is_true(h_order[:6] == [1, 2, 1, 2, 1], "sn-1 H ordering is not sorted")

def test_cnames_pl(self, systems):
from fairmd.lipids.auxiliary.opconvertor import build_nice_OPdict
from fairmd.lipids.api import get_OP
Expand All @@ -66,10 +86,21 @@ def has_c(cname: str, flist: dict) -> bool:
check.is_true(has_c("16", rdict["sn-1"]))
check.is_false(has_c("1", rdict["sn-1"]))
check.is_false(has_c("17", rdict["sn-1"]))
check.is_true(has_c("2", rdict["sn-2"]))
check.is_true(has_c("18", rdict["sn-2"]))
check.is_false(has_c("1", rdict["sn-2"]))
check.is_false(has_c("19", rdict["sn-2"]))
# H check names
check.is_true(all(_c["H"] in ["1", "2", "3"] for _c in rdict["sn-1"]))
check.is_true(all(_c["H"] in ["1", "2", "3"] for _c in rdict["sn-2"]))
# check backbone
check.is_true(has_c("g1", rdict["glycerol backbone"]))
check.is_true(has_c("g2", rdict["glycerol backbone"]))
check.is_true(has_c("g3", rdict["glycerol backbone"]))
# check headgroup
check.is_true(has_c("α", rdict["headgroup"]))
# check ordering
c_order = [int(x["C"]) for x in rdict["sn-1"]]
check.is_true(c_order == sorted(c_order), "sn-1 C ordering is not sorted")
h_order = [int(x["H"]) for x in rdict["sn-1"]]
check.is_true(h_order[:6] == [1, 2, 1, 2, 1, 2], "sn-1 H ordering is not sorted")