-
Notifications
You must be signed in to change notification settings - Fork 32
WIP: Added instance method to generate MolBar from Structure #236
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e7281ad
c2b4722
7436e2c
a8e539d
926f0da
c91504b
2314045
f9e3a81
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ | |
| ) | ||
| from opi.input.structures.coordinates import Coordinates | ||
| from opi.utils.element import ATOMIC_MASSES_FROM_ELEMENT, Element | ||
| from opi.utils.molbar import MolBarMode, call_molbar, requires_molbar | ||
| from opi.utils.rotconst import ( | ||
| PrincipalMoments, | ||
| RotationalConstants, | ||
|
|
@@ -1110,6 +1111,123 @@ def from_lists( | |
|
|
||
| return cls(atoms=atoms, charge=charge, multiplicity=multiplicity) | ||
|
|
||
| # ------------------------------------------------------------------ # | ||
| # MOLBAR # | ||
| # ------------------------------------------------------------------ # | ||
|
|
||
| @requires_molbar | ||
| def calculate_molbar( | ||
| self, | ||
| *, | ||
| mode: "MolBarMode | str" = MolBarMode.MB, | ||
| ) -> str: | ||
| """ | ||
| Compute the MolBar barcode string for this `Structure`. | ||
|
|
||
| MolBar is a molecular identifier analogous to InChI, encoding the | ||
| topology and geometry of a molecule into a canonical string. | ||
| See the MolBar documentation for a full description of the barcode format. | ||
|
|
||
| Only real `Atom` entries are passed to MolBar; `EmbeddingPotential`, | ||
| `GhostAtom`, and `PointCharge` instances are silently skipped. Note | ||
| that `GhostAtom` is a subclass of `Atom`, so an exact type check is | ||
| used rather than `isinstance`. | ||
|
|
||
| The total charge is taken from `charge`. | ||
|
|
||
| MolBar is **not** a dependency of OPI and must be installed separately:: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here you write molbar is not a dependency and below you write it is optional. We should clearly decide for one thing and go with it. Please make it an optional dependency and pin a version number we want to use.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added it as an optional dependency please document it accordingly. |
||
|
|
||
| pip install molbar | ||
|
|
||
| Parameters | ||
| ---------- | ||
| mode : MolBarMode | str, default MolBarMode.MB | ||
| MolBar calculation mode. Accepts a `MolBarMode` member or a | ||
| plain string (case-insensitive). `"mb"` computes the full barcode; | ||
| `"topo"` computes only the topology part. | ||
|
|
||
| Returns | ||
| ------- | ||
| str | ||
| The MolBar barcode string. | ||
|
|
||
| Raises | ||
| ------ | ||
| ImportError | ||
| If MolBar is not installed. | ||
| ValueError | ||
| If *mode* is not a valid `MolBarMode` value. | ||
| ValueError | ||
| If this structure contains no real atoms. | ||
|
|
||
| See Also | ||
| -------- | ||
| calculate_molbar_data : Returns the barcode together with the full MolBar data dictionary. | ||
| """ | ||
| return cast( | ||
| str, | ||
| self._get_molbar_from_coordinates(self._validate_molbar_mode(mode), return_data=False), | ||
| ) | ||
|
|
||
| @requires_molbar | ||
| def calculate_molbar_data( | ||
| self, | ||
| *, | ||
| mode: "MolBarMode | str" = MolBarMode.MB, | ||
| ) -> "tuple[str, dict[str, Any]]": | ||
| """ | ||
| Compute the MolBar barcode string and full data dictionary for this | ||
| `Structure`. | ||
|
|
||
| Behaves identically to `calculate_molbar` regarding MolBar installation, | ||
| atom filtering, and mode selection — see `calculate_molbar` for details. | ||
|
|
||
| The data dictionary contains the following top-level keys: | ||
|
|
||
| `"molbar"` | ||
| The barcode string, identical to the `calculate_molbar` return value. | ||
| `"atoms"` | ||
| Per-atom information after MolBar's internal geometry normalisation, | ||
| including `"atomic_numbers"`, `"positions"` (Å), and | ||
| `"partial_charges"`. | ||
| `"bonds"` | ||
| Detected bond graph: `"bond_indices"` (pairs) and `"bond_orders"`. | ||
| `"fragments"` | ||
| List of disconnected fragments found in the structure. | ||
| `"topo"` | ||
| Topology-only barcode (the first component of the full barcode). | ||
|
|
||
| See the MolBar documentation for an authoritative and up-to-date description of every key. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| mode : MolBarMode | str, default MolBarMode.MB | ||
| MolBar calculation mode. See `calculate_molbar` for details. | ||
|
|
||
| Returns | ||
| ------- | ||
| tuple[str, dict[str, Any]] | ||
| A two-element tuple of the MolBar barcode string and the full | ||
| MolBar data dictionary. | ||
|
|
||
| Raises | ||
| ------ | ||
| ImportError | ||
| If MolBar is not installed. | ||
| ValueError | ||
| If *mode* is not a valid `MolBarMode` value. | ||
| ValueError | ||
| If this structure contains no real atoms. | ||
|
|
||
| See Also | ||
| -------- | ||
| calculate_molbar : Returns only the barcode string. | ||
| """ | ||
| return cast( | ||
| "tuple[str, dict[str, Any]]", | ||
| self._get_molbar_from_coordinates(self._validate_molbar_mode(mode), return_data=True), | ||
| ) | ||
|
|
||
| # ------------------------------------------------------------------ # | ||
| # RMSD # | ||
| # ------------------------------------------------------------------ # | ||
|
|
@@ -1424,6 +1542,84 @@ def _iter_xyz_structures( | |
| if n_struc_limit and n_struc >= n_struc_limit: | ||
| break | ||
|
|
||
| def _get_molbar_from_coordinates( | ||
| self, | ||
| mode: MolBarMode, | ||
| return_data: bool, | ||
| ) -> "str | tuple[str, dict[str, Any]]": | ||
| """ | ||
| Shared implementation for `calculate_molbar` and `calculate_molbar_data`. | ||
|
|
||
| Collects elements and coordinates from `real_atoms` via | ||
| `get_coordinates`, and delegates to `call_molbar`. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| mode : MolBarMode | ||
| Already-validated calculation mode. | ||
| return_data : bool | ||
| If `True` returns `tuple[str, dict[str, Any]]`; if `False` returns `str`. | ||
|
|
||
| Returns | ||
| ------- | ||
| str | tuple[str, dict[str, Any]] | ||
| Either the barcode string or the barcode string together with the | ||
| full MolBar data dictionary, depending on *return_data*. | ||
|
|
||
| Raises | ||
| ------ | ||
| ValueError | ||
| If this structure contains no real atoms. | ||
| """ | ||
| if not self.real_atoms: | ||
| raise ValueError( | ||
| f"{self.__class__.__name__}: structure contains no real atoms; " | ||
| "cannot build MolBar input." | ||
| ) | ||
|
|
||
| # Only real Atom instances are passed to MolBar; EmbeddingPotential, | ||
| # GhostAtom, and PointCharge entries are excluded via real_atoms. | ||
| # Note that GhostAtom is a subclass of Atom, so type(a) is Atom is | ||
| # used rather than isinstance inside the real_atoms property. | ||
| real_indices = [i for i, a in enumerate(self.atoms) if type(a) is Atom] | ||
| elements: list[str] = [atom.element.value for atom in self.real_atoms] | ||
| coordinates: list[list[float]] = self.get_coordinates(only_atoms=real_indices).tolist() | ||
|
|
||
| return call_molbar( | ||
| elements=elements, | ||
| coordinates=coordinates, | ||
| total_charge=self.charge, | ||
| mode=mode, | ||
| return_data=return_data, | ||
| ) | ||
|
|
||
| def _validate_molbar_mode(self, mode: "MolBarMode | str") -> MolBarMode: | ||
| """ | ||
| Validate and normalise a MolBar calculation mode. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| mode : MolBarMode | str | ||
| Calculation mode to validate. | ||
|
|
||
| Returns | ||
| ------- | ||
| MolBarMode | ||
| The validated and normalised mode. | ||
|
|
||
| Raises | ||
| ------ | ||
| ValueError | ||
| If *mode* is not a valid `MolBarMode` value. | ||
| """ | ||
| # Normalise and validate mode (case-insensitive via StringEnum._missing_) | ||
| try: | ||
| return MolBarMode(mode) | ||
| except ValueError: | ||
| raise ValueError( | ||
| f"Invalid mode {mode!r}. Must be one of {[m.value for m in MolBarMode]}" | ||
| ) | ||
|
|
||
| def _filtered_atoms( | ||
| self, | ||
| only_atoms: Sequence[int], | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| """ | ||
| Optional interface to MolBar (https://git.rwth-aachen.de/bannwarthlab/molbar). | ||
|
|
||
| MolBar is not a dependency of OPI. Install it separately:: | ||
|
|
||
| pip install molbar | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from functools import wraps | ||
| from typing import Any, Callable, TypeVar, cast | ||
|
|
||
| from opi.models.string_enum import StringEnum | ||
|
|
||
| __all__ = ( | ||
| "MolBarMode", | ||
| "requires_molbar", | ||
| ) | ||
|
|
||
| # ============================================================ | ||
| # MolBar optional import | ||
| # ============================================================ | ||
|
|
||
| # MolBar is an optional dependency. The decorator below guards every method | ||
| # that calls it and raises a clear ImportError if it is not installed. | ||
| try: | ||
| from molbar.barcode import get_molbar_from_coordinates # type: ignore | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes molbar get imported whenever |
||
| except ImportError: | ||
| # Ensure the name exists in the global namespace so the decorator can | ||
| # check it unconditionally without a NameError. | ||
| get_molbar_from_coordinates = None # type: ignore[assignment,unused-ignore] | ||
|
|
||
| # ============================================================ | ||
| # Mode classification | ||
| # ============================================================ | ||
|
|
||
|
|
||
| class MolBarMode(StringEnum): | ||
| """Valid calculation modes for `Structure.to_molbar`. | ||
|
|
||
| Matching is case-insensitive: pass `"MB"`, `"mb"`, or `"Mb"` | ||
| and all will be accepted. | ||
|
|
||
| Attributes | ||
| ---------- | ||
| MB : str | ||
| Full MolBar barcode (`"mb"`). | ||
| TOPO : str | ||
| Topology-only barcode (`"topo"`). | ||
| """ | ||
|
|
||
| MB = "mb" | ||
| TOPO = "topo" | ||
|
|
||
|
|
||
| # ============================================================ | ||
| # Decorator | ||
| # ============================================================ | ||
|
|
||
| _T = TypeVar("_T") | ||
|
|
||
|
|
||
| def requires_molbar(func: Callable[..., _T]) -> Callable[..., _T]: | ||
| """ | ||
| Decorator that raises `ImportError` if MolBar is not installed. | ||
|
|
||
| Apply to any method that calls `molbar.barcode.get_molbar_from_coordinates` | ||
| to ensure a clear error message is raised at call time rather than at | ||
| import time. | ||
| """ | ||
|
|
||
| @wraps(func) | ||
| def wrapper(*args: Any, **kwargs: Any) -> _T: | ||
| if get_molbar_from_coordinates is None: | ||
| raise ImportError("MolBar is not installed. Install it with: pip install molbar") | ||
| return func(*args, **kwargs) | ||
|
|
||
| return wrapper | ||
|
|
||
|
|
||
| # ============================================================ | ||
| # Free helper function | ||
| # ============================================================ | ||
|
|
||
|
|
||
| def call_molbar( | ||
| elements: list[str], | ||
| coordinates: list[list[float]], | ||
| total_charge: int, | ||
| mode: MolBarMode, | ||
| return_data: bool, | ||
| ) -> "str | tuple[str, dict[str, Any]]": | ||
| """ | ||
| Thin wrapper around `molbar.barcode.get_molbar_from_coordinates`. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| elements : list[str] | ||
| Element symbols for all real atoms. | ||
| coordinates : list[list[float]] | ||
| Cartesian coordinates in Ångström, shape (N, 3). | ||
| total_charge : int | ||
| Total charge of the structure. | ||
| mode : MolBarMode | ||
| Already-validated calculation mode. | ||
| return_data : bool | ||
| If `True` returns `tuple[str, dict[str, Any]]`; if `False` returns `str`. | ||
|
|
||
| Returns | ||
| ------- | ||
| str | tuple[str, dict[str, Any]] | ||
| Either the barcode string or the barcode string together with the | ||
| full MolBar data dictionary, depending on *return_data*. | ||
| """ | ||
|
|
||
| return cast( | ||
| "str | tuple[str, dict[str, Any]]", | ||
| get_molbar_from_coordinates( # type: ignore[operator,unused-ignore] | ||
| coordinates, | ||
| elements, | ||
| total_charge=total_charge, | ||
| return_data=return_data, | ||
| mode=mode, | ||
| ), | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we add the link to the paper here? With the doi?