Wrappers and Adapters¶
MolPy provides a standardized way to integrate with external software and libraries. This integration is handled by two distinct layers:
- Wrappers: Execute external binaries/CLIs via subprocess (e.g., LAMMPS, AmberTools, Gaussian).
- Adapters: Convert data between MolPy and external library representations (e.g., RDKit, OpenMM) without subprocesses.
These tools ensure a clean separation between execution logic (Wrappers) and data translation (Adapters).
When to use what?¶
| Requirement | Solution | Example |
|---|---|---|
| Call a command-line tool | Wrapper | Running antechamber or lmp_serial |
| Use a Python library's object | Adapter | Converting Atomistic to RDKit Mol |
| Manage execution order/files | Workflow | A script combining Wrappers and Adapters |
Key Idea¶
- Wrappers are for action: they run things that have side effects (files, logs).
- Adapters are for data: they provide a synchronized view of the same system in two different formats.
Part 1: Wrappers¶
A Wrapper encapsulates a command-line tool. It handles:
- Locating the executable
- Setting up the environment (conda, venv, or shell variables)
- executing the command in a specific working directory
Wrappers are safe to instantiate even if the tool is missing (verification happens later).
Example: Creating a Simple Wrapper¶
Let's create a wrapper for the standard echo command. In a real scenario, this would be gmx, tleap, etc.
from molpy.wrapper import Wrapper
from pathlib import Path
import shutil
# 1. Define the wrapper
# We point to the 'echo' executable (available on most unix systems)
echo_wrapper = Wrapper(name="echo_tool", exe="echo")
# 2. Run the wrapper
# args is a list of strings passed to the command
process = echo_wrapper.run(args=["Hello", "from", "MolPy!"])
# 3. Check output
if process.returncode == 0:
print(f"Success! Output: {process.stdout.strip()}")
else:
print(f"Error: {process.stderr}")
Success! Output: Hello from MolPy!
Environment Management¶
Wrappers can automatically handle Conda environments or Virtualenvs. This is crucial for calling tools installed in isolated environments.
# Example: Run 'antechamber' from a specific Conda environment
ac_wrapper = Wrapper(
name="antechamber",
exe="antechamber",
env="AmberTools22", # Name of the conda env
env_manager="conda" # Use 'conda run -n ...'
)
# This will execute: conda run -n AmberTools22 antechamber -i ...
# ac_wrapper.run(args=["-i", "input.pdb", ...])
Part 2: Adapters¶
An Adapter synchronizes state between a MolPy object (Internal) and an External object. It implements a two-way sync protocol:
sync_to_external(): Update External object from Internalsync_to_internal(): Update Internal object from External
Example: Dictionary Adapter¶
Here is a simple adapter that converts a Python dict (internal) to a semicolon-separated string (external).
from molpy.adapter import Adapter
class StringDictAdapter(Adapter[dict[str, str], str]):
def _do_sync_to_external(self):
# Convert dict to "key=value;key=value"
if self._internal is None:
return
self._external = ";".join([f"{k}={v}" for k, v in self._internal.items()])
def _do_sync_to_internal(self):
# Convert string back to dict
if self._external is None:
return
self._internal = dict(item.split("=") for item in self._external.split(";") if item)
# Usage
internal_data = {"name": "MolPy", "type": "Library"}
adapter = StringDictAdapter(internal=internal_data)
# 1. Sync to external
adapter.sync_to_external()
external_string = adapter.get_external()
print(f"External representation: '{external_string}'")
# 2. Modify external and sync back
adapter.set_external("name=MolPy;type=Toolkit;status=Active")
adapter.sync_to_internal()
new_internal = adapter.get_internal()
print(f"Synced internal: {new_internal}")
External representation: 'name=MolPy;type=Library'
Synced internal: {'name': 'MolPy', 'type': 'Toolkit', 'status': 'Active'}
Real World: Geometry Optimization with RDKit¶
A powerful use case for Adapters is leveraging external libraries for complex algorithms, such as geometry optimization. Here, we use RDKit to generate 3D coordinates for a molecule created in MolPy.
from molpy import Atomistic
from molpy.adapter import RDKitAdapter
from rdkit.Chem import AllChem
# 1. Create a molecule in MolPy (Ethanol: C-C-O)
# Note: We initialize with all atoms at (0,0,0)
mol = Atomistic()
c1 = mol.def_atom(symbol="C", x=0.0, y=0.0, z=0.0)
c2 = mol.def_atom(symbol="C", x=0.0, y=0.0, z=0.0)
o1 = mol.def_atom(symbol="O", x=0.0, y=0.0, z=0.0)
mol.def_bond(c1, c2)
mol.def_bond(c2, o1)
print(f"Initial C1 position: {c1.get('x'), c1.get('y'), c1.get('z')}")
# 2. Use Adapter to bridge to RDKit
adapter = RDKitAdapter(internal=mol)
rd_mol = adapter.get_external()
# 3. Perform Geometry Optimization in RDKit
# Embed (generate initial 3D coords) and Optimize (MMFF force field)
AllChem.EmbedMolecule(rd_mol)
AllChem.MMFFOptimizeMolecule(rd_mol)
# 4. Sync optimized coordinates back to MolPy
# RDKitAdapter automatically maps coordinates back to the correct atoms
adapter.set_external(rd_mol)
adapter.sync_to_internal()
print(f"Optimized C1 position: ({c1.get('x'):.2f}, {c1.get('y'):.2f}, {c1.get('z'):.2f})")
print(f"Optimized O1 position: ({o1.get('x'):.2f}, {o1.get('y'):.2f}, {o1.get('z'):.2f})")
Initial C1 position: (0.0, 0.0, 0.0)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[3], line 18 16 # 2. Use Adapter to bridge to RDKit 17 adapter = RDKitAdapter(internal=mol) ---> 18 rd_mol = adapter.get_external() 20 # 3. Perform Geometry Optimization in RDKit 21 # Embed (generate initial 3D coords) and Optimize (MMFF force field) 22 AllChem.EmbedMolecule(rd_mol) File ~/.asdf/installs/python/3.13.3/lib/python3.13/site-packages/molpy/adapter/base.py:55, in Adapter.get_external(self) 52 return self._external 54 if self._internal is not None: ---> 55 self.sync_to_external() 56 if self._external is not None: 57 return self._external File ~/.asdf/installs/python/3.13.3/lib/python3.13/site-packages/molpy/adapter/rdkit.py:557, in RDKitAdapter.sync_to_external(self) 556 def sync_to_external(self) -> None: --> 557 super().sync_to_external() File ~/.asdf/installs/python/3.13.3/lib/python3.13/site-packages/molpy/adapter/base.py:103, in Adapter.sync_to_external(self) 98 if self._internal is None: 99 raise ValueError( 100 "Cannot sync to external: internal representation is None. " 101 "Set internal using set_internal() first." 102 ) --> 103 self._do_sync_to_external() File ~/.asdf/installs/python/3.13.3/lib/python3.13/site-packages/molpy/adapter/rdkit.py:564, in RDKitAdapter._do_sync_to_external(self) 561 if atomistic is None: 562 return --> 564 new_mol = self._build_mol_from_atomistic(atomistic) 565 self._external = new_mol 566 self._rebuild_atom_mapper() File ~/.asdf/installs/python/3.13.3/lib/python3.13/site-packages/molpy/adapter/rdkit.py:327, in RDKitAdapter._build_mol_from_atomistic(self, atomistic) 324 if begin_idx is None or end_idx is None: 325 continue --> 327 bt = _rdkit_bond_type(bond.get("order")) 328 mol.AddBond(begin_idx, end_idx, bt) 330 # Optional coordinates: expect x/y/z on atoms File ~/.asdf/installs/python/3.13.3/lib/python3.13/site-packages/molpy/adapter/rdkit.py:37, in _rdkit_bond_type(order) 36 def _rdkit_bond_type(order: float) -> Chem.BondType: ---> 37 order_float = float(order) 38 if order_float not in BOND_ORDER_TO_RDKIT: 39 raise ValueError( 40 f"Bond order {order_float} is not supported. " 41 f"Supported orders: {list(BOND_ORDER_TO_RDKIT.keys())}" 42 ) TypeError: float() argument must be a string or a real number, not 'NoneType'
Summary¶
- Use Wrappers to run external programs. They handle paths, environments, and execution parameters.
- Use Adapters to convert data to/from other libraries in-memory. They ensure data consistency.
- Combine both to build powerful workflows that leverage the entire scientific Python ecosystem.