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
108import 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