@@ -28,6 +28,7 @@ class MermaidMolParser:
2828 '---' : Chem .BondType .SINGLE ,
2929 '===' : Chem .BondType .DOUBLE ,
3030 '-.-' : Chem .BondType .TRIPLE ,
31+ '<-->' : Chem .BondType .AROMATIC ,
3132 '-->' : Chem .BondType .DATIVE , # 配位键
3233 }
3334
@@ -86,7 +87,7 @@ def _parse_line(self, line: str):
8687
8788 # 尝试匹配普通键连接: atom1 bond_type atom2
8889 # 原子ID可能包含手性后缀 (_R 或 _S)
89- bond_pattern = r'([\w_]+)\s*(---|\===|-\.-|-->)\s*([\w_]+)'
90+ bond_pattern = r'([\w_]+)\s*(<-->| ---|\===|-\.-|-->)\s*([\w_]+)'
9091 bond_match = re .search (bond_pattern , line )
9192
9293 if bond_match :
@@ -215,17 +216,10 @@ def _build_mol(self) -> Optional[Chem.Mol]:
215216 idx = mol .AddAtom (atom )
216217 atom_id_to_idx [atom_id ] = idx
217218
218- # 设置手性(如果有)
219- if atom_id in self .chirality :
220- chirality_type = self .chirality [atom_id ]
221- atom_obj = mol .GetAtomWithIdx (idx )
222-
223- if chirality_type == 'R' :
224- atom_obj .SetChiralTag (Chem .ChiralType .CHI_TETRAHEDRAL_CW )
225- elif chirality_type == 'S' :
226- atom_obj .SetChiralTag (Chem .ChiralType .CHI_TETRAHEDRAL_CCW )
227-
228- # 添加键
219+ # 添加键。双键 E/Z 和四面体 R/S 都需要在完整拓扑存在后设置,
220+ # 因此这里先记录,待 SanitizeMol 后统一恢复。
221+ stereo_bonds = []
222+ aromatic_atom_idxs = set ()
229223 for bond_info in self .bonds :
230224 if len (bond_info ) == 3 :
231225 # 普通键: (atom1_id, atom2_id, bond_type_str)
@@ -244,40 +238,16 @@ def _build_mol(self) -> Optional[Chem.Mol]:
244238
245239 mol .AddBond (idx1 , idx2 , bond_type )
246240
247- # 设置立体化学(稍后统一处理,需要先添加所有键)
241+ if bond_type_str == '<-->' :
242+ aromatic_atom_idxs .update ((idx1 , idx2 ))
243+ bond = mol .GetBondBetweenAtoms (idx1 , idx2 )
244+ bond .SetIsAromatic (True )
245+
248246 if stereo_type :
249- # 记录需要设置立体化学的键
250- if not hasattr (mol , '_stereo_bonds' ):
251- mol ._stereo_bonds = []
252- mol ._stereo_bonds .append ((idx1 , idx2 , stereo_type ))
253-
254- # 在转换为不可编辑的Mol之前,设置立体化学
255- if hasattr (mol , '_stereo_bonds' ):
256- for idx1 , idx2 , stereo_type in mol ._stereo_bonds :
257- bond = mol .GetBondBetweenAtoms (idx1 , idx2 )
258-
259- # 获取双键两端原子的邻接原子(用于定义立体化学)
260- atom1 = mol .GetAtomWithIdx (idx1 )
261- atom2 = mol .GetAtomWithIdx (idx2 )
262-
263- # 找到idx1的邻居(除了idx2)
264- neighbors1 = [n .GetIdx () for n in atom1 .GetNeighbors () if n .GetIdx () != idx2 ]
265- # 找到idx2的邻居(除了idx1)
266- neighbors2 = [n .GetIdx () for n in atom2 .GetNeighbors () if n .GetIdx () != idx1 ]
267-
268- # 如果两端都有邻居,设置立体化学
269- if neighbors1 and neighbors2 :
270- # 使用第一个邻居作为参考原子
271- bond .SetStereoAtoms (neighbors1 [0 ], neighbors2 [0 ])
272-
273- if stereo_type == 'E' :
274- bond .SetStereo (Chem .BondStereo .STEREOE )
275- elif stereo_type == 'Z' :
276- bond .SetStereo (Chem .BondStereo .STEREOZ )
277- elif stereo_type == 'CIS' :
278- bond .SetStereo (Chem .BondStereo .STEREOCIS )
279- elif stereo_type == 'TRANS' :
280- bond .SetStereo (Chem .BondStereo .STEREOTRANS )
247+ stereo_bonds .append ((idx1 , idx2 , stereo_type ))
248+
249+ for idx in aromatic_atom_idxs :
250+ mol .GetAtomWithIdx (idx ).SetIsAromatic (True )
281251
282252 # 转换为不可编辑的Mol对象
283253 mol = mol .GetMol ()
@@ -296,9 +266,82 @@ def _build_mol(self) -> Optional[Chem.Mol]:
296266 # 完全失败,返回未清理的版本
297267 pass
298268
269+ self ._assign_chirality_from_ids (mol , atom_id_to_idx )
270+ self ._assign_double_bond_stereo (mol , stereo_bonds )
271+
299272 return mol
300273
301274
275+ def _assign_chirality_from_ids (self , mol : Chem .Mol , atom_id_to_idx : Dict [str , int ]):
276+ """根据 atom id 的 _R/_S 后缀恢复绝对 CIP 手性。"""
277+ if not self .chirality :
278+ return
279+
280+ for atom_id , desired_cip in self .chirality .items ():
281+ idx = atom_id_to_idx .get (atom_id )
282+ if idx is None :
283+ continue
284+
285+ atom = mol .GetAtomWithIdx (idx )
286+ matched = False
287+
288+ for chiral_tag in (
289+ Chem .ChiralType .CHI_TETRAHEDRAL_CW ,
290+ Chem .ChiralType .CHI_TETRAHEDRAL_CCW ,
291+ ):
292+ atom .SetChiralTag (chiral_tag )
293+ try :
294+ Chem .AssignStereochemistry (mol , cleanIt = True , force = True )
295+ except Exception :
296+ continue
297+
298+ if atom .HasProp ('_CIPCode' ) and atom .GetProp ('_CIPCode' ) == desired_cip :
299+ matched = True
300+ break
301+
302+ if not matched :
303+ atom .SetChiralTag (Chem .ChiralType .CHI_UNSPECIFIED )
304+
305+ try :
306+ Chem .AssignStereochemistry (mol , cleanIt = False , force = True )
307+ except Exception :
308+ pass
309+
310+ def _assign_double_bond_stereo (self , mol : Chem .Mol , stereo_bonds : List [Tuple [int , int , str ]]):
311+ """恢复 ===|E| / ===|Z| 双键构型。"""
312+ for idx1 , idx2 , stereo_type in stereo_bonds :
313+ bond = mol .GetBondBetweenAtoms (idx1 , idx2 )
314+ if bond is None :
315+ continue
316+
317+ # 获取双键两端原子的邻接原子(用于定义立体化学)
318+ atom1 = mol .GetAtomWithIdx (idx1 )
319+ atom2 = mol .GetAtomWithIdx (idx2 )
320+
321+ neighbors1 = [n .GetIdx () for n in atom1 .GetNeighbors () if n .GetIdx () != idx2 ]
322+ neighbors2 = [n .GetIdx () for n in atom2 .GetNeighbors () if n .GetIdx () != idx1 ]
323+
324+ if not neighbors1 or not neighbors2 :
325+ continue
326+
327+ bond .SetStereoAtoms (neighbors1 [0 ], neighbors2 [0 ])
328+
329+ if stereo_type == 'E' :
330+ bond .SetStereo (Chem .BondStereo .STEREOE )
331+ elif stereo_type == 'Z' :
332+ bond .SetStereo (Chem .BondStereo .STEREOZ )
333+ elif stereo_type == 'CIS' :
334+ bond .SetStereo (Chem .BondStereo .STEREOCIS )
335+ elif stereo_type == 'TRANS' :
336+ bond .SetStereo (Chem .BondStereo .STEREOTRANS )
337+
338+ try :
339+ # 不使用 cleanIt=True,避免清掉刚刚从 EGL 明确恢复的 E/Z 标记。
340+ Chem .AssignStereochemistry (mol , cleanIt = False , force = True )
341+ except Exception :
342+ pass
343+
344+
302345def has_invalid_atoms (mol : Chem .Mol ) -> bool :
303346 """
304347 检查分子是否包含无效原子(Dummy Atom)
0 commit comments