Architecture: pdb2reaction¶
1. Overview¶
pdb2reaction is a Python CLI for enzymatic reaction-path analysis on an
active-site cluster model. It uses built-in MLIPs or a custom ASE calculator
for geometry/path stages and can add an optional PySCF/GPU4PySCF DFT
single-point stage (extract → MEP → tsopt → IRC → freq → dft).
Two bundled forks (pysisyphus/, thermoanalysis/) live at the repo top as repo-internal modules. They are deliberately not the upstream PyPI distributions; reinstalling them from PyPI alongside this package silently breaks the local extensions. See §6.
2. Layered structure (6 core layers)¶
The package also ships a 7th, optional-integration subpackage
pdb2reaction/mcp/(MCP server:server.py,_tools.py,_runner.py; entry pointsp2r-mcp/pdb2reaction-mcp). It sits outside the 6-layer core graph below and is documented in mcp_server.md.
2.1 Layer table¶
layer |
dir |
responsibility |
may depend on |
|---|---|---|---|
L1 Interface |
|
Click root group, shared option-decorator factories ( |
|
L2 Application |
|
per-subcommand orchestration; one file per stage runner ( |
|
L3 Domain |
|
chemistry-aware helper logic (bond change detection, bond summary, element-info propagation) |
|
L4a Infra (MLIP) |
|
MLIP backend dispatcher + per-backend adapter (UMA / Orb / MACE / AIMNet2) |
|
L4b Infra (I/O) |
|
output layout, summary, trajectory, PDB fix, energy diagram, Hessian cache |
|
L5 Foundation |
|
defaults (primary source for shared defaults), utilities, logging, output, and result publication |
(none, by design intent) |
(bundle, not a layer) |
|
repo-internal forks (optimizer / thermochemistry) |
(sibling, layer-external) |
Dependency direction (design goal): L1 → L2 → {L3, L4} → L5. The full top-to-bottom direction is not mechanically enforced. The enforced subset keeps the product module graph acyclic (no strongly connected component among pdb2reaction/* modules) and prevents any core/domain module from importing a workflows/* module. Two CI gates cover different invariants: .github/scripts/check_engineering_markers.py verifies # CHEMISTRY-RULE:{4,5,7} marker coverage, the # DOMAIN_PURE markers on workflows/dft.py, workflows/tsopt.py, and workflows/sp.py, and that the MLIP SDKs (fairchem / orb_models / mace / aimnet) are imported only under backends/; .github/scripts/check_import_graph.py (a static AST import graph) asserts there is no product cycle, no pysisyphus/** import of pdb2reaction, and no core → workflows / domain → workflows back-edge. Remaining allowed downward edges / one-way facades are: workflows/* → cli.common_options / cli.decorators / cli.help_pages; core/utils.py → domain.add_elem_info, io.structure_formats, and io.charge; io/charge.py → domain.residue_data and io.structure_formats; io/structure_formats.py → domain.add_elem_info; and io/trj2fig.py → backends. The canonical residue tables (domain/residue_data.py), the charge engine (io/charge.py), and the console-gated charge-summary logger (core/utils.py) are re-exported from workflows/extract.py so existing import paths keep working. Bundled forks sit outside the layer graph and may be imported from any layer via their absolute package path (from pysisyphus.X import Y).
2.2 ASCII map of the package tree¶
pdb2reaction/ [GH: t-0hmura/pdb2reaction]
├── pyproject.toml packages.find = ["pdb2reaction*", ...] (glob, frozen)
├── README.md / CONTRIBUTING.md / CHANGELOG.md
├── docs/
│ ├── architecture.md ← this file
│ └──... (Sphinx documentation site)
├── pdb2reaction/ ← package body, 6-layer physical dir
│ ├── __init__.py PEP 562 lazy: _LAZY_SYMBOLS / _LAZY_MODULES + __getattr__
│ ├── __main__.py `from .cli import cli`
│ ├── _version.py / py.typed
│ │
│ ├── cli/ # === L1 Interface ===
│ │ ├── app.py Click group + _LAZY_SUBCOMMANDS registry (absolute paths)
│ │ ├── common_options.py @add_print_every_option / @add_irc_pos_def_option / @add_precision_option / @add_coord_type_option / @add_ml_charge_spin_options
│ │ ├── decorators.py resolve_yaml_sources / load_merged_yaml_cfg / _write_error_json
│ │ ├── help_pages.py --help-advanced pager
│ │ ├── bool_compat.py --flag / --no-flag normalization
│ │ └── default_group.py subcommand resolver, lazy module import
│ │
│ ├── workflows/ # === L2 Application ===
│ │ ├── all.py full pipeline orchestrator (extract → … → DFT)
│ │ ├── path_search.py / path_opt.py MEP search / COS wrapper
│ │ ├── tsopt.py / freq.py / irc.py / dft.py per-stage runners
│ │ ├── opt.py / sp.py / scan.py / scan2d.py /
│ │ │ scan3d.py / scan_common.py geometry opt / single point / scans
│ │ ├── extract.py active-site extraction CLI
│ │ ├── restraints.py restraint helpers
│ │ └── align_freeze.py Kabsch + frozen-subset rmsd
│ │
│ ├── domain/ # === L3 Domain ===
│ │ ├── bond_changes.py R↔P bond detection
│ │ ├── bond_summary.py post-IRC diagnostic
│ │ ├── add_elem_info.py PDB element column normalizer
│ │ └── residue_data.py residue/ion charge tables
│ │
│ ├── backends/ # === L4a Infra (MLIP) ===
│ │ ├── __init__.py backend dispatch + registry
│ │ ├── base.py MLIPCalculator protocol
│ │ ├── custom.py custom ASE-calculator adapter
│ │ ├── _determinism.py deterministic reduction shim
│ │ └── uma.py / orb.py / mace.py / aimnet2.py per-backend adapters
│ │
│ ├── io/ # === L4b Infra (I/O) ===
│ │ ├── summary.py summary.json / summary.log writer
│ │ ├── energy_diagram.py Plotly diagram
│ │ ├── trj2fig.py trajectory → PNG / SVG / PDF / HTML / CSV
│ │ ├── pdb_fix.py altloc resolution
│ │ ├── altloc.py altloc identity/selection helpers
│ │ ├── charge.py residue-aware charge resolution
│ │ ├── structure_formats.py PDB/mmCIF bridge + identifier restoration
│ │ └── hessian_cache.py in-memory Hessian cache
│ │
│ └── core/ # === L5 Foundation ===
│ ├── defaults.py primary source for shared defaults
│ ├── utils.py PDB / XYZ / plot helpers
│ ├── output.py / result_commit.py output/result ownership
│ └── pes_composition.py energy-component composition
│
├── tests/ smoke / unit
├── .github/ workflows/ + scripts/ (CI, release, engineering, and documentation checks)
└── (repo-top sibling, layer-external bundled forks)
pysisyphus/ repo-internal fork (slimmed to the numerical features used here)
thermoanalysis/ repo-internal fork
2.3 Per-layer responsibility detail¶
L1 cli/ owns the root group, lazy registry, argv normalization, progressive help, and shared option-decorator factories. Workflow and utility modules own their actual @click.command() objects and may define command-local options inline; the root resolves and invokes those objects. Every _LAZY_SUBCOMMANDS entry uses an absolute module path (pdb2reaction.workflows.all, pdb2reaction.io.trj2fig, …).
L2 workflows/ contains compute-command modules plus shared stage helpers such as scan_common.py, restraints.py, and align_freeze.py. A command module normally exposes one @click.command() named cli; utility commands also live in io/ and domain/, so neither “one file per subcommand” nor “every workflow file is a command” is an invariant.
L3 domain/. Chemistry-aware helper logic that may import torch / numpy / pysisyphus.constants (numeric backends), but may not import MLIP runtimes (fairchem, orb_models, mace, aimnet). .github/scripts/check_engineering_markers.py enforces this deny list via an external-library import-scope check across non-backends/ files. (The # DOMAIN_PURE docstring marker itself lives on selected workflow modules — workflows/dft.py, tsopt.py, sp.py — not on domain/.) Domain helpers are reusable by any L2 stage runner.
L4a backends/. MLIP backend dispatcher (__init__.py + base.py) plus one adapter per supported MLIP (uma.py, orb.py, mace.py, aimnet2.py). The dispatcher also exposes the custom ASE-calculator boundary.
L4b io/. Output-side I/O concerns: per-stage summary writer, energy diagram, trajectory rendering, PDB altloc fix, PDB/mmCIF bridge/template restoration, and in-memory Hessian cache. Output format is owned here and consumed by stage runners; current non-foundation imports are listed in the back-edge inventory above.
L5 core/ is the lowest layer. defaults.py is the primary source for shared numerical and CLI defaults; grep it first, then inspect justified command-local defaults (for example, path-engine choices). utils.py contains shared configuration, structure, coordinate, and plotting helpers.
2.4 Lazy-import mechanism (conceptual diagram)¶
External consumer Package root Layer dir
--------------------------------------- ---------------------- ---------
from pdb2reaction.core.utils import x ──► (direct dotted import) ──► pdb2reaction/core/utils.py
import pdb2reaction.io.trj2fig ──► (direct dotted import) ──► pdb2reaction/io/trj2fig.py
from pdb2reaction import <Symbol> ──► pdb2reaction/__init__.py
__getattr__("<Symbol>")
└─► _LAZY_SYMBOLS["<Symbol>"]
= "pdb2reaction.<layer>.<module>"
└─► importlib.import_module(...)
from pdb2reaction import <module> ──► pdb2reaction/__init__.py
(= module attr) __getattr__("<module>")
└─► _LAZY_MODULES["<module>"]
= "pdb2reaction.<layer>.<module>"
└─► importlib.import_module(...) returns module
pdb2reaction myaction ──► pdb2reaction/cli/app.py
_LAZY_SUBCOMMANDS["myaction"]
= ("pdb2reaction.workflows.myaction", "cli", "...")
└─► importlib.import_module(absolute path)
└─► getattr(module, "cli") → Click command
Two layers of lazy-import compatibility plus CLI dispatch:
Root symbol attribute (
from pdb2reaction import <Symbol>) — handled bypdb2reaction/__init__.py:_LAZY_SYMBOLS+ PEP 562__getattr__. Symbols are loaded on first access from the layer-dir path; import cost stays zero atpdb2reactionimport time.Root module attribute (
from pdb2reaction import <module>) — handled by_LAZY_MODULES.__getattr__returns the module object itself viaimportlib.import_module. The registry is currently empty, so no root module-attribute paths are exported.
The CLI subcommand resolver (cli/app.py:_LAZY_SUBCOMMANDS) uses absolute module paths (e.g. "pdb2reaction.workflows.all") so that moving default_group.py into cli/ does not silently break subcommand discovery (the registry no longer depends on __package__).
4. File index — “where does this concern live?”¶
4.1 CLI / entry (L1 cli/)¶
concern |
file |
|---|---|
Click root group + subcommand dispatch |
|
Subcommand resolver (lazy import) |
|
|
|
YAML source resolution + standardized exception handling |
|
|
|
Bool flag compat ( |
|
Shared option-decorator factories ( |
|
4.2 Workflow stage runners (L2 workflows/)¶
concern |
file |
|---|---|
Full pipeline orchestrator |
|
Geometry optimization (L-BFGS / RFO) |
|
Scan and 2D/3D energy-landscape grids + shared |
|
MEP search (GSM) |
|
MEP optimizer core (pysisyphus COS) |
|
TS optimization (RS-P-RFO + Bofill + macro/micro) |
|
Vibrational analysis (PHVA + backend-agnostic active-DOF Hessian handling) |
|
IRC integration (macro / micro) |
|
Single-point DFT (gpu4pyscf subprocess) |
|
Active-site extraction (cluster cap) |
|
Restraint helpers |
|
Kabsch / frozen-subset alignment |
|
4.3 Chemistry helpers (L3 domain/)¶
concern |
file |
|---|---|
R↔P bond change detection |
|
Post-IRC bond summary |
|
PDB element column normalizer |
|
Residue and ion charge tables |
|
4.4 MLIP backends (L4a backends/)¶
concern |
file |
|---|---|
Backend dispatch + registry |
|
|
|
Custom ASE-calculator adapter |
|
Deterministic reduction shim |
|
Per-backend adapters |
|
See Backends for the add-a-backend recipe.
4.5 I/O (L4b io/)¶
concern |
file |
|---|---|
|
|
Plotly energy diagram |
|
Trajectory → PNG / SVG / PDF / HTML / CSV |
|
PDB altloc resolution |
|
Altloc identity and coherent selection |
|
Residue-aware charge resolution |
|
PDB/mmCIF bridge + identifier restoration |
|
In-memory Hessian cache (per-run TTL) |
|
Harmonic restraint setup |
|
4.6 Foundation (L5 core/)¶
concern |
file |
|---|---|
Shared/default numerical settings (primary source; verify command-local exceptions) |
|
PDB / XYZ / plot helpers |
|
|
|
Output/result ownership helpers |
|
Energy-component composition |
|
4.7 Repo-internal bundled forks¶
dir |
role |
selected divergent files (see the directory README for the complete list) |
|---|---|---|
|
optimizer / TS / IRC engine |
|
|
thermochemistry (ΔG, ZPE, partition functions) |
|
See each dir’s README.md for the touch-restriction boundary.
6. Bundled forks (repo-internal)¶
pdb2reaction ships two repo-internal modules at the repo top:
dir |
upstream PyPI? |
purpose |
scope of edits allowed |
|---|---|---|---|
|
NO — fork, do not |
optimizer, TS, IRC, COS, calculators |
maintained locally; behavior changes need a focused regression test and matching numerical benchmark |
|
NO — fork (branding/I/O diff) |
ΔG, ZPE, partition functions, |
maintained locally; numerical/I/O changes need thermochemistry golden tests |
Each directory carries a README.md listing its divergent files and required
validation. From the layer model these forks live outside the L1..L5
graph: any layer may import them via the absolute package path
(from pysisyphus.X import Y) without breaking the layer direction.
7. Recommended deeper reading order¶
After the Fresh-eyes tour (§3), follow this depth-first reading order:
pdb2reaction/core/defaults.py— internalise the default-value table; everything downstream reads from here.pdb2reaction/cli/app.py— Click root +_LAZY_SUBCOMMANDSregistry.pdb2reaction/workflows/all.py— one full pipeline top-to-bottom.pdb2reaction/workflows/extract.py— active-site cluster cap.pdb2reaction/backends/__init__.py+base.py— MLIP dispatcher and per-backend adapter contract.pdb2reaction/workflows/tsopt.py— RS-P-RFO + Bofill scatter (CHEMISTRY-RULE:7).pdb2reaction/workflows/freq.py— vibrational analysis on the cluster model.pdb2reaction/workflows/irc.py— VRAM hygiene + IRC integration.pdb2reaction/workflows/dft.py— single-point DFT with gpu4pyscf (CHEMISTRY-RULE:4 + :5).pdb2reaction/core/utils.py— shared PDB / XYZ / plot helpers.