MoleditPy-linux 4.5.0__py3-none-any.whl → 4.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,6 +30,93 @@ class PointTuple(tuple):
30
30
  return float(self[1])
31
31
 
32
32
 
33
+ def _mdl_radical_code(radical_electrons: int) -> int:
34
+ """Map a count of unpaired electrons to an MDL radical code."""
35
+ # MDL encodes 1 = singlet, 2 = doublet, 3 = triplet.
36
+ if radical_electrons <= 0:
37
+ return 0
38
+ return 2 if radical_electrons == 1 else 3
39
+
40
+
41
+ def _mdl_property_lines(atom_records: List[Dict[str, Any]]) -> List[str]:
42
+ """Build the M CHG / M RAD property lines for a V2000 block."""
43
+ lines = []
44
+ for tag, key, encode in (
45
+ ("CHG", "charge", int),
46
+ ("RAD", "radical", _mdl_radical_code),
47
+ ):
48
+ entries = [
49
+ (idx, encode(rec[key]))
50
+ for idx, rec in enumerate(atom_records, start=1)
51
+ if rec[key]
52
+ ]
53
+ # The MDL property block allows at most 8 entries per line.
54
+ for start in range(0, len(entries), 8):
55
+ chunk = entries[start : start + 8]
56
+ lines.append(
57
+ f"M {tag}{len(chunk):3d}"
58
+ + "".join(f"{idx:4d}{val:4d}" for idx, val in chunk)
59
+ )
60
+ return lines
61
+
62
+
63
+ def _to_v2000_block(
64
+ atom_records: List[Dict[str, Any]], bond_records: List[Dict[str, Any]]
65
+ ) -> str:
66
+ """Render collected atom/bond records as an MDL V2000 MOL block."""
67
+ lines = ["", " MoleditPy", ""]
68
+ lines.append(
69
+ f"{len(atom_records):3d}{len(bond_records):3d} 0 0 0 0 0 0 0 0999 V2000"
70
+ )
71
+ for rec in atom_records:
72
+ # Charge and radical travel in the M CHG / M RAD blocks below, so every
73
+ # per-atom field here stays zero.
74
+ lines.append(
75
+ f"{rec['x']:10.4f}{rec['y']:10.4f}{0.0:10.4f} {rec['symbol']:<3}"
76
+ " 0 0 0 0 0 0 0 0 0 0 0 0"
77
+ )
78
+ for rec in bond_records:
79
+ lines.append(
80
+ f"{rec['a1']:3d}{rec['a2']:3d}{rec['order']:3d}{rec['stereo']:3d} 0 0 0"
81
+ )
82
+ lines.extend(_mdl_property_lines(atom_records))
83
+ lines.append("M END")
84
+ return "\n".join(lines) + "\n"
85
+
86
+
87
+ def _to_v3000_block(
88
+ atom_records: List[Dict[str, Any]], bond_records: List[Dict[str, Any]]
89
+ ) -> str:
90
+ """Render collected atom/bond records as an MDL V3000 MOL block."""
91
+ lines = ["", " MoleditPy", "", " 0 0 0 0 0 0 0 0 0 0999 V3000"]
92
+ lines.append("M V30 BEGIN CTAB")
93
+ lines.append(f"M V30 COUNTS {len(atom_records)} {len(bond_records)} 0 0 0")
94
+ lines.append("M V30 BEGIN ATOM")
95
+ for idx, rec in enumerate(atom_records, start=1):
96
+ extra = ""
97
+ if rec["charge"]:
98
+ extra += f" CHG={int(rec['charge'])}"
99
+ if rec["radical"]:
100
+ extra += f" RAD={_mdl_radical_code(rec['radical'])}"
101
+ lines.append(
102
+ f"M V30 {idx} {rec['symbol']} "
103
+ f"{rec['x']:.4f} {rec['y']:.4f} 0.0000 0{extra}"
104
+ )
105
+ lines.append("M V30 END ATOM")
106
+ lines.append("M V30 BEGIN BOND")
107
+ for idx, rec in enumerate(bond_records, start=1):
108
+ cfg = ""
109
+ if rec["stereo"] == 1:
110
+ cfg = " CFG=1"
111
+ elif rec["stereo"] == 6:
112
+ cfg = " CFG=3"
113
+ lines.append(f"M V30 {idx} {rec['order']} {rec['a1']} {rec['a2']}{cfg}")
114
+ lines.append("M V30 END BOND")
115
+ lines.append("M V30 END CTAB")
116
+ lines.append("M END")
117
+ return "\n".join(lines) + "\n"
118
+
119
+
33
120
  class MolecularData:
34
121
  """In-memory graph of atoms and bonds, independent of any UI framework."""
35
122
 
@@ -348,7 +435,7 @@ class MolecularData:
348
435
 
349
436
  # Counts line and bond indices must only reflect atoms actually written
350
437
  atom_map: Dict[int, int] = {}
351
- atom_lines: List[str] = []
438
+ atom_records: List[Dict[str, Any]] = []
352
439
  for old_id, atom in self.atoms.items():
353
440
  # Convert scene pixel coordinates to angstroms when emitting MOL block
354
441
  pos = atom.get("pos")
@@ -362,37 +449,23 @@ class MolecularData:
362
449
  else:
363
450
  continue
364
451
 
365
- x, y = x_px * ANGSTROM_PER_PIXEL, y_px * ANGSTROM_PER_PIXEL
366
- z, symbol = 0.0, atom["symbol"]
367
- charge = atom.get("charge", 0)
368
-
369
- chg_code = 0
370
- if charge == 3:
371
- chg_code = 1
372
- elif charge == 2:
373
- chg_code = 2
374
- elif charge == 1:
375
- chg_code = 3
376
- elif charge == -1:
377
- chg_code = 5
378
- elif charge == -2:
379
- chg_code = 6
380
- elif charge == -3:
381
- chg_code = 7
382
-
383
- atom_map[old_id] = len(atom_lines)
384
- atom_lines.append(
385
- f"{x:10.4f}{y:10.4f}{z:10.4f} {symbol:<3} 0 0 0{chg_code:3d} 0 0 0 0 0 0 0\n"
452
+ atom_map[old_id] = len(atom_records)
453
+ atom_records.append(
454
+ {
455
+ "x": x_px * ANGSTROM_PER_PIXEL,
456
+ "y": y_px * ANGSTROM_PER_PIXEL,
457
+ "symbol": atom["symbol"],
458
+ "charge": int(atom.get("charge", 0) or 0),
459
+ "radical": int(atom.get("radical", 0) or 0),
460
+ }
386
461
  )
387
462
 
388
- bond_lines = []
463
+ bond_records: List[Dict[str, Any]] = []
389
464
  for (id1, id2), bond in self.bonds.items():
390
465
  if id1 not in atom_map or id2 not in atom_map:
391
466
  continue
392
- idx1, idx2 = atom_map[id1] + 1, atom_map[id2] + 1
393
- # Bond order may be a float (1.5 = aromatic); V2000 uses code 4.
467
+ # Bond order may be a float (1.5 = aromatic); MDL uses code 4.
394
468
  order_val = float(bond["order"])
395
- order = 4 if order_val == 1.5 else int(order_val)
396
469
  stereo_code = 0
397
470
  bond_stereo = bond.get("stereo", 0)
398
471
  if bond_stereo == 1:
@@ -400,16 +473,20 @@ class MolecularData:
400
473
  elif bond_stereo == 2:
401
474
  stereo_code = 6
402
475
 
403
- bond_lines.append(
404
- f"{idx1:3d}{idx2:3d}{order:3d}{stereo_code:3d} 0 0 0\n"
476
+ bond_records.append(
477
+ {
478
+ "a1": atom_map[id1] + 1,
479
+ "a2": atom_map[id2] + 1,
480
+ "order": 4 if order_val == 1.5 else int(order_val),
481
+ "stereo": stereo_code,
482
+ }
405
483
  )
406
484
 
407
- mol_block = "\n MoleditPy\n\n"
408
- mol_block += f"{len(atom_lines):3d}{len(bond_lines):3d} 0 0 0 0 0 0 0 0999 V2000\n"
409
- mol_block += "".join(atom_lines)
410
- mol_block += "".join(bond_lines)
411
- mol_block += "M END\n"
412
- return mol_block
485
+ # V2000 packs the counts into three-character fields, so anything past
486
+ # 999 would run the atom count into the bond count and corrupt the file.
487
+ if len(atom_records) > 999 or len(bond_records) > 999:
488
+ return _to_v3000_block(atom_records, bond_records)
489
+ return _to_v2000_block(atom_records, bond_records)
413
490
 
414
491
  def to_template_dict(
415
492
  self, name: str, version: str = "1.0", application_version: str = ""
moleditpy_linux/main.py CHANGED
@@ -189,7 +189,7 @@ def setup_logging() -> None:
189
189
  log_dir = os.path.join(os.path.expanduser("~"), ".moleditpy")
190
190
  try:
191
191
  os.makedirs(log_dir, exist_ok=True)
192
- log_path = os.path.join(log_dir, "moleditpy_linux.log")
192
+ log_path = os.path.join(log_dir, "moleditpy.log")
193
193
  fh = logging.handlers.RotatingFileHandler(
194
194
  log_path, maxBytes=1_048_576, backupCount=3, encoding="utf-8"
195
195
  )
@@ -231,7 +231,7 @@ def main() -> None:
231
231
  if sys.platform == "win32":
232
232
  # Taskbar grouping ID follows the major version
233
233
  major = VERSION.split(".")[0] if VERSION and VERSION != "Unknown" else "0"
234
- myappid = f"hyoko.moleditpy_linux.{major}"
234
+ myappid = f"hyoko.moleditpy.{major}"
235
235
  ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
236
236
 
237
237
  parser = argparse.ArgumentParser(
@@ -35,6 +35,7 @@ from ..core.mol_geometry import (
35
35
  calc_angle_deg,
36
36
  get_connected_group,
37
37
  )
38
+ from ..utils.suppress_log import suppress_log
38
39
 
39
40
  if TYPE_CHECKING:
40
41
  from .main_window import MainWindow
@@ -468,5 +469,26 @@ class AngleDialog(GeometryBaseDialog):
468
469
  atoms_to_move,
469
470
  )
470
471
 
472
+ achieved = calc_angle_deg(positions[idx_a], positions[idx_b], positions[idx_c])
473
+
471
474
  # Write updated positions back using inherited helper
472
475
  self._update_molecule_geometry(positions)
476
+
477
+ self._warn_if_angle_not_applied(new_angle_deg, achieved)
478
+
479
+ def _warn_if_angle_not_applied(self, requested: float, achieved: float) -> None:
480
+ """Tell the user when the requested angle could not be reached.
481
+
482
+ For an angle inside a ring the connected group reached from one arm
483
+ comes back around and contains all three atoms, so they rotate
484
+ together and the angle is unchanged. Leaving the old value silently
485
+ looks like the edit was applied.
486
+ """
487
+ if abs(achieved - requested) < 0.5:
488
+ return
489
+ with suppress_log(AttributeError, RuntimeError, TypeError):
490
+ self.main_window.statusBar().showMessage(
491
+ f"Angle unchanged ({achieved:.2f} deg): the three atoms rotate "
492
+ "together, which happens for an angle inside a ring.",
493
+ 5000,
494
+ )
@@ -14,7 +14,7 @@ from __future__ import annotations
14
14
  import logging
15
15
 
16
16
  import numpy as np
17
- from typing import Any, TYPE_CHECKING, Optional, Sequence
17
+ from typing import Any, Dict, TYPE_CHECKING, Optional, Sequence
18
18
 
19
19
  from PyQt6.QtCore import Qt
20
20
  from PyQt6.QtWidgets import (
@@ -32,6 +32,7 @@ from rdkit import Chem
32
32
 
33
33
  from .geometry_base_dialog import GeometryBaseDialog
34
34
  from ..core.mol_geometry import calc_distance, get_connected_group
35
+ from ..utils.suppress_log import suppress_log
35
36
 
36
37
  if TYPE_CHECKING:
37
38
  from .main_window import MainWindow
@@ -359,6 +360,8 @@ class BondLengthDialog(GeometryBaseDialog):
359
360
  if current_distance == 0:
360
361
  return
361
362
 
363
+ original = {i: np.array(p, dtype=float) for i, p in positions.items()}
364
+
362
365
  direction = direction / current_distance
363
366
 
364
367
  if self.both_groups_radio.isChecked():
@@ -400,3 +403,46 @@ class BondLengthDialog(GeometryBaseDialog):
400
403
 
401
404
  # Write updated positions back using inherited helper
402
405
  self._update_molecule_geometry(positions)
406
+
407
+ self._warn_if_other_bonds_moved(original, positions, idx1, idx2)
408
+
409
+ def _warn_if_other_bonds_moved(
410
+ self,
411
+ before: Dict[int, Any],
412
+ after: Dict[int, Any],
413
+ idx1: int,
414
+ idx2: int,
415
+ ) -> None:
416
+ """Report a neighbouring bond that this edit resized as a side effect.
417
+
418
+ A bond inside a ring cannot be resized alone: the group translated
419
+ along the bond axis wraps back around the ring, so the ring closes at
420
+ a different length. Stretching a benzene bond to 1.80 A pulls its
421
+ neighbour to 1.24 A, shorter than a double bond, with nothing on
422
+ screen to say so.
423
+ """
424
+ edited = {idx1, idx2}
425
+ worst_delta = 0.0
426
+ worst_label = ""
427
+ for bond in self.mol.GetBonds():
428
+ i, j = bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()
429
+ if {i, j} == edited or i not in before or j not in before:
430
+ continue
431
+ delta = calc_distance(after[i], after[j]) - calc_distance(
432
+ before[i], before[j]
433
+ )
434
+ if abs(delta) > abs(worst_delta):
435
+ worst_delta = delta
436
+ worst_label = (
437
+ f"{self.mol.GetAtomWithIdx(i).GetSymbol()}{i}-"
438
+ f"{self.mol.GetAtomWithIdx(j).GetSymbol()}{j}"
439
+ )
440
+
441
+ if abs(worst_delta) < 1e-4:
442
+ return
443
+ with suppress_log(AttributeError, RuntimeError, TypeError):
444
+ self.main_window.statusBar().showMessage(
445
+ f"Bond {worst_label} also changed by {worst_delta:+.3f} A: a bond "
446
+ "inside a ring cannot be resized on its own.",
447
+ 5000,
448
+ )
@@ -19,6 +19,7 @@ from PyQt6.QtCore import QThread, QTimer, QPoint
19
19
  from PyQt6.QtGui import QAction, QColor
20
20
  from PyQt6.QtWidgets import QMenu, QMessageBox, QWidget
21
21
  from rdkit import Chem
22
+ from rdkit.Chem import AllChem
22
23
 
23
24
  from . import OBABEL_AVAILABLE
24
25
 
@@ -458,7 +459,31 @@ class ComputeManager:
458
459
  # Defer so Qt finishes routing the modal result before restoring focus.
459
460
  QTimer.singleShot(0, self.host.init_manager.view_2d.setFocus)
460
461
 
462
+ def _ez_consistent_mol_block(self) -> Optional[str]:
463
+ """MOL block whose 2D geometry encodes the editor's E/Z labels."""
464
+ data = self.host.state_manager.data
465
+ if not any(b.get("stereo", 0) in (3, 4) for b in data.bonds.values()):
466
+ return None
467
+ stereo_mol = data.to_rdkit_mol(use_2d_stereo=False)
468
+ if stereo_mol is None:
469
+ return None
470
+ try:
471
+ # MDL carries double-bond cis/trans only in the 2D coordinates, so a
472
+ # label that contradicts how the user drew the bond is lost unless
473
+ # the layout is rebuilt from the stereo. These coordinates only seed
474
+ # 3D generation; the 2D canvas keeps the user's own layout.
475
+ planar = Chem.Mol(stereo_mol)
476
+ planar.RemoveAllConformers()
477
+ AllChem.Compute2DCoords(planar)
478
+ return str(Chem.MolToMolBlock(planar, includeStereo=True))
479
+ except (RuntimeError, ValueError) as e:
480
+ logging.warning("E/Z-consistent 2D layout failed: %s", e)
481
+ return None
482
+
461
483
  def _setup_mol_block_for_worker(self, mol: Chem.Mol) -> str:
484
+ ez_block = self._ez_consistent_mol_block()
485
+ if ez_block:
486
+ return ez_block
462
487
  mol_block = self.host.state_manager.data.to_mol_block()
463
488
  if not mol_block:
464
489
  mol_block = Chem.MolToMolBlock(mol, includeStereo=True)
@@ -35,6 +35,7 @@ from ..core.mol_geometry import (
35
35
  calculate_dihedral,
36
36
  get_connected_group,
37
37
  )
38
+ from ..utils.suppress_log import suppress_log
38
39
 
39
40
  if TYPE_CHECKING:
40
41
  from .main_window import MainWindow
@@ -469,5 +470,27 @@ class DihedralDialog(GeometryBaseDialog):
469
470
  positions, idx1, idx2, idx3, idx4, new_dihedral_deg, atoms_to_move
470
471
  )
471
472
 
473
+ achieved = calculate_dihedral(positions, idx1, idx2, idx3, idx4)
474
+
472
475
  # Write updated positions back using inherited helper
473
476
  self._update_molecule_geometry(positions)
477
+
478
+ self._warn_if_torsion_not_applied(new_dihedral_deg, achieved)
479
+
480
+ def _warn_if_torsion_not_applied(self, requested: float, achieved: float) -> None:
481
+ """Tell the user when the requested torsion could not be reached.
482
+
483
+ A torsion inside a ring cannot rotate: the connected group reached from
484
+ one side comes back around and contains all four atoms, so they turn
485
+ together and the angle is unchanged. Silently leaving the old value
486
+ looks like the edit was applied.
487
+ """
488
+ difference = abs(achieved - requested) % 360.0
489
+ if min(difference, 360.0 - difference) < 0.5:
490
+ return
491
+ with suppress_log(AttributeError, RuntimeError, TypeError):
492
+ self.main_window.statusBar().showMessage(
493
+ f"Dihedral unchanged ({achieved:.2f}°): the four atoms rotate "
494
+ "together, which happens for a bond inside a ring.",
495
+ 5000,
496
+ )
@@ -583,26 +583,17 @@ class EditActionsManager:
583
583
  if orig_id not in self.host.state_manager.data.atoms:
584
584
  continue
585
585
 
586
- # Get implicit hydrogens; fallback to total - explicit
587
- implicit_h = (
588
- int(rd_atom.GetNumImplicitHs())
589
- if hasattr(rd_atom, "GetNumImplicitHs")
590
- else 0
591
- )
592
- if implicit_h is None or implicit_h < 0:
586
+ # Hydrogens still needing an atom of their own. GetTotalNumHs()
587
+ # excludes H already drawn as atoms but includes the
588
+ # property-explicit H that sanitization assigns to an aromatic
589
+ # N-H; subtracting GetNumExplicitHs() here would cancel exactly
590
+ # that term and leave luminol's two N-H without hydrogens.
591
+ try:
592
+ implicit_h = int(rd_atom.GetTotalNumHs())
593
+ except (AttributeError, RuntimeError, ValueError, TypeError):
594
+ implicit_h = 0
595
+ if implicit_h < 0:
593
596
  implicit_h = 0
594
- if implicit_h == 0:
595
- # Fallback
596
- try:
597
- total_h = int(rd_atom.GetTotalNumHs())
598
- explicit_h = (
599
- int(rd_atom.GetNumExplicitHs())
600
- if hasattr(rd_atom, "GetNumExplicitHs")
601
- else 0
602
- )
603
- implicit_h = max(0, total_h - explicit_h)
604
- except (AttributeError, RuntimeError, ValueError, TypeError):
605
- implicit_h = 0
606
597
 
607
598
  if implicit_h <= 0:
608
599
  continue
@@ -954,12 +945,16 @@ class EditActionsManager:
954
945
  continue
955
946
  original_id = atom.GetIntProp("_original_atom_id")
956
947
 
957
- # Robust retrieval of H counts: prefer implicit, fallback to total or 0
948
+ # GetTotalNumHs() counts implicit plus property-explicit H but
949
+ # not H drawn as its own atom, which is exactly what the label
950
+ # should show. Sanitization files an aromatic N-H (luminol,
951
+ # pyrrole) under the property, where GetNumImplicitHs() alone
952
+ # reports 0 and the H silently vanishes from the label.
958
953
  try:
959
- h_count = int(atom.GetNumImplicitHs())
954
+ h_count = int(atom.GetTotalNumHs())
960
955
  except (AttributeError, RuntimeError, ValueError, TypeError):
961
956
  try:
962
- h_count = int(atom.GetTotalNumHs())
957
+ h_count = int(atom.GetNumImplicitHs())
963
958
  except (AttributeError, RuntimeError, ValueError, TypeError):
964
959
  h_count = 0
965
960
 
@@ -152,8 +152,11 @@ class IOManager:
152
152
 
153
153
  def _mol_from_xyz_lines(self, raw_lines: list[str]) -> Any:
154
154
  """Create an RDKit molecule from XYZ text lines."""
155
- lines = [ln.strip() for ln in raw_lines if not ln.strip().startswith("#")]
156
- while lines and not lines[0]:
155
+ lines = [ln.strip() for ln in raw_lines]
156
+ # Only leading comments are metadata. In a headed XYZ the second line is
157
+ # the title and may itself begin with '#', so discarding every '#' line
158
+ # would shift the atom rows up one and silently drop the first atom.
159
+ while lines and (not lines[0] or lines[0].startswith("#")):
157
160
  lines.pop(0)
158
161
 
159
162
  if not lines:
@@ -167,9 +170,11 @@ class IOManager:
167
170
  if num_atoms == 0:
168
171
  raise ValueError("XYZ file has zero atoms")
169
172
  except ValueError as exc:
170
- # Not a standard headed XYZ — treat all lines as atom rows
173
+ # Not a standard headed XYZ — treat all lines as atom rows. There is
174
+ # no title line here, so interleaved comments are safe to drop.
171
175
  if "zero atoms" in str(exc) or "too few" in str(exc):
172
176
  raise
177
+ lines = [ln for ln in lines if ln and not ln.startswith("#")]
173
178
  num_atoms = len(lines)
174
179
  atom_start = 0
175
180
 
@@ -442,6 +447,7 @@ class IOManager:
442
447
  num_atoms = mol.GetNumAtoms()
443
448
  bonds_added = []
444
449
 
450
+ candidates = []
445
451
  for i in range(num_atoms):
446
452
  for j in range(i + 1, num_atoms):
447
453
  atom_i = mol.GetAtomWithIdx(i)
@@ -456,21 +462,27 @@ class IOManager:
456
462
  expected = radius_i + radius_j
457
463
  tolerance = 1.2 if (symbol_i == "H" or symbol_j == "H") else 1.3
458
464
  if expected * 0.5 <= distance <= expected * tolerance:
459
- if mol.GetBondBetweenAtoms(i, j) is None:
460
- if (
461
- symbol_i == "H" and mol.GetAtomWithIdx(i).GetDegree() >= 1
462
- ) or (
463
- symbol_j == "H" and mol.GetAtomWithIdx(j).GetDegree() >= 1
464
- ):
465
- continue
466
- try:
467
- mol.AddBond(i, j, Chem.BondType.SINGLE)
468
- bonds_added.append((i, j, distance))
469
- except (RuntimeError, ValueError, TypeError):
470
- # Safe defensive fallback catching RuntimeError, ValueError, TypeError
471
- logging.debug(
472
- "Suppressed non-critical error", exc_info=True
473
- )
465
+ candidates.append((distance, i, j, symbol_i, symbol_j))
466
+
467
+ # Nearest pairs first. The monovalent-H rule below accepts whichever
468
+ # partner it sees first, so index order would otherwise decide the
469
+ # connectivity and the same geometry could bond an H differently
470
+ # depending only on how the file happened to list its atoms.
471
+ candidates.sort(key=lambda c: c[0])
472
+
473
+ for distance, i, j, symbol_i, symbol_j in candidates:
474
+ if mol.GetBondBetweenAtoms(i, j) is not None:
475
+ continue
476
+ if (symbol_i == "H" and mol.GetAtomWithIdx(i).GetDegree() >= 1) or (
477
+ symbol_j == "H" and mol.GetAtomWithIdx(j).GetDegree() >= 1
478
+ ):
479
+ continue
480
+ try:
481
+ mol.AddBond(i, j, Chem.BondType.SINGLE)
482
+ bonds_added.append((i, j, distance))
483
+ except (RuntimeError, ValueError, TypeError):
484
+ # Safe defensive fallback catching RuntimeError, ValueError, TypeError
485
+ logging.debug("Suppressed non-critical error", exc_info=True)
474
486
 
475
487
  return len(bonds_added)
476
488
 
@@ -816,6 +828,7 @@ class IOManager:
816
828
  atom.GetSymbol(),
817
829
  QPointF(scene_x, scene_y),
818
830
  charge=atom.GetFormalCharge(),
831
+ radical=atom.GetNumRadicalElectrons(),
819
832
  )
820
833
  rdkit_idx_to_my_id[i] = atom_id
821
834
 
@@ -117,10 +117,10 @@ class SettingsOtherTab(SettingsTabBase):
117
117
 
118
118
  self.log_to_file_checkbox = QCheckBox()
119
119
  self.log_to_file_checkbox.setToolTip(
120
- "Save application log to ~/.moleditpy/moleditpy_linux.log (rotated, max 1 MB × 3)."
120
+ "Save application log to ~/.moleditpy/moleditpy.log (rotated, max 1 MB × 3)."
121
121
  )
122
122
  form_layout.addRow(
123
- "Save log to file (~/.moleditpy/moleditpy_linux.log):", self.log_to_file_checkbox
123
+ "Save log to file (~/.moleditpy/moleditpy.log):", self.log_to_file_checkbox
124
124
  )
125
125
 
126
126
  self.log_level_debug_checkbox = QCheckBox()
@@ -119,6 +119,7 @@ class StringImporterManager:
119
119
  atom.GetSymbol(),
120
120
  QPointF(scene_x, scene_y),
121
121
  charge=atom.GetFormalCharge(),
122
+ radical=atom.GetNumRadicalElectrons(),
122
123
  )
123
124
  rdkit_idx_to_my_id[i] = atom_id
124
125
  return rdkit_idx_to_my_id
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 4.5.0
3
+ Version: 4.5.1
4
4
  Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
5
5
  Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -1,13 +1,13 @@
1
1
  moleditpy_linux/__init__.py,sha256=QYE14Hc9eAwykvTIDIXvur6NXlGcZ0hg2nUZesj6soI,462
2
2
  moleditpy_linux/__main__.py,sha256=VHCD-CCX6iKUbmUyRv2BGVXCjHWEaISpmjO7_yh9AaU,654
3
- moleditpy_linux/main.py,sha256=CKGMAuDPhcs-9U7wSJstOt63VStfMlIyovo979053hY,12958
3
+ moleditpy_linux/main.py,sha256=Lz3EaQmCGizoUXzCwa1sfYa40WZ1wRB0HCxbocQoPR0,12946
4
4
  moleditpy_linux/assets/file_icon.ico,sha256=yyVj084A7HuMNbV073cE_Ag3Ne405qgOP3Mia1ZqLpE,101632
5
5
  moleditpy_linux/assets/icon.icns,sha256=wD5R6-Vw7K662tVKhu2E1ImN0oUuyAP4youesEQsn9c,139863
6
6
  moleditpy_linux/assets/icon.ico,sha256=RfgFcx7-dHY_2STdsOQCQziY5SNhDr3gPnjO6jzEDPI,147975
7
7
  moleditpy_linux/assets/icon.png,sha256=kCFN1WacYIdy0GN6SFEbNA00ef39pCczBnFdkkBI8Bs,147110
8
8
  moleditpy_linux/core/__init__.py,sha256=BEOaCz93HzgTe0FhBzSj6Sfk8MAWxcY9vCDGmOriJx4,267
9
9
  moleditpy_linux/core/mol_geometry.py,sha256=PzdJGxWs6vXwVyILBG6-6qhiVYuB33YTr8lcHRJVLNE,23933
10
- moleditpy_linux/core/molecular_data.py,sha256=CboEOZMjtbyRSQ7-f6kj0jngUM0Tw-7L6lU9eDtdycQ,18606
10
+ moleditpy_linux/core/molecular_data.py,sha256=6dOi9cqO-JSSLApVbSDP9Dr5-mdnHNLZxYc0DnxyZ54,21712
11
11
  moleditpy_linux/plugins/__init__.py,sha256=BEOaCz93HzgTe0FhBzSj6Sfk8MAWxcY9vCDGmOriJx4,267
12
12
  moleditpy_linux/plugins/plugin_interface.py,sha256=shBuVwQu3cQxhhM5M82EkQWMt19RcOxyXZ6CvkjXHtg,22981
13
13
  moleditpy_linux/plugins/plugin_manager.py,sha256=NzDKllVYLNaFDyV_rEDxtMyKfMWl4LFHfEKnEcOUtcA,34390
@@ -17,27 +17,27 @@ moleditpy_linux/ui/about_dialog.py,sha256=saCFGcJXoWTIwWkH3iwIPy9dRy-zlgVUKfN6x2
17
17
  moleditpy_linux/ui/align_plane_dialog.py,sha256=pDkZi3NGj5KEqy5hcipiCWrVRWnhg5itDhpXAB-b5oc,11051
18
18
  moleditpy_linux/ui/alignment_dialog.py,sha256=21-RCNXHwdeD9ARQ1GZxdOS0wTeYwzQmFm1hkpQU-6M,11136
19
19
  moleditpy_linux/ui/analysis_window.py,sha256=tAsm8GLPjXGuMupM3uHbcaMVuJ3X_MZNB8yBcjECcyk,9800
20
- moleditpy_linux/ui/angle_dialog.py,sha256=Wgyz3VYTVJZYILxCuj_0NGtKBnq47sSg0IwlXqFRNK8,18842
20
+ moleditpy_linux/ui/angle_dialog.py,sha256=m03NUjyGQwX_e-pft7KPl7y5yTCuS5Pjg-1g8ozo6KY,19873
21
21
  moleditpy_linux/ui/app_state.py,sha256=4Q1Lygj_1DbG83prV1I3eLbuNWtF3_19SGpVHhCxOxE,32442
22
22
  moleditpy_linux/ui/atom_item.py,sha256=vWBINnyhN8w8GzEflibfQ268wVQJerqE0rHA-HEUGlQ,22551
23
23
  moleditpy_linux/ui/atom_picking.py,sha256=nbdFdcaEOgLQtqZXYoKQUvFyVHagPKlms96chw00m2Y,10566
24
24
  moleditpy_linux/ui/base_picking_dialog.py,sha256=F4XAx7Rrk6pZG7KECrJxNghtz4sB1JIaOd2mDsz2BfU,5975
25
25
  moleditpy_linux/ui/bond_item.py,sha256=rVjORuGLp_Cxhmw7ARTE83QM614VSIDA5LVcIYpWWc0,25995
26
- moleditpy_linux/ui/bond_length_dialog.py,sha256=lss8J2g_pUK-BCbqWk25JMFsn-g8SxBjiBp8mZrn57I,15930
26
+ moleditpy_linux/ui/bond_length_dialog.py,sha256=XRITkrY0HaW7CHyy9npBKzkqpj-9nUaRrzUxdsDwqWI,17799
27
27
  moleditpy_linux/ui/calculation_worker.py,sha256=bwrzCW5935Twe5EY7Mtd35YQaWJILvBPwzPIQgmqz98,41849
28
28
  moleditpy_linux/ui/color_settings_dialog.py,sha256=RFp8h0R1yC8YwqOxc972Da9xO95Gu-0qywAAEeXMj4E,14224
29
- moleditpy_linux/ui/compute_logic.py,sha256=OBEJvw4atAaEXIv4eueoilIjTF1sxvJQGQB4ABOthYA,31462
29
+ moleditpy_linux/ui/compute_logic.py,sha256=ZrX0sKENLA7kTs5vvm6eS83ggYOlaUVaee-U6xM9_k4,32679
30
30
  moleditpy_linux/ui/constrained_optimization_dialog.py,sha256=rc3YK6DOJdIbhriwsM2jOAW9j_-3PvFz1XtFHpGw4E4,36346
31
31
  moleditpy_linux/ui/custom_interactor_style.py,sha256=ut4p-S4iUr8lTVp2T1m_-d7l9pSUCoOdzk08qssECn8,65499
32
32
  moleditpy_linux/ui/custom_qt_interactor.py,sha256=76L0Jpbg09VXzHOzJs9kG260Vs9qcwbt8Chn0iOopDg,2820
33
33
  moleditpy_linux/ui/dialog_3d_picking_mixin.py,sha256=nsZQ27PYZrO9s_w5EXxxluVfOOmNKcv0vvm89IWN7mU,10604
34
34
  moleditpy_linux/ui/dialog_logic.py,sha256=KCz3ZPJap4K_tiNDjCzGqQHYxdEsvcUG6Lws2tDhvx4,19617
35
- moleditpy_linux/ui/dihedral_dialog.py,sha256=0-EeJZG6YCehCWwqCEOougzZjF-zyzKEHCUC_cls7GE,18833
35
+ moleditpy_linux/ui/dihedral_dialog.py,sha256=FZP1gZ9fgDvIc7VPa0rR1TJ8dGytRZc3Hel1lH5tn0k,19932
36
36
  moleditpy_linux/ui/edit_3d_logic.py,sha256=DcdZZwPRkJ0RIReyTyvelkZILYG-vggop92dz2hkYSY,19192
37
- moleditpy_linux/ui/edit_actions_logic.py,sha256=no8SM7munZv7Nb5tEEaRr9vCIiDgYCns9wK4QZ1Tq4c,66656
37
+ moleditpy_linux/ui/edit_actions_logic.py,sha256=rVnhlhbsC1bEDNaGDUwr7hjfNVXGwrj_6IogdLfYQZI,66675
38
38
  moleditpy_linux/ui/export_logic.py,sha256=lZnzTLIEy_UWHiZpJ5Decr1-fy1JFMVjJNgXXc2cs2o,43765
39
39
  moleditpy_linux/ui/geometry_base_dialog.py,sha256=jj9uI2cexEjC3WxFm8HAlsrGUcTX6lQv-BX70hkSW7o,4472
40
- moleditpy_linux/ui/io_logic.py,sha256=X-xxKU2Eo4QkGqdinC7DdWXUWJKVr_8z-gKBquNHoSM,50960
40
+ moleditpy_linux/ui/io_logic.py,sha256=LbMK-BQOe4IHSWoXaBH4nwBIYkUQOv4G9q4cTxl3cxg,51713
41
41
  moleditpy_linux/ui/main_window.py,sha256=qVSN2XclGCBsRCRtC10xBUn5VdzPer0rzMPHOuYw2ZU,12553
42
42
  moleditpy_linux/ui/main_window_init.py,sha256=94_Sf_E5ZTtkyQvSIURWXbuXxabg46L0AxOMsVPKHf8,77244
43
43
  moleditpy_linux/ui/mirror_dialog.py,sha256=Tsc2O9Ktjz0zzJWB_YkVgkqgDhytQ7ksN_PwpcY9vJI,5169
@@ -49,7 +49,7 @@ moleditpy_linux/ui/periodic_table_dialog.py,sha256=yadE1bbJaObAiIfpKwANqiDOBI8R3
49
49
  moleditpy_linux/ui/planarize_dialog.py,sha256=PvEXAx27iO3d9OWrr0kL4WTeNI2eMIx-FRZaFWW-cao,7936
50
50
  moleditpy_linux/ui/plugin_menu_manager.py,sha256=EXju-Bcy-Wxg_MpJLsV2nUgjlwVpyqZBVU5VohzUk5w,22101
51
51
  moleditpy_linux/ui/settings_dialog.py,sha256=tAw-Ko73oBG5o8jnsiK4MiPar88GxiAkcnE5h6aUlYo,7319
52
- moleditpy_linux/ui/string_importers.py,sha256=cgsMoIYRKKP-RZZzhcpCdlDqr8KJgmazZdJ7C3GCSaE,8078
52
+ moleditpy_linux/ui/string_importers.py,sha256=KTAnbJp1NBlCvxsC8iuUrOCR-DuIu2k_lJNo5cWTe_c,8134
53
53
  moleditpy_linux/ui/template_preview_item.py,sha256=exJ4r3qmLOCQrEgl9ktNu4zTrgqixZcq82rUb9flWiM,7743
54
54
  moleditpy_linux/ui/template_preview_view.py,sha256=XMJ5tg4CdF2QA_6Yt0DMXAMWFugmiOadhIEvLpclsbo,3817
55
55
  moleditpy_linux/ui/translation_dialog.py,sha256=jAEHOCICixT1TcsQHnW-8h1o1HMxPC8Mw3_qYrUaoI0,16663
@@ -60,7 +60,7 @@ moleditpy_linux/ui/zoomable_view.py,sha256=Kpr9SaIYAKH-Fexm5-gi3cswsiT5V7g7yTk7a
60
60
  moleditpy_linux/ui/settings_tabs/__init__.py,sha256=BEOaCz93HzgTe0FhBzSj6Sfk8MAWxcY9vCDGmOriJx4,267
61
61
  moleditpy_linux/ui/settings_tabs/settings_2d_tab.py,sha256=GbiRYuf1Jem7TsdXL9Kcdp_SNx4vuBG6f5_PanJgBU0,15294
62
62
  moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py,sha256=GqHS8Ni17fPyPAS1ElnLr4Q5Fr2zDjsU2qA8VHe-fP0,13910
63
- moleditpy_linux/ui/settings_tabs/settings_other_tab.py,sha256=gCg9ambXvN9WCYWaH5qDgbQzvom02Vscrbp0Pt4NrFM,7295
63
+ moleditpy_linux/ui/settings_tabs/settings_other_tab.py,sha256=24WjETL2-xRfe57GKMolBF41VfOiWGs1U9mym-YmwSE,7283
64
64
  moleditpy_linux/ui/settings_tabs/settings_tab_base.py,sha256=c7nX6I0-8mgemI9q9cVSwDWtJSOU36laEfNiq7hOprE,3619
65
65
  moleditpy_linux/utils/__init__.py,sha256=BEOaCz93HzgTe0FhBzSj6Sfk8MAWxcY9vCDGmOriJx4,267
66
66
  moleditpy_linux/utils/constants.py,sha256=73UzGo1aJBp_jDulb_1F2TUz6YTcI_4_wWuOV3qnexM,8016
@@ -68,9 +68,9 @@ moleditpy_linux/utils/default_settings.py,sha256=c3FfGWpaCvUYLJ57fdMhWyqGEeaYEbC
68
68
  moleditpy_linux/utils/sip_isdeleted_safe.py,sha256=aXoW_PDHAsu0SkqcvkUZAsWDhl9Ka8-VnzgNE5ub41Y,1215
69
69
  moleditpy_linux/utils/suppress_log.py,sha256=EFkN79q6QfYXAuii8g1fBuvFJrnVsio3aFTKMvAmXW4,928
70
70
  moleditpy_linux/utils/system_utils.py,sha256=j1RAiiKXg3N-rjpTh7yxKVAYED_kUJy4aVrY-9Hh0Mc,2674
71
- moleditpy_linux-4.5.0.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
72
- moleditpy_linux-4.5.0.dist-info/METADATA,sha256=K2BAcuLvCky-mQGYbzrNRCgpOhn5Zh_CUjmyc_Emmas,65353
73
- moleditpy_linux-4.5.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
74
- moleditpy_linux-4.5.0.dist-info/entry_points.txt,sha256=-OzipSi__yVwlimNtu3eiRP5t5UMg55Cs0udyhXYiyw,60
75
- moleditpy_linux-4.5.0.dist-info/top_level.txt,sha256=qyqe-hDYL6CXyin9E5Me5rVl3PG84VqiOjf9bQvfJLs,16
76
- moleditpy_linux-4.5.0.dist-info/RECORD,,
71
+ moleditpy_linux-4.5.1.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
72
+ moleditpy_linux-4.5.1.dist-info/METADATA,sha256=PxbRmbwq4tWHpN0Xcit8pzgbN5MBbLwRVcyPaG9pITg,65353
73
+ moleditpy_linux-4.5.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
74
+ moleditpy_linux-4.5.1.dist-info/entry_points.txt,sha256=-OzipSi__yVwlimNtu3eiRP5t5UMg55Cs0udyhXYiyw,60
75
+ moleditpy_linux-4.5.1.dist-info/top_level.txt,sha256=qyqe-hDYL6CXyin9E5Me5rVl3PG84VqiOjf9bQvfJLs,16
76
+ moleditpy_linux-4.5.1.dist-info/RECORD,,