Skip to content

Commit 897a9d9

Browse files
authored
Add naming.yaml for molecule (#470)
* Start molecule refactoring. First failed fragments test. (cherry picked from commit 2d9b53b726560251dfdc7fdb8398dc5b07da58ac) * molecule can live with old-style mapping or with mapping divided into mapping and naming. It will allow us to migrate gradually to new mapping structure. (cherry picked from commit 453170de7b67f3444052b802602f0fa82a30aac4)
1 parent 3271937 commit 897a9d9

5 files changed

Lines changed: 286 additions & 32 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
NMRlipids:
2+
id: BOG
3+
name: beta-octyl D-glucopyranoside
4+
charge: 0
5+
sameAs:
6+
ChEBI: CHEBI:41128
7+
pubchem.compound: 62852
8+
metabolights: MTBLC41128
9+
pdb.ligand: BOG
10+
ChEMBL: CHEMBL446037
11+
bioschema_properties:
12+
iupacName: (2R,3S,4S,5R,6R)-2-(hydroxymethyl)-6-octoxyoxane-3,4,5-triol
13+
smiles: CCCCCCCCO[C@H]1[C@@H]([C@H]([C@@H]([C@H](O1)CO)O)O)O
14+
inChI: InChI=1S/C14H28O6/c1-2-3-4-5-6-7-8-19-14-13(18)12(17)11(16)10(9-15)20-14/h10-18H,2-9H2,1H3/t10-,11-,12+,13-,14-/m1/s1
15+
inChIKey: HEGSGKPQLMEBJL-RKQHYHRCSA-N
16+
molecularFormula: C14H28O6
17+
molecularWeight: 292.37
18+
name: (2R,3S,4S,5R,6R)-2-(hydroxymethyl)-6-octoxyoxane-3,4,5-triol
19+
image: https://www.ebi.ac.uk/chembl/api/data/image/CHEMBL446037?dimensions=200
20+
alternateName:
21+
- β-octylglucoside
22+
- 1-octyl-β-D-glucopyranoside
23+
- 1-O-n-octyl-β-D-glucopyranoside
24+
- β-D-octyl glucoside
25+
- 1-O-octyl-β-D-glucopyranoside
26+
- octyl-β-D-glucoside
27+
- Oct β-Glc
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
M_G0C1_M:
2+
FRAGMENT: glucose
3+
SMILEIDX: 9
4+
M_G0C1H1_M:
5+
FRAGMENT: glucose
6+
M_G0C1O1_M:
7+
FRAGMENT: glucose
8+
SMILEIDX: 8
9+
M_G0C2_M:
10+
FRAGMENT: glucose
11+
SMILEIDX: 10
12+
M_G0C2H1_M:
13+
FRAGMENT: glucose
14+
M_G0C2O1_M:
15+
FRAGMENT: glucose
16+
SMILEIDX: 19
17+
M_G0C2O1H1_M:
18+
FRAGMENT: glucose
19+
M_G0C3_M:
20+
FRAGMENT: glucose
21+
SMILEIDX: 11
22+
M_G0C3H1_M:
23+
FRAGMENT: glucose
24+
M_G0C3O1_M:
25+
FRAGMENT: glucose
26+
SMILEIDX: 18
27+
M_G0C3O1H1_M:
28+
FRAGMENT: glucose
29+
M_G0C4_M:
30+
FRAGMENT: glucose
31+
SMILEIDX: 12
32+
M_G0C4H1_M:
33+
FRAGMENT: glucose
34+
M_G0C4O1_M:
35+
FRAGMENT: glucose
36+
SMILEIDX: 17
37+
M_G0C4O1H1_M:
38+
FRAGMENT: glucose
39+
M_G0C5_M:
40+
FRAGMENT: glucose
41+
SMILEIDX: 13
42+
M_G0C5H1_M:
43+
FRAGMENT: glucose
44+
M_G0C5O1_M:
45+
FRAGMENT: glucose
46+
SMILEIDX: 14
47+
M_G0C6_M:
48+
FRAGMENT: glucose
49+
SMILEIDX: 15
50+
M_G0C6H1_M:
51+
FRAGMENT: glucose
52+
M_G0C6H2_M:
53+
FRAGMENT: glucose
54+
M_G0C6O1_M:
55+
FRAGMENT: glucose
56+
SMILEIDX: 16
57+
M_G0C6O1H1_M:
58+
FRAGMENT: glucose
59+
M_C1_M:
60+
FRAGMENT: tail
61+
SMILEIDX: 7
62+
M_C1H1_M:
63+
FRAGMENT: tail
64+
M_C1H2_M:
65+
FRAGMENT: tail
66+
M_C2_M:
67+
FRAGMENT: tail
68+
SMILEIDX: 6
69+
M_C2H1_M:
70+
FRAGMENT: tail
71+
M_C2H2_M:
72+
FRAGMENT: tail
73+
M_C3_M:
74+
FRAGMENT: tail
75+
SMILEIDX: 5
76+
M_C3H1_M:
77+
FRAGMENT: tail
78+
M_C3H2_M:
79+
FRAGMENT: tail
80+
M_C4_M:
81+
FRAGMENT: tail
82+
SMILEIDX: 4
83+
M_C4H1_M:
84+
FRAGMENT: tail
85+
M_C4H2_M:
86+
FRAGMENT: tail
87+
M_C5_M:
88+
FRAGMENT: tail
89+
SMILEIDX: 3
90+
M_C5H1_M:
91+
FRAGMENT: tail
92+
M_C5H2_M:
93+
FRAGMENT: tail
94+
M_C6_M:
95+
FRAGMENT: tail
96+
SMILEIDX: 2
97+
M_C6H1_M:
98+
FRAGMENT: tail
99+
M_C6H2_M:
100+
FRAGMENT: tail
101+
M_C7_M:
102+
FRAGMENT: tail
103+
SMILEIDX: 1
104+
M_C7H1_M:
105+
FRAGMENT: tail
106+
M_C7H2_M:
107+
FRAGMENT: tail
108+
M_C8_M:
109+
FRAGMENT: tail
110+
SMILEIDX: 0
111+
M_C8H1_M:
112+
FRAGMENT: tail
113+
M_C8H2_M:
114+
FRAGMENT: tail
115+
M_C8H3_M:
116+
FRAGMENT: tail

src/fairmd/lipids/molecules.py

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
"""
2-
:module: settings/molecules.py
2+
Define classes for molecules (lipids, ions, etc.) and their sets.
33
4-
:description: Module file with definition of different global-level dictionaries.
5-
6-
There is a dictionary of lipids, ions, etc. If you add a lipid which is not yet
7-
in the databank, you have to add it here!
4+
File defines crucial API classes for working with molecules.
5+
There is a dictionary of lipids, ions, etc. with their metadata and mapping files.
86
"""
97

108
import fnmatch
@@ -42,10 +40,14 @@ class MoleculeMappingError(MoleculeError):
4240
def __init__(self, message: str, mol=None) -> None:
4341
if mol is None:
4442
msg = message
45-
elif mol.mapping_file is None:
43+
elif not mol._can_load_mapping():
4644
msg = f"From {mol}: {message}"
4745
else:
48-
msg = f"From {mol}[{mol.mapping_file}]: {message}" if mol is not None else message
46+
if mol._mapping_fpath is None:
47+
disp_name = "naming.yaml"
48+
else:
49+
disp_name = os.path.relpath(mol._mapping_fpath, FMDL_MOL_PATH)
50+
msg = f"From {mol}[{disp_name}]: {message}" if mol is not None else message
4951
super().__init__(msg, mol=mol)
5052

5153

@@ -85,7 +87,6 @@ def register_mapping(self, fname: str | None = None) -> None:
8587
raise MoleculeMappingError(msg, mol=self)
8688
fname = _possible_mfiles[0] # take the first one
8789
# set mapping file path
88-
self._disp_mapping = fname
8990
self._mapping_fpath = os.path.join(self._get_path(), fname)
9091
if not os.path.isfile(self._mapping_fpath):
9192
msg = f"Cannot find '{self._mapping_fpath}' mapping for molecule {self.name}"
@@ -116,18 +117,22 @@ def check_mapping(self, u: mda.Universe, name: str) -> bool:
116117
@property
117118
def mapping_dict(self) -> dict:
118119
"""Return mapping dictionary (load on first call)"""
119-
if self._mapping_fpath is None:
120+
if not self._can_load_mapping():
120121
msg = "Mapping file is not registered!"
121122
raise MoleculeError(msg, mol=self)
122-
if self._mapping_dict is None:
123-
try:
124-
with open(self._mapping_fpath) as yaml_file:
125-
self._mapping_dict = yaml.safe_load(yaml_file) # yaml.load(yaml_file, Loader=yaml.FullLoader)
126-
except OSError as e:
127-
msg = "Error opening mapping-file!"
128-
raise MoleculeError(msg, mol=self) from e
123+
if self._mapping_dict is None: # load on first request
124+
self._load_mapping_dict()
129125
return self._mapping_dict
130126

127+
def _load_mapping_dict(self) -> None:
128+
"""Load mapping dictionary from the registered mapping file."""
129+
try:
130+
with open(self._mapping_fpath) as yaml_file:
131+
self._mapping_dict = yaml.safe_load(yaml_file) # yaml.load(yaml_file, Loader=yaml.FullLoader)
132+
except OSError as e:
133+
msg = "Error opening mapping-file!"
134+
raise MoleculeError(msg, mol=self) from e
135+
131136
def md2uan(self, mdatomname: str, mdresname: str | None = None) -> str:
132137
"""
133138
Convert MD atom name to the Universal Atom Name.
@@ -136,6 +141,12 @@ def md2uan(self, mdatomname: str, mdresname: str | None = None) -> str:
136141
:return: Universal Atom Name (str)
137142
"""
138143
for universal_name, mrecord in self.mapping_dict.items():
144+
if "ATOMNAME" not in mrecord:
145+
msg = (
146+
f"ATOMNAME field is missing for {universal_name} in mapping dictionary." # !
147+
" MD mapping is not possible."
148+
)
149+
raise MoleculeMappingError(msg, mol=self)
139150
mapping_aname = mrecord["ATOMNAME"]
140151
# MDAnalysis uses fnmatch patterns for selection language
141152
# https://userguide.mdanalysis.org/stable/selections.html
@@ -155,6 +166,9 @@ def uan2selection(self, uname: str, resname: str) -> str:
155166
:raises KeyError: if the universal name is not found in the mapping.
156167
:return: selection string for MDAnalysis
157168
"""
169+
if "ATOMNAME" not in self.mapping_dict[uname]:
170+
msg = f"ATOMNAME field is missing for {uname} in mapping dictionary. MD mapping is not possible."
171+
raise MoleculeMappingError(msg, mol=self)
158172
anm = self.mapping_dict[uname]["ATOMNAME"]
159173
selstr = f"name {anm}"
160174
if "RESIDUE" in self.mapping_dict[uname]:
@@ -172,7 +186,6 @@ def __init__(self, name: str) -> None:
172186
"""
173187
self.__check_name(name)
174188
self._molname = name
175-
self._disp_mapping = None
176189
self._mapping_fpath = None
177190
self._mapping_dict = None
178191

@@ -214,10 +227,9 @@ def name(self) -> str:
214227
"""Molecule name"""
215228
return self._molname
216229

217-
@property
218-
def mapping_file(self) -> str:
219-
"""Mapping file name"""
220-
return self._disp_mapping
230+
def _can_load_mapping(self) -> bool:
231+
"""Is mapping registered for the molecule?"""
232+
return self._mapping_fpath is not None
221233

222234
# comparison by name to behave in a set
223235
# It's case-insesitive as folder structure should work on mac/win
@@ -256,6 +268,40 @@ def _populate_meta_data(self) -> None:
256268
msg = f"Metadata file not found for {self.name}."
257269
raise FileNotFoundError(msg)
258270

271+
def _can_load_mapping(self) -> bool:
272+
super_is_reg = super()._can_load_mapping()
273+
naming_path = os.path.join(self._get_path(), "naming.yaml")
274+
return super_is_reg or os.path.isfile(naming_path)
275+
276+
def _load_mapping_dict(self) -> None:
277+
if super()._can_load_mapping():
278+
super()._load_mapping_dict()
279+
else:
280+
self._mapping_dict = {}
281+
self._load_naming_dict()
282+
283+
def _load_naming_dict(self) -> None:
284+
"""
285+
Load naming dictionary from `naming.yaml` file if it exists.
286+
287+
Updates existing mapping dictionary.
288+
"""
289+
naming_path = os.path.join(self._get_path(), "naming.yaml")
290+
if os.path.isfile(naming_path):
291+
with open(naming_path) as yaml_file:
292+
_naming_dict = yaml.load(yaml_file, Loader=yaml.FullLoader)
293+
for unm, record in _naming_dict.items():
294+
self._mapping_dict.setdefault(unm, {}).update(record)
295+
296+
@property
297+
def fragments(self) -> list[str]:
298+
"""Return list of fragments for the lipid."""
299+
frags = set()
300+
for mrecord in self.mapping_dict.values():
301+
frag = mrecord.get("FRAGMENT", "total")
302+
frags.add(frag)
303+
return sorted(frags)
304+
259305
@property
260306
def metadata(self) -> dict:
261307
"""

0 commit comments

Comments
 (0)