├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── build.sh ├── conda_build_config.yaml ├── examples └── input.txt ├── meta.yaml ├── pesviewer ├── __init__.py ├── gen_resonant_structs.py └── pesviewer.py └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | build 3 | dist 4 | __pycache__ 5 | *egg-info 6 | .vscode 7 | .DS_store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ruben Van de Vijver 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PESViewer 2 | #### Potential energy surface visualizer 3 | 4 | This code was originally developed by [Ruben Van de Vijver](https://github.com/rubenvdvijver). 5 | Our fork has been developed and improved on top of the original to maintain compatibility with KinBot and integrate new features. 6 | 7 | PESViewer is a code to depict and analyze a potential energy surface 8 | characterized by wells, bimolecular products, transition states and barrierless reactions. 9 | 13 | 14 | To run PESViewer, you need python >= 3.7, matplotlib, numpy and OpenBabel or RDKit to create 2D plots. 15 | 16 | While the code can be used as a standalone application, it is designed to work with KinBot, which automatically generates the input files for PESViewer. 17 | 18 | ## How to Install 19 | 20 | PESViewer can be installed both in two different ways, from the conda-forge repo (`conda install`) or by cloning this github repo and then install it locally. 21 | 22 | ### conda-forge 23 | 24 | conda install -c conda-forge pesviewer 25 | 26 | ### From Github 27 | 28 | If you want to have the very last version of PESViewer without waiting for a 29 | release or you want to modify PESViewer acccording to your needs you can clone the project 30 | from github: 31 | 32 | git clone git@github.com:zadorlab/PESViewer.git 33 | 34 | and then, from within the PESViewer directory produced after cloning, type: 35 | 36 | pip install -e . 37 | 38 | > **Note** 39 | > If you want to modify PESViewer yourself it's better to fork the project 40 | > into your own repository and then clone it. 41 | 42 | ## INPUT 43 | 44 | All the information needed for PESViewer to make plots and graph depictions of the PES is provided through a text input file. 45 | In this file, stationary points, energies, names identifiers and options are listed in different sections. 46 | An example of an input file is provided in `input.txt`. 47 | 48 | ### Stationary Points 49 | Each stationary point is written on a separate line, under the the relevant section. 50 | 51 | For wells, the name and energy of each unimolecular species must be specified under the `> ` header, as follows. Additionally, a SMILES code can be provided alongside the name and energy. This can be convenient when no xyz is present, OpenBabel fails to interpret connectivity correctly or a particular resonant structure is desired: 52 | 53 | name energy [smiles] 54 | 55 | Similarly, for bimolecular products each pair of species is written on a separate line under the `> ` header: 56 | 57 | species1_species2 energy [smiles] 58 | 59 | For reactions occurring via a transition state, the name, energy, reactant and product of the transition state must be specified under the `> ` section. Additionally, a color can be provided to highlight each reaction: 60 | 61 | name energy reactant product [color] 62 | 63 | For barrierless reactions, a similar approach is followed. The `> ` header indicatess reactions occurring without a barrier. The energy is not needed in this case: 64 | 65 | name reactant product [color] 66 | 67 | ### Options 68 | The plotting options (to be written in the input file) are listed here. 69 | 70 | 71 | | option | default | Description | Scope* | 72 | | ------- | ------- | ------- | ------- | 73 | | `title` | `0` | print a title (1) or not (0) on the PES. | T | 74 | | `units` | `kJ/mol` | energy units in the input file. | TG | 75 | | `display_units` | same as the units of `units` | energy units to be displayed. Allowed values: `kJ/mol`, `kcal/mol`, `eV` | TG | 76 | | `rounding` | `1` | number of decimals for the energy values | TG | 77 | | `energy_shift` | `0.` | shift energy scale by this amount measured in `units` | TG | 78 | | `use_xyz` | `1` | use xyz files to generate each species 2D depiction. The `*.xyz` files should be named the same as the name specified in the relevant line and placed inside a directory called `xyz` (0 switch it off). | TG | 79 | | `rescale` | `0` | which species is used to rescale all energies, the name of the relevant well or bimolecular species is needed. | TG | 80 | | `fh` | `9.` | figure height. | T | 81 | | `fw` | `18.` | figure width. | T | 82 | | `fs` | `1` | scale factor for species 2D depictions. The images generated under the `*_2d` directory need to be deleted for them to be regenerated with a new scaling. | T | 83 | | `lw` | `1.5` | line width of the potential energy vs reaction coordinate plot. | T | 84 | | `margin` | `0.2` | margin fraction on the x and y axis | T | 85 | | `dpi` | `120` | Image resolution in dpi of the molecule figures | TG | 86 | | `save` | `0` | does the plot need to be saved a an image (1) or displayed to be interactively modified (0). | T | 87 | | `plot` | `1` | whether to plot (1) or not (0) the potential energy vs reaction coordinate representation of the PES. | T | 88 | | `write_ts_values` | `1` | whether the TS energy values should be written in the plot. | T | 89 | | `write_well_values` | `1` | whether the well and bimolecular energy values should be written in the plot. | T | 90 | | `bimol_color` | `red` | color of the energy values for the bimolecular products. | T | 91 | | `well_color` | `blue` | color of the energy values of the wells. | T | 92 | | `ts_color` | `green` | color or the energy values of the ts, 'none' to use same color as line. | T | 93 | | `show_images` | `1` | boolean tells whether the molecule images should be shown on the plot. | T | 94 | | `rdkit4depict` | `1` | boolean that specifies which code to use for the 2D depiction. | TG | 95 | | `axes_size` | `10` | font size of the axes. | T | 96 | | `text_size` | `10` | font size of the energy values on the plot. | T | 97 | | `linear_lines` | `0` | plot polynomials (0) or linear lines (1) between de stationary points. | T | 98 | | `interpolation` | `hanning` | image interpolation method. | T | 99 | | `graph_edge_color` | `black` | color of the graph edges, if set to `energy`, color is scaled accordingly. | G | 100 | | `reso_2D` | `1` | enable (1) or disable (0) generation of 2D depictions for resonant structures. Additional images, one for each resonant structure named `_X` are generated under the `_2d` directory. | TG | 101 | | `path_report` | `[]` | Compute the MEP between two species. Format: `chemid_start chemid_end` | TG | 102 | | `search_cutoff` | `10` | Maximum length (in reactive steps) to search for of the path report. | TG | 103 | | `node_size_diff` | `0` | Size difference of nodes in the graph depiction based on to their stability. More stable nodes are larger. 0 to make all nodes the same size. Reasonable values: 20-40.| G | 104 | 105 | * This column shows whether the parameter impacts the traditional (T) Potential vs Reaction coordinate PES depiction, the graph (G) one, or both (TG). 106 | 107 | The other input is a folder called `xyz/` containing the coordinates of the stationary points in xyz format (`name.xyz`) 108 | (for bimolecular products, use several xyz coordinates files, `name_index.xyz`). These 109 | 110 | 111 | ## RUN 112 | 113 | With the input file `input.inp`, type: 114 | 115 | pesviewer input.inp 116 | 117 | ## OUTPUT 118 | 119 | When the code runs and the depiction of the molecules is requested, first, all depictions are generated based on the content of the `/xyz` folder. On repeated runs the depictions are reused, unless missing. 120 | 121 | **Traditional PES depiction** 122 | 123 | The output is a modifiable matplotlib figure, which can be displayed and interactively arranged, or saved. 124 | The possible modifications are: 125 | - modifing the x-position of a stationary point by draggning the energy value 126 | - modifing the position of 2D structure images by dragging the image 127 | - zooming in an out. 128 | 129 | Helper files with `.txt` extension are also generated saving the positions of stationary points and images, which, in certain cases need to be deleted to recreate the plot on subsequent runs (e.g., when changing the `fs` parameter). However, the information about the adjustmens made are stored here. 130 | 131 | By selecting a stationary point, all direct neighbors and pathways are lit up, and the others are dimmed to help navigation. 132 | 133 | This is an example of a nicely arranged traditional PES plot from our recent paper: 134 | 135 | ![image](https://user-images.githubusercontent.com/40675474/227331800-373cf4b7-5d17-4f7a-8347-06544badc5b8.png) 136 | 137 | * Martí, C., Michelsen, H. A., Najm, H. N., Zádor, J.: _Comprehensive kinetics on the C7H7 potential energy surface under combustion conditions._ J. Phys. Chem. A, **2023**, 127, 1941–1959. https://pubs.acs.org/doi/full/10.1021/acs.jpca.2c08035 138 | 139 | **Interactive graph representation** 140 | 141 | Here, wells (black-border circles) and bimolecular products (blue-border circles) are shown as nodes of a graph. Their energies are displayed too, and the name of the species can be read by hovering over their depiction. Transition states are shown as edges. Edges representing saddle points are black (unless specified), barrierless reaction pathways are gray (unless specified). The thickness of the edge is inversely proportional to the absolute height of the barrier, and the energy can be read when hovering over the edge. Optionally, the color of the edges can be controlled using the 142 | 143 | This representation uses the [pyvis](https://pyvis.readthedocs.io/en/latest/) package, generates an `html` file, which can be opened in a browser `(file://)`. Note the settings below the graph to help arrange the graph using intuitive physics analogies, which can also be turned off. 144 | 145 | This is a static screenshot example from the same publication - follow this link https://pubs.acs.org/doi/full/10.1021/acs.jpca.2c08035 for the Supporting Information to download interactive plots like this. 146 | 147 | ![image](https://user-images.githubusercontent.com/40675474/227336672-c7448207-fc3b-42c3-ad89-7da45f84e985.png) 148 | 149 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zadorlab/PESViewer/10689bf73d7554f029dc7462005c8c0b9956c278/__init__.py -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | $PYTHON setup.py build 3 | $PYTHON setup.py install develop --user 4 | -------------------------------------------------------------------------------- /conda_build_config.yaml: -------------------------------------------------------------------------------- 1 | python: 2 | - 2.7 3 | - 3.6 4 | -------------------------------------------------------------------------------- /examples/input.txt: -------------------------------------------------------------------------------- 1 | > 2 | This comment is not interpreted, so store any extra info here. 3 | Keywords are case insensitive. Look at the help below. 4 | IMPORTANT: avoid the use of '2d' and '3d' in the names of species, transition states and reactions 5 | (these strings are employed when generating the 2d and 3d files of the molecules) 6 | If you want to use 3D coordinates, store them in a xyz/ directory in the same directory as the python script 7 | 8 | This example is based on figure 1 in the article: 9 | J Zador et al. Phys Chem Chem Phys 11 (46), 11040-11053. 2009 Oct 13. 10 | 11 | 12 | 13 | > propene_hydroxyl 14 | 15 | > 16 | title 0 # print a title (1) or not (0) 17 | units kcal/mol # energy units 18 | use_xyz 1 # use xyz, put 0 to switch off 19 | rescale 0 # no rescale , put the well or bimolecular name here to rescale to that value 20 | fh 9. # figure height 21 | fw 18. # figure width 22 | margin 0.2 # margin fraction on the x and y axis 23 | dpi 120 # dpi of the molecule figures 24 | save 0 # does the plot need to be saved (1) or displayed (0) 25 | write_ts_values 1 # booleans tell if the ts energy values should be written 26 | write_well_values 1 # booleans tell if the well and bimolecular energy values should be written 27 | bimol_color red # color of the energy values for the bimolecular products 28 | well_color blue # color of the energy values of the wells 29 | ts_color green # color or the energy values of the ts, put to 'none' to use same color as line 30 | show_images 1 # boolean tells whether the molecule images should be shown on the graph 31 | rdkit4depict 1 # boolean that specifies which code to use for the 2D depiction 32 | axes_size 10 # font size of the axes 33 | text_size 10 # font size of the energy values on the graph 34 | linear_lines 0 # plot polynomials (0) or linear lines (1) between de stationary points 35 | graph_edge_color None # color of graph edge, if set to 'energy', will be scaled accordingly 36 | 37 | > 38 | vdW -2.2 C[CH]CO 39 | b_rad -27.2 C[CH]CO 40 | a_rad -30.9 CC[CH]O 41 | o_rad -24.9 CCC[O] 42 | c_rad -20.9 [CH2]CCO 43 | 44 | > 45 | propene_oh 0. C=CC O 46 | allylalcohol_h 7.9 C=CCO [H] 47 | propenol_h 2.1 OC=CC [H] 48 | ethene_CH2OH -5.3 C=C [CH2]O 49 | propanal_h -6.8 CCC=O [H] 50 | vinylalcohol_methyl -9.0 C=CO [CH3] 51 | ethyl_formaldehyde -11.6 [CH2]C C=O 52 | oxetane_h 19.6 C1CCO1 [H] 53 | epoxypropane_h 16.1 CC1OC1 [H] 54 | cyclopropanol_h 14.4 C1CC1O [H] 55 | 56 | > 57 | entrance -1.8 vdW b_rad 58 | cycl1 31.8 b_rad epoxypropane_h 59 | isom1 12.9 b_rad a_rad 60 | beta1 6.2 b_rad propenol_h 61 | isom2 13.1 b_rad o_rad 62 | isom3 5.6 b_rad c_rad 63 | beta2 9.6 b_rad allylalcohol_h 64 | isom4 12.7 a_rad o_rad 65 | cycl2 30.4 a_rad cyclopropanol_h 66 | isom5 6.6 a_rad c_rad 67 | beta3 3.7 a_rad propenol_h 68 | beta4 3.6 a_rad propanal_h 69 | beta5 0.1 a_rad vinylalcohol_methyl 70 | isom6 -0.1 o_rad c_rad 71 | cycl3 36.7 o_rad oxetane_h 72 | cycl4 35.0 o_rad cyclopropanol_h 73 | beta6 10.9 o_rad allylalcohol_h 74 | alpha1 2.7 o_rad ethene_CH2OH 75 | cycl5 41.9 c_rad oxetane_h 76 | cycl6 31.6 c_rad epoxypropane_h 77 | beta7 -0.6 c_rad propanal_h 78 | beta8 -6.6 c_rad ethyl_formaldehyde 79 | 80 | > 81 | b1 propene_oh vdW 82 | 83 | > 84 | File follows the rules of SD file format for keywords. Keywords are case 85 | insensitive when parsed. 86 | Keywords: 87 | units: units of the energies supplied above 88 | 89 | usexyz: use the xyz coordinates of all the species and render a 2D/3D depiction 90 | 91 | rescale: energies are rescaled relative to the energy of the species given here 92 | 93 | wells: all the wells of the PES, separated by lines 94 | each line contains the name, the energy, and optionally the smiles 95 | 96 | bimolec: all the bimolecular products of the PES, separated by lines 97 | each line contains the name, the energy, and optionally the smiles of both bimolecular products 98 | 99 | ts: all the transition states of the PES, separated by lines 100 | each line contains the name, the energy, and the names of the reactant and product 101 | 102 | barrierless: all the barrierless reactions of the PES, separated by lines 103 | each line contains the name and the names of the reactant and product 104 | 105 | 106 | -------------------------------------------------------------------------------- /meta.yaml: -------------------------------------------------------------------------------- 1 | {% set name = "pesviewer" %} 2 | {% set version = "1.2.0" %} 3 | 4 | package: 5 | name: {{ name|lower }} 6 | version: {{ version }} 7 | 8 | source: 9 | url: https://github.com/zadorlab/PESViewer/archive/{{ version }}.tar.gz 10 | sha256: b466288c20ca417a520e128671a58e124381bed037353c7b3571e178eb16df3b 11 | 12 | build: 13 | noarch: python 14 | script: {{ PYTHON }} -m pip install . -vv 15 | number: 0 16 | entry_points: 17 | - pesviewer = pesviewer.pesviewer:main 18 | 19 | requirements: 20 | host: 21 | - python >=3.8 22 | - pip 23 | run: 24 | - python >=3.8 25 | - numpy >=1.19.0 26 | - matplotlib 27 | - networkx 28 | - pillow 29 | - rdkit 30 | - openbabel 31 | - pyvis 32 | 33 | 34 | test: 35 | imports: 36 | - pesviewer 37 | commands: 38 | - pip check 39 | requires: 40 | - pip 41 | 42 | about: 43 | home: https://github.com/zadorlab/PESViewer 44 | license: MIT 45 | license_family: MIT 46 | license_file: LICENSE 47 | summary: "Potential Energy Surface Visualizer" 48 | 49 | description: | 50 | PESViewer is a code to depict and analyze a potential energy surface 51 | characterized by wells, bimolecular products, transition states and 52 | barrierless reactions. 53 | doc_url: https://github.com/zadorlab/PESViewer/wiki 54 | dev_url: https://github.com/zadorlab/PESViewer 55 | 56 | extra: 57 | recipe-maintainers: 58 | - juditzador 59 | -------------------------------------------------------------------------------- /pesviewer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zadorlab/PESViewer/10689bf73d7554f029dc7462005c8c0b9956c278/pesviewer/__init__.py -------------------------------------------------------------------------------- /pesviewer/gen_resonant_structs.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import itertools 3 | from copy import deepcopy 4 | 5 | import rdkit 6 | from rdkit import Chem 7 | from rdkit.Chem import Draw 8 | from rdkit.Chem.rdchem import BondType 9 | from rdkit import rdBase, RDLogger 10 | 11 | 12 | def num_rad_elecs(molec: rdkit.Chem.Mol) -> int: 13 | """Count the total number of radical electrons in a molecule. 14 | 15 | @param molec: rdkit Mol object of the target molecule. 16 | """ 17 | return sum([a.GetNumRadicalElectrons() for a in molec.GetAtoms()]) 18 | 19 | 20 | def gen_bond_combs(n_bonds: int, n_conns: int) -> list: 21 | """Generates all possible combinations of single double and triple bonds. 22 | 23 | @param n_bonds: Number of shared pairs of electrons: A double bond counts 24 | as two. 25 | @param n_conns: Number of connections between atoms: A double bond counts 26 | as one. 27 | 28 | The only condition to fulfill at this step is that the sum of each of 29 | the combinations has to be equal to n_bonds. This accepts configurations 30 | breaking the octet. 31 | """ 32 | iters = [[1, 2, 3]] * n_conns 33 | combs = [] 34 | for comb in itertools.product(*iters): 35 | if sum(comb) == n_bonds: 36 | combs.append(comb) 37 | return combs 38 | 39 | 40 | def filter_valid_structs(mol: rdkit.Chem.Mol, combs: list, hvy_bond_ids: list) -> list: 41 | """Removes all configurations exceeding the octet rule, radicals are allowed. 42 | 43 | @param mol: rdkit Mol object of the target molecule. 44 | @param combs: Possible combinations of bond configuration for the molecule. 45 | @param hvy_bond_ids: List of bonds connecting non-H atoms (C-C, C-O, etc.) 46 | """ 47 | # Disable logging 48 | logger = RDLogger.logger() 49 | logger.setLevel(RDLogger.ERROR) 50 | rdBase.DisableLog('rdApp.error') 51 | 52 | atomic_valences = {'C': [4], 53 | 'N': [3, 4, 5], 54 | 'O': [2], 55 | 'H': [1], 56 | 'S': [2, 4, 6], 57 | 'F': [1], 58 | 'Cl': [1, 3, 5, 7], 59 | 'Br': [1, 3, 5, 7], 60 | 'I': [1, 3, 5, 7], 61 | 'Xe': [0]} 62 | valid_mols = [] 63 | for comb in combs: 64 | new_mol = deepcopy(mol) 65 | for i, bo in enumerate(comb): 66 | bond = new_mol.GetBondWithIdx(hvy_bond_ids[i]) 67 | bond.SetBondType(BondType(bo)) 68 | 69 | # Don't include unphysical structures/ 70 | try: 71 | new_mol.UpdatePropertyCache(strict=False) 72 | Chem.SanitizeMol(new_mol, Chem.SanitizeFlags.SANITIZE_FINDRADICALS 73 | | Chem.SanitizeFlags.SANITIZE_KEKULIZE 74 | | Chem.SanitizeFlags.SANITIZE_SETAROMATICITY 75 | | Chem.SanitizeFlags.SANITIZE_SETCONJUGATION 76 | | Chem.SanitizeFlags.SANITIZE_SETHYBRIDIZATION 77 | | Chem.SanitizeFlags.SANITIZE_SYMMRINGS, 78 | catchErrors=True) 79 | except rdkit.Chem.rdchem.AtomSanitizeException: 80 | continue 81 | if any([a.GetExplicitValence() > max(atomic_valences[a.GetSymbol()]) + a.GetFormalCharge() 82 | for a in new_mol.GetAtoms()]): 83 | continue 84 | 85 | # Correct radical centers 86 | valid_comb = False 87 | for a in new_mol.GetAtoms(): 88 | for val in sorted(atomic_valences[a.GetSymbol()]): 89 | if a.GetExplicitValence() > val + a.GetFormalCharge(): 90 | valid_comb = False 91 | continue 92 | else: 93 | real_val = val 94 | valid_comb = True 95 | break 96 | if not valid_comb: 97 | break 98 | n_rad_elecs = real_val - a.GetExplicitValence() - a.GetFormalCharge() 99 | n_rad_elecs = max(n_rad_elecs, 0) 100 | a.SetNumRadicalElectrons(n_rad_elecs) 101 | if not valid_comb: 102 | continue 103 | if not any([Chem.MolToSmiles(new_mol) == Chem.MolToSmiles(vmol) 104 | for vmol in valid_mols]): 105 | valid_mols.append(new_mol) 106 | return valid_mols 107 | 108 | 109 | def gen_reso_structs(smi: str, min_rads=True) -> list: # C(=C\\1/[C]C1)\\[CH2] 110 | """Generate all possible resonant structures of a given molecule. 111 | 112 | @param smi: SMILES code of the given molecule 113 | @param min_rads: Whether to minimize the number of radical electrons or not. 114 | """ 115 | mol = Chem.MolFromSmiles(smi, sanitize=False) 116 | mol.UpdatePropertyCache(strict=False) 117 | Chem.SanitizeMol(mol, Chem.SanitizeFlags.SANITIZE_FINDRADICALS 118 | | Chem.SanitizeFlags.SANITIZE_KEKULIZE 119 | | Chem.SanitizeFlags.SANITIZE_SETAROMATICITY 120 | | Chem.SanitizeFlags.SANITIZE_SETCONJUGATION 121 | | Chem.SanitizeFlags.SANITIZE_SETHYBRIDIZATION 122 | | Chem.SanitizeFlags.SANITIZE_SYMMRINGS, 123 | catchErrors=True) 124 | if not mol: 125 | raise RuntimeError(f'Unable to make rdkit mol structure for {smi}.') 126 | mol = Chem.AddHs(mol) 127 | hvy_bond_ids = [b.GetIdx() for b in mol.GetBonds() 128 | if b.GetBeginAtom().GetSymbol() != 'H' 129 | and b.GetEndAtom().GetSymbol() != 'H'] 130 | num_bonds = int(sum([mol.GetBondWithIdx(bid).GetBondTypeAsDouble() for bid in hvy_bond_ids])) 131 | num_conns = len(hvy_bond_ids) 132 | radic_elecs = num_rad_elecs(mol) 133 | max_bonds = num_bonds + radic_elecs // 2 134 | valid_structs = [] 135 | for new_bonds in range(num_bonds, max_bonds + 1): 136 | combs = gen_bond_combs(new_bonds, num_conns) 137 | valid_structs += filter_valid_structs(mol, combs, hvy_bond_ids) 138 | 139 | if not valid_structs: 140 | raise RuntimeError 141 | 142 | # Remove Structures with larger number of radical electrons 143 | if min_rads: 144 | min_num_rads = min([num_rad_elecs(struct) for struct in valid_structs]) 145 | valid_structs = [struct for struct in valid_structs 146 | if num_rad_elecs(struct) == min_num_rads] 147 | 148 | # Remove explicit Hydrogen atoms 149 | for s, struct in enumerate(valid_structs): 150 | valid_structs[s] = Chem.RemoveHs(struct, sanitize=False) 151 | 152 | return valid_structs 153 | 154 | 155 | if __name__ == '__main__': 156 | smiles = sys.argv[1] 157 | valid_strs = gen_reso_structs(smiles) 158 | for j, m in enumerate(valid_strs): 159 | Draw.MolToFile(m, f'aaa{j}.png') 160 | -------------------------------------------------------------------------------- /pesviewer/pesviewer.py: -------------------------------------------------------------------------------- 1 | """This code reads in an input files containing the wells, bimolecular products, 2 | transition states andbarrierless reactions and creates a PES plot 3 | """ 4 | import os 5 | import sys 6 | import math 7 | 8 | import matplotlib 9 | matplotlib.use('TkAgg') 10 | from matplotlib import pylab as plt # translate into pyplot. 11 | import matplotlib.image as mpimg 12 | import numpy as np 13 | import numpy.linalg as la 14 | from rdkit import Chem 15 | from rdkit.Chem import Draw, AllChem 16 | from rdkit.Chem.Draw.cairoCanvas import Canvas 17 | from openbabel import pybel 18 | from PIL import Image 19 | import networkx as nx 20 | from pyvis import network as net 21 | 22 | from pesviewer.gen_resonant_structs import gen_reso_structs 23 | 24 | pybel.ob.obErrorLog.SetOutputLevel(0) 25 | # contains all the options for this PES 26 | options = {} 27 | 28 | # global parameters for the plot 29 | xlow = 0.0 # lowest x value on x axis 30 | xhigh = 0.0 # highest x value on x axis 31 | xmargin = 0.0 # margin on x axis 32 | ylow = 0.0 # lowest y value on x axis 33 | yhigh = 0.0 # highest y value on x axis 34 | ymargin = 0.0 # margin on y axis 35 | xlen = 1.0 # length of horizontal lines per st pt 36 | 37 | wells = [] # list of wells 38 | bimolecs = [] # list of bimolecular products 39 | tss = [] # list of transition states 40 | barrierlesss = [] # list of barrierless reactions 41 | 42 | # text dictionary: key is the chemical structure, value is the text 43 | textd = {} 44 | # lines dictionary: key is the chemical structure, 45 | # value is a list of lines for that structure 46 | linesd = {} 47 | # figures dictionary: key is the chemical structure, 48 | # value is the figure of that structure 49 | imgsd = {} 50 | # extents of the images 51 | extsd = {} 52 | 53 | 54 | class dragimage(object): 55 | """ 56 | Class to drag an image 57 | """ 58 | def __init__(self, figure=None): 59 | if figure is None: 60 | figure = plt.gcf() 61 | # simple attibute to store the dragged text object 62 | self.struct = None 63 | self.img = None 64 | # Connect events and callbacks 65 | figure.canvas.mpl_connect("button_press_event", self.pick_image) 66 | figure.canvas.mpl_connect("button_release_event", self.release_image) 67 | figure.canvas.mpl_connect("motion_notify_event", self.move_image) 68 | # end def 69 | 70 | def pick_image(self, event): 71 | for key in imgsd.keys(): 72 | self.struct = None 73 | self.img = None 74 | if (imgsd[key].get_extent()[0] < event.xdata and 75 | imgsd[key].get_extent()[1] > event.xdata and 76 | imgsd[key].get_extent()[2] < event.ydata and 77 | imgsd[key].get_extent()[3] > event.ydata): 78 | self.struct = key 79 | self.img = imgsd[key] 80 | self.current_pos = (event.xdata, event.ydata) 81 | break 82 | # end if 83 | # end for 84 | # end def 85 | 86 | def move_image(self, event): 87 | if self.img is not None: 88 | old_extent = self.img.get_extent() 89 | xchange = event.xdata-self.current_pos[0] 90 | ychange = event.ydata-self.current_pos[1] 91 | extent_change = (xchange, xchange, ychange, ychange) 92 | extent = [old_extent[i] + extent_change[i] for i in range(0, 4)] 93 | self.img.set_extent(extent=extent) 94 | self.current_pos = (event.xdata, event.ydata) 95 | plt.draw() 96 | # end def 97 | 98 | def release_image(self, event): 99 | if self.img is not None: 100 | self.struct = None 101 | self.img = None 102 | save_im_extent() 103 | # end if 104 | # end if 105 | # end def 106 | # end class 107 | 108 | 109 | class selecthandler(object): 110 | """ 111 | Class to select and move stationary points, which highlight its reactions 112 | and in the future (TODO) renders the 3D images 113 | """ 114 | def __init__(self, figure=None): 115 | if figure is None: 116 | figure = plt.gcf() 117 | self.struct = None # stationary point that is selected 118 | figure.canvas.mpl_connect("button_press_event", self.on_pick_event) 119 | figure.canvas.mpl_connect("button_release_event", 120 | self.on_release_event) 121 | figure.canvas.mpl_connect("motion_notify_event", 122 | self.motion_notify_event) 123 | # end def 124 | 125 | def on_pick_event(self, event): 126 | self.struct = None 127 | # TODO: find more efficient way to iterate 128 | # all stationary points in one loop? 129 | # create a new list? 130 | for w in wells: 131 | if self.is_close(w, event): 132 | self.struct = w 133 | highlight_structure(self.struct) 134 | # end if 135 | # end for 136 | for b in bimolecs: 137 | if self.is_close(b, event): 138 | self.struct = b 139 | highlight_structure(self.struct) 140 | # end if 141 | # end for 142 | for t in tss: 143 | if self.is_close(t, event): 144 | self.struct = t 145 | highlight_structure(self.struct) 146 | # end if 147 | # end for 148 | if self.struct is None: 149 | highlight_structure() 150 | # end if 151 | # end def 152 | 153 | def motion_notify_event(self, event): 154 | if self.struct is not None: 155 | # a stationary point got selected 156 | # current position of the stationary point 157 | old_pos = (self.struct.x, self.struct.y) 158 | self.struct.x = event.xdata # set the new position 159 | # move all the elements(image, text and lines) 160 | updateplot(self.struct, (event.xdata-old_pos[0])) 161 | self.current_pos = old_pos 162 | # end def 163 | 164 | def on_release_event(self, event): 165 | if self.struct is not None: 166 | self.struct = None 167 | # save the x-values of the startionary points to a file 168 | save_x_values() 169 | # save the image extents (x and y coordinates) to a file 170 | save_im_extent() 171 | # end if 172 | return True 173 | # end def 174 | 175 | def is_close(self, struct, event): 176 | """ 177 | An event is close if it comes within 2% of the stationary point 178 | both in the x and in the y direction 179 | """ 180 | xc = math.fabs(event.xdata - struct.x) < (xhigh-xlow)*0.02 181 | yc = math.fabs(event.ydata - struct.y) < (yhigh-ylow)*0.02 182 | return xc and yc 183 | # end class 184 | 185 | 186 | class line: 187 | """ 188 | A line contains information about the line on the graph 189 | it is either a line between a reactant and ts, between a ts and product 190 | or between a reactant and product (for barrierless reactions) 191 | the chemstructs are the reactant and ts (f orward), 192 | ts and product (reverse), or reactant and product (barrierless) 193 | """ 194 | def __init__(self, x1, y1, x2, y2, chemstruct=None, col='black'): 195 | if x1 <= x2: 196 | self.xmin = x1 197 | self.y1 = y1 # y1 corresponds to xmin 198 | self.xmax = x2 199 | self.y2 = y2 # y2 corresponds to xmax 200 | else: 201 | self.xmin = x2 202 | self.y1 = y2 # y1 corresponds to xmin 203 | self.xmax = x1 204 | self.y2 = y1 # y2 corresponds to xmax 205 | # end if 206 | 207 | 208 | if x1 == x2 or y1 == y2: 209 | self.straight_line = True 210 | self.coeffs = [] 211 | else: 212 | self.straight_line = False 213 | self.coeffs = get_polynomial(self.xmin, self.y1, 214 | self.xmax, self.y2) 215 | # end if 216 | if chemstruct is None: 217 | self.chemstruct = [] 218 | else: 219 | self.chemstruct = chemstruct 220 | self.color = col 221 | # end def 222 | # end class 223 | 224 | 225 | class well: 226 | # Well class, contains the name, smiles and energy of a well 227 | def __init__(self, name, energy, smi=None, energy2=None): 228 | self.name = name 229 | self.energy = convert_units(energy) 230 | self.smi = smi 231 | self.x = 0. 232 | self.y = 0. 233 | self.xyz_files = [] 234 | self.energy2 = energy2 235 | fn = 'xyz/{name}.xyz'.format(name=name) 236 | if os.path.exists(fn): 237 | self.xyz_files.append(fn) 238 | # end def 239 | # end class 240 | 241 | 242 | class bimolec: 243 | # Bimolec class, contains the name, 244 | # both smiles and energy of a bimolecular product 245 | def __init__(self, name, energy, smi=None, energy2=None): 246 | self.name = name 247 | self.energy = convert_units(energy) 248 | if smi is None: 249 | self.smi = [] 250 | else: 251 | self.smi = smi 252 | self.x = 0. 253 | self.y = 0. 254 | self.xyz_files = [] 255 | # this bimolecular product is placed on the right side of the graph 256 | self.right = False 257 | self.energy2 = energy2 258 | i = 1 259 | fn = 'xyz/{name}{i}.xyz'.format(name=name, i=i) 260 | while os.path.exists(fn): 261 | self.xyz_files.append(fn) 262 | i += 1 263 | fn = 'xyz/{name}{i}.xyz'.format(name=name, i=i) 264 | # end for 265 | # end def 266 | # end class 267 | 268 | 269 | class ts: 270 | """ 271 | TS class, contains the name, the names of the 272 | reactant and product and the energy of the ts 273 | """ 274 | def __init__(self, name, names, energy, col='black'): 275 | self.name = name 276 | self.energy = convert_units(energy) 277 | self.color = col 278 | self.xyz_files = [] 279 | fn = 'xyz/{name}.xyz'.format(name=name) 280 | if os.path.exists(fn): 281 | self.xyz_files.append(fn) 282 | self.reactant = next((w for w in wells if w.name == names[0]), None) 283 | if self.reactant is None: 284 | list = (b for b in bimolecs if b.name == names[0]) 285 | self.reactant = next(list, None) 286 | if self.reactant is None: 287 | e = exceptions.not_recognized('reactant', names[0], name) 288 | raise Exception(e) 289 | self.product = next((w for w in wells if w.name == names[1]), None) 290 | if self.product is None: 291 | list = (b for b in bimolecs if b.name == names[1]) 292 | self.product = next(list, None) 293 | if self.product is None: 294 | e = exceptions.not_recognized('product', names[1], name) 295 | raise Exception(e) 296 | self.lines = [] 297 | self.x = 0. 298 | self.y = 0. 299 | # end def 300 | # end class 301 | 302 | 303 | class barrierless: 304 | """ 305 | Barrierless class, contains the name and the 306 | names of the reactant and product 307 | """ 308 | def __init__(self, name, names, col='black'): 309 | self.name = name 310 | self.xyz_files = [] 311 | self.color = col 312 | fn = f'xyz/{name}.xyz' 313 | if os.path.exists(fn): 314 | self.xyz_files.append(fn) 315 | self.reactant = next((w for w in wells if w.name == names[0]), None) 316 | if self.reactant is None: 317 | list = (b for b in bimolecs if b.name == names[0]) 318 | self.reactant = next(list, None) 319 | if self.reactant is None: 320 | e = exceptions.not_recognized('reactant', names[0], name) 321 | raise Exception(e) 322 | self.product = next((w for w in wells if w.name == names[1]), None) 323 | if self.product is None: 324 | list = (b for b in bimolecs if b.name == names[1]) 325 | self.product = next(list, None) 326 | if self.product is None: 327 | e = exceptions.not_recognized('product', names[1], name) 328 | raise Exception(e) 329 | self.energy = convert_units(self.product.energy) 330 | self.line = None 331 | # end def 332 | # end class 333 | 334 | 335 | class exceptions(object): 336 | """ 337 | Class that stores the exception messages 338 | """ 339 | def not_recognized(role, ts_name, species_name): 340 | s = 'Did not recognize {role}'.format(role=role) 341 | s += ' {prod} '.format(prod=species_name) 342 | s += 'for the transition state {ts}'.format(ts=ts_name) 343 | return s 344 | 345 | 346 | def get_polynomial(x1, y1, x2, y2): 347 | """ 348 | Method fits a third order polynomial through two points as such 349 | that the derivative in both points is zero 350 | This method should only be used if x1 is not equal to x2 351 | """ 352 | if x1 == x2: 353 | print('Error, cannot fit a polynomial if x1 equals x2') 354 | sys.exit() 355 | else: 356 | y = np.matrix([[y1], [y2], [0], [0]]) 357 | x = np.matrix([[x1**3, x1**2, x1, 1], 358 | [x2**3, x2**2, x2, 1], 359 | [3*x1**2, 2*x1, 1, 0], 360 | [3*x2**2, 2*x2, 1, 0]]) 361 | xinv = la.inv(x) 362 | a = np.dot(xinv, y) 363 | return np.transpose(a).tolist()[0] 364 | # end def 365 | 366 | 367 | def read_input(fname): 368 | """ 369 | Method to read the input file 370 | """ 371 | if not os.path.exists(fname): # check if file exists 372 | raise FileNotFoundError(fname + ' does not exist') 373 | # end if 374 | with open(fname, 'r') as f: 375 | input_str = f.read() 376 | 377 | input_str = input_str.replace('\r\n', '\n').replace('\r', '\n').replace('\n\n', '\n') 378 | input_dict = get_sd_prop(input_str) 379 | options['id'] = input_dict['id'][0] 380 | # by default, print the graph title 381 | options['title'] = 1 382 | # default units 383 | options['units'] = 'kJ/mol' 384 | # units to display the results, defaults to whatever is set in units 385 | options['display_units'] = None 386 | # rounding of values on plot 387 | options['rounding'] = 1 388 | # shifting energies (in original units) 389 | options['energy_shift'] = 0. 390 | # use xyz by default, put 0 to switch off 391 | options['use_xyz'] = 1 392 | # no rescale as default, put the well or 393 | # bimolecular name here to rescale to that value 394 | options['rescale'] = 0 395 | # default figure height 396 | options['fh'] = 9. 397 | # default figure width 398 | options['fw'] = 18. 399 | # scale factor for figures 400 | options['fs'] = 1. 401 | # change the linewidth of the traditional Pot. vs Reac. Coord. plot. 402 | options['lw'] = 1.5 403 | # default margin on the x and y axis 404 | options['margin'] = 0.2 405 | # default dpi of the molecule figures 406 | options['dpi'] = 120 407 | # does the plot need to be saved (1) or displayed (0) 408 | options['save'] = 0 409 | # Whether to plot the V vs RC plot. 410 | options['plot'] = 1 411 | # Whether to plot the PES graph 412 | options['graph_plot'] = 1 413 | # booleans tell if the ts energy values should be written 414 | options['write_ts_values'] = 1 415 | # booleans tell if the well and bimolecular energy values should be written 416 | options['write_well_values'] = 1 417 | # color of the energy values for the bimolecular products 418 | options['bimol_color'] = 'red' 419 | # color of the energy values of the wells 420 | options['well_color'] = 'blue' 421 | # color or the energy of the ts, put to 'none' to use same color as line 422 | options['ts_color'] = 'none' 423 | # boolean tells whether the molecule images should be shown on the graph 424 | options['show_images'] = 1 425 | # boolean that specifies which code was used for the 2D depiction 426 | options['rdkit4depict'] = 1 427 | # font size of the axes 428 | options['axes_size'] = 10 429 | # font size of the energy values 430 | options['text_size'] = 10 431 | # use linear lines instead of a polynomial 432 | options['linear_lines'] = 0 433 | # image interpolation 434 | options['interpolation'] = 'hanning' 435 | # graphs edge color, if set to 'energy', will be colored by that 436 | options['graph_edge_color'] = 'black' 437 | # graphs edge color, if set to 'energy', will be colored by that 438 | options['graph_bimolec_color'] = 'blue' 439 | # enable/disable generation of 2D depictions for resonant structures. 440 | options['reso_2d'] = 0 441 | # print report on paths connecting two species. Replace 0 with the two species names if to be activated. 442 | options['path_report'] = [] 443 | # depth of search 444 | options['search_cutoff'] = 10 445 | # Scale graph nodes according to their stability. 446 | options['node_size_diff'] = 0 447 | 448 | if 'options' in input_dict: 449 | for line in input_dict['options']: 450 | if line.startswith('title'): 451 | options['title'] = int(line.split()[1]) 452 | elif line.startswith('units'): 453 | options['units'] = line.split()[1] 454 | elif line.startswith('display_units'): 455 | options['display_units'] = line.split()[1] 456 | elif line.startswith('rounding'): 457 | options['rounding'] = int(line.split()[1]) 458 | elif line.startswith('energy_shift'): 459 | options['energy_shift'] = float(line.split()[1]) 460 | elif line.startswith('use_xyz'): 461 | options['use_xyz'] = int(line.split()[1]) 462 | elif line.startswith('rescale'): 463 | options['rescale'] = line.split()[1] 464 | elif line.startswith('fh'): 465 | options['fh'] = float(line.split()[1]) 466 | elif line.startswith('fw'): 467 | options['fw'] = float(line.split()[1]) 468 | elif line.startswith('fs'): 469 | options['fs'] = float(line.split()[1]) 470 | elif line.startswith('lw'): 471 | options['lw'] = float(line.split()[1]) 472 | elif line.startswith('margin'): 473 | options['margin'] = float(line.split()[1]) 474 | elif line.startswith('dpi'): 475 | options['dpi'] = int(line.split()[1]) 476 | elif line.startswith('save'): 477 | if not options['save_from_command_line']: 478 | options['save'] = int(line.split()[1]) 479 | elif line.startswith('plot'): 480 | options['plot'] = int(line.split()[1]) 481 | elif line.startswith('graph_plot'): 482 | options['graph_plot'] = bool(int(line.split()[1])) 483 | elif line.startswith('write_ts_values'): 484 | options['write_ts_values'] = int(line.split()[1]) 485 | elif line.startswith('write_well_values'): 486 | options['write_well_values'] = int(line.split()[1]) 487 | elif line.startswith('bimol_color'): 488 | options['bimol_color'] = line.split()[1] 489 | elif line.startswith('well_color'): 490 | options['well_color'] = line.split()[1] 491 | elif line.startswith('ts_color'): 492 | options['ts_color'] = line.split()[1] 493 | elif line.startswith('show_images'): 494 | options['show_images'] = int(line.split()[1]) 495 | elif line.startswith('rdkit4depict'): 496 | options['rdkit4depict'] = int(line.split()[1]) 497 | elif line.startswith('axes_size'): 498 | options['axes_size'] = float(line.split()[1]) 499 | elif line.startswith('text_size'): 500 | options['text_size'] = float(line.split()[1]) 501 | elif line.startswith('linear_lines'): 502 | options['linear_lines'] = int(line.split()[1]) 503 | elif line.startswith('graph_edge_color'): 504 | options['graph_edge_color'] = str(line.split()[1]) 505 | elif line.startswith('graph_bimolec_color'): 506 | options['graph_bimolec_color'] = line.split()[1] 507 | elif line.startswith('reso_2d'): 508 | options['reso_2d'] = int(line.split()[1]) 509 | elif line.startswith('path_report'): 510 | options['path_report'].append(tuple(str(i) 511 | for i in line.split()[1:])) 512 | elif line.startswith('search_cutoff'): 513 | options['search_cutoff'] = int(line.split()[1]) 514 | elif line.startswith('node_size_diff'): 515 | options['node_size_diff'] = float(line.split()[1]) 516 | elif line.startswith('#'): 517 | # comment line, don't do anything 518 | continue 519 | else: 520 | if len(line) > 0: 521 | print('Cannot recognize input line:') 522 | print(line) 523 | # end if 524 | # end if 525 | # end for 526 | else: 527 | print('Warning, the input file arcitecture has changed,' + 528 | 'use an "options" input tag to put all the options') 529 | # end if 530 | 531 | if options['display_units'] is None: 532 | options['display_units'] = options['units'] 533 | 534 | for w in input_dict['wells']: 535 | w = w.split() 536 | name = w[0] 537 | energy = float(w[1]) 538 | energy2 = None 539 | smi = None 540 | if len(w) > 2 and w[2] != '#': 541 | try: 542 | energy2 = float(w[2]) 543 | except ValueError: 544 | smi = w[2] 545 | # end if 546 | w = well(name, energy, smi, energy2) 547 | wells.append(w) 548 | # end for 549 | for b in input_dict['bimolec']: 550 | b = b.split() 551 | name = b[0] 552 | energy = float(b[1]) 553 | energy2 = None 554 | smi = [] 555 | if len(b) > 2 and b[2] != '#': 556 | try: 557 | energy2 = float(b[2]) 558 | except ValueError: 559 | smi = b[2:] 560 | b = bimolec(name, energy, smi, energy2) 561 | bimolecs.append(b) 562 | # end for 563 | 564 | # it is important that the wells and bimolecular products 565 | # are read prior to the transition states and barrierless 566 | # reactions because they need to be added as reactants 567 | # and products 568 | for t in input_dict['ts']: 569 | t = t.split() 570 | name = t[0] 571 | energy = eval(t[1]) 572 | names = [t[2], t[3]] 573 | col = 'black' 574 | if options['graph_edge_color'] != 'energy': 575 | col = options['graph_edge_color'] 576 | if len(t) > 4: 577 | col = t[4] 578 | t = ts(name, names, energy, col=col) 579 | tss.append(t) 580 | # end for 581 | for b in input_dict['barrierless']: 582 | b = b.split() 583 | name = b[0] 584 | names = [b[1], b[2]] 585 | col = 'gray' 586 | if len(b) > 3: 587 | col = b[3] 588 | b = barrierless(name, names, col=col) 589 | barrierlesss.append(b) 590 | # end for 591 | return input_str 592 | # end def 593 | 594 | 595 | def get_sd_prop(all_lines): 596 | """ 597 | The get_sd_prop method interprets the input file 598 | which has a structure comparable to sdf molecular files 599 | """ 600 | # split all the lines according to the keywords 601 | inputs = all_lines.split('> <') 602 | # do not consider the first keyword, this contains the comments 603 | inputs = inputs[1:] 604 | ret = {} 605 | for inp in inputs: 606 | inp = inp .split('>', 1) 607 | kw = inp[0].lower() # keyword in lower case 608 | val = inp[1].strip().split('\n') # values of the keywords 609 | val = [vi.strip() for vi in val if not vi.startswith('#')] 610 | val = [vi for vi in val if vi] 611 | ret[kw] = val 612 | # end for 613 | return ret 614 | # end def 615 | 616 | 617 | def position(): 618 | """ 619 | This method find initial position for all the wells, products and 620 | transition states. Initially, the wells are put in the middle and 621 | the products are divided on both sides. The transition states are 622 | positioned inbetween the reactants and products of the reaction 623 | """ 624 | # do the rescaling, i.e. find the y values 625 | # y0 is the energy of the species to which rescaling is done 626 | y0 = next((w.energy for w in wells if w.name == options['rescale']), 0.) 627 | if y0 == 0.: 628 | list = (b.energy for b in bimolecs if b.name == options['rescale']) 629 | y0 = next(list, 0.) 630 | for w in wells: 631 | if np.isnan(w.energy): 632 | w.y = w.energy2 - y0 633 | else: 634 | w.y = w.energy - y0 635 | # end for 636 | for b in bimolecs: 637 | if np.isnan(b.energy): 638 | b.y = b.energy2 - y0 639 | else: 640 | b.y = b.energy - y0 641 | # end for 642 | for t in tss: 643 | t.y = t.energy - y0 644 | # end for 645 | 646 | # find the x values 647 | file_name = '{id}_xval.txt'.format(id=options['id']) 648 | if os.path.exists(file_name): 649 | # if file exists, read the x values 650 | fi = open(file_name, 'r') 651 | a = fi.read() 652 | fi.close() 653 | a = a.split('\n') 654 | for entry in a: 655 | if len(entry) > 0: 656 | name = entry.split(' ')[0] 657 | xval = eval(entry.split(' ')[1]) 658 | for w in wells: 659 | if w.name == name: 660 | w.x = xval 661 | # end for 662 | for b in bimolecs: 663 | if b.name == name: 664 | b.x = xval 665 | # end for 666 | for t in tss: 667 | if t.name == name: 668 | t.x = xval 669 | # end for 670 | # end if 671 | # end for 672 | else: 673 | n = len(bimolecs) # n is the number of bimolecular products 674 | n2 = n // 2 675 | 676 | for i, w in enumerate(wells): 677 | # wells are put in the middle 678 | w.x = n2 + 1 + i + 0. 679 | 680 | # end for 681 | for i, b in enumerate(bimolecs): 682 | # bimolecular products are put on both sides 683 | if i < n2: 684 | b.x = 1 + i + 0. 685 | else: 686 | b.x = len(wells) + 1 + i + 0. 687 | b.right = True 688 | # end if 689 | 690 | # end for 691 | for i, t in enumerate(tss): 692 | # transition states are put inbetween the reactant and proudct 693 | x1 = t.reactant.x 694 | x2 = t.product.x 695 | x3 = (x1+x2)/2. 696 | t.x = x3 697 | # end for 698 | save_x_values() # write the x values to a file 699 | # end if 700 | # end def 701 | 702 | 703 | def generate_lines(): 704 | """ 705 | The method loops over the transition states and barrierless reactions 706 | and creates lines accordingly depending on the x and y coordinates 707 | """ 708 | for t in tss: 709 | line1 = line(t.x, 710 | t.y, 711 | t.reactant.x, 712 | t.reactant.y, 713 | [t, t.reactant], 714 | col=t.color) 715 | line2 = line(t.x, 716 | t.y, 717 | t.product.x, 718 | t.product.y, 719 | [t, t.product], 720 | col=t.color) 721 | t.lines.append(line1) 722 | t.lines.append(line2) 723 | # end for 724 | for b in barrierlesss: 725 | b.line = line(b.reactant.x, 726 | b.reactant.y, 727 | b.product.x, 728 | b.product.y, 729 | [b, b.reactant, b.product], 730 | col=b.color) 731 | # end for 732 | # end def 733 | 734 | 735 | def get_sizes(): 736 | """ 737 | Get the axis lengths and the sizes of the images 738 | """ 739 | global xlow, xhigh, xmargin, ylow, yhigh, ymargin 740 | # TODO: what if wells or bimoleculs is empty, 741 | # min and max functions will give error 742 | if len(bimolecs) > 0: 743 | # x coords of tss are always inbetween stable species 744 | xlow = min(min([w.x for w in wells]), min([b.x for b in bimolecs])) 745 | xhigh = max(max([w.x for w in wells]), max([b.x for b in bimolecs])) 746 | # all tss lie above the lowest well 747 | ylow = min(min([w.y for w in wells]), min([b.y for b in bimolecs])) 748 | else: 749 | # x coords of tss are always inbetween stable species 750 | xlow = min([w.x for w in wells]) 751 | xhigh = max([w.x for w in wells]) 752 | # all tss lie above the lowest well 753 | ylow = min([w.y for w in wells]) 754 | xmargin = options['margin']*(xhigh-xlow) 755 | try: 756 | yhigh = max([t.y for t in tss]) 757 | except ValueError: 758 | yhigh = max([b.y for b in bimolecs]) 759 | yhigh = max(yhigh, max([w.y for w in wells])) 760 | if len(bimolecs) > 0: 761 | yhigh = max(yhigh, max([b.y for b in bimolecs])) 762 | ymargin = options['margin']*(yhigh-ylow) 763 | # end def 764 | 765 | 766 | def plot(): 767 | """Plotter method takes all the lines and plots them in one graph""" 768 | global xlow, xhigh, xmargin, ylow, yhigh, ymargin, xlen 769 | 770 | def showimage(s): 771 | """ 772 | Get the extent and show the image on the plot 773 | s is the structure of which the image is to be 774 | plotted 775 | """ 776 | global ymargin 777 | fn = '{id}_2d/{name}_2d.png'.format(id=options['id'], name=s.name) 778 | if os.path.exists(fn): 779 | img = mpimg.imread(fn) 780 | extent = None 781 | if s.name in extsd: 782 | extent = extsd[s.name] 783 | else: 784 | if not options['rdkit4depict']: 785 | options['dpi'] = 120 786 | # end if 787 | 788 | imy = len(img) * options['fs'] 789 | imx = len(img[0]) * options['fs'] 790 | imw = (xhigh-xlow+0.)/(options['fw']+0.)*imx/options['dpi'] 791 | imh = (yhigh-ylow+0.)/(options['fh']+0.)*imy/options['dpi'] 792 | 793 | if isinstance(s, bimolec): 794 | if s.x > (xhigh-xlow)/2.: 795 | extent = (s.x + xmargin/5., 796 | s.x + xmargin/5. + imw, 797 | s.y-imh/2., 798 | s.y+imh/2.) 799 | else: 800 | extent = (s.x - xmargin/5. - imw, 801 | s.x - xmargin/5., 802 | s.y-imh/2., 803 | s.y+imh/2.) 804 | # end if 805 | else: 806 | extent = (s.x - imw/2., 807 | s.x + imw/2., 808 | s.y-ymargin/5. - imh, 809 | s.y-ymargin/5.) 810 | # end if 811 | # end if 812 | im = ax.imshow(img, aspect='auto', extent=extent, zorder=-1, interpolation=options['interpolation']) 813 | # add to dictionary with the well it belongs to as key 814 | imgsd[s] = im 815 | # end if 816 | 817 | lines = [] 818 | for t in tss: 819 | lines.append(t.lines[0]) 820 | lines.append(t.lines[1]) 821 | # end for 822 | for b in barrierlesss: 823 | lines.append(b.line) 824 | # end for 825 | get_sizes() 826 | plt.rcParams["figure.figsize"] = [options['fw'], options['fh']] 827 | plt.rcParams["font.size"] = options['axes_size'] 828 | plt.rcParams['figure.dpi'] = options['dpi'] 829 | 830 | matplotlib.rc("figure", facecolor="white") 831 | fig, ax = plt.subplots() 832 | ax.spines['top'].set_visible(False) 833 | ax.spines['bottom'].set_visible(False) 834 | ax.spines['right'].set_visible(False) 835 | 836 | ax.xaxis.set_ticks_position('none') 837 | ax.yaxis.set_ticks_position('left') 838 | ax.set_xticklabels([]) 839 | 840 | if options['show_images']: 841 | for w in wells: 842 | showimage(w) 843 | # end for 844 | for b in bimolecs: 845 | showimage(b) 846 | # end for 847 | save_im_extent() # save the positions of the images to a file 848 | 849 | # draw the lines 850 | # in case of linear lines, calculate the distance of the horizontal pieces 851 | if options['linear_lines']: 852 | xlen = (len(wells) + len(bimolecs)) / (4 * (xhigh - xlow)) 853 | 854 | for line in lines: 855 | lw = options['lw'] 856 | alpha = 1.0 857 | ls = 'solid' 858 | if line.color == 'dotted': 859 | ls = 'dotted' 860 | line.color = 'gray' 861 | # elif line.color == 'blue' or line.color == 'b': 862 | # ls = 'dashed' 863 | if line.straight_line: 864 | if line.xmin == line.xmax: # plot a vertical line 865 | ymin = min(line.y1, line.y2) 866 | ymax = max(line.y1, line.y2) 867 | a = ax.vlines(x=line.xmin, 868 | ymin=ymin, 869 | ymax=ymax, 870 | color=line.color, 871 | ls=ls, 872 | linewidth=lw, 873 | alpha=alpha) 874 | for struct in line.chemstruct: 875 | # add to the lines dictionary 876 | linesd[struct].append(a) 877 | # end for 878 | else: # plot a horizontal line 879 | a = ax.hlines(y=line.y1, 880 | xmin=line.xmin, 881 | xmax=line.xmax, 882 | color=line.color, 883 | linewidth=lw, 884 | alpha=alpha) 885 | for struct in line.chemstruct: 886 | # add to the lines dictionary 887 | linesd[struct].append(a) 888 | # end for 889 | # end if 890 | else: 891 | if options['linear_lines']: 892 | xlist = [line.xmin, line.xmin + xlen / 2, 893 | line.xmax - xlen / 2, line.xmax] 894 | y = [line.y1, line.y1, line.y2, line.y2] 895 | else: 896 | xlist = np.arange(line.xmin, 897 | line.xmax, 898 | (line.xmax-line.xmin) / 1000) 899 | a = line.coeffs 900 | y = a[0]*xlist**3 + a[1]*xlist**2 + a[2]*xlist + a[3] 901 | pl = ax.plot(xlist, 902 | y, 903 | color=line.color, 904 | ls=ls, 905 | linewidth=lw, 906 | alpha=alpha) 907 | for struct in line.chemstruct: 908 | # add to the lines dictionary 909 | linesd[struct].append(pl) 910 | # end for 911 | # end if 912 | # end for 913 | ax.set_xlim([xlow-xmargin, xhigh+xmargin]) 914 | ax.set_ylim([ylow-ymargin, yhigh+ymargin]) 915 | 916 | # write the name and energies to the plot 917 | for w in wells: 918 | if options['write_well_values']: 919 | t = ax.text(w.x, 920 | w.y-ymargin/10, 921 | '{:.{}f}'.format(w.y, options['rounding']), 922 | fontdict={'size': options['text_size']}, 923 | ha='center', va='top', 924 | color=options['well_color'], 925 | picker=True) 926 | # add to dictionary with the well it belongs to as key 927 | textd[w] = t 928 | # end if 929 | # end for 930 | for b in bimolecs: 931 | if options['write_well_values']: 932 | # write the text values below the line: 933 | t = ax.text(b.x, 934 | b.y-ymargin/10, 935 | '{:.{}f}'.format(b.y, options['rounding']), 936 | fontdict={'size': options['text_size']}, 937 | ha='center', 938 | va='top', 939 | color=options['bimol_color'], 940 | picker=True) 941 | # add to dictionary with the well it belongs to as key 942 | textd[b] = t 943 | # end if 944 | # end for 945 | for t in tss: 946 | if options['write_ts_values']: 947 | color = t.color 948 | if options['ts_color'] != 'none': 949 | color = options['ts_color'] 950 | te = ax.text(t.x, 951 | t.y+ymargin/30, 952 | '{:.{}f}'.format(t.y, options['rounding']), 953 | fontdict={'size': options['text_size']}, 954 | ha='center', 955 | va='bottom', 956 | color=color, 957 | picker=True) 958 | # add to dictionary with the well it belongs to as key 959 | textd[t] = te 960 | # end if 961 | # end for 962 | 963 | sel = selecthandler() 964 | dr = dragimage() 965 | 966 | if options['title']: 967 | plt.title('Potential energy surface of {id}'.format(id=options['id'])) 968 | # defaults to units if not specified 969 | plt.ylabel('Energy ({display_units})'.format(display_units=options['display_units'])) 970 | if options['save']: 971 | plt.savefig(f'{options["id"]}_pes_plot.png', bbox_inches='tight') 972 | else: 973 | plt.show() 974 | # end def 975 | 976 | 977 | def generate_2d_depiction(): 978 | """ 979 | 2D depiction is generated (if not yet available) and 980 | stored in the directory join(input_id, '_2d') 981 | This is only done for the wells and bimolecular products, 982 | 2D of tss need to be supplied by the user 983 | """ 984 | def get_smis(m, smis, files): 985 | # name and path of png file 986 | if len(smis) > 0: 987 | return smis 988 | elif files and all([os.path.exists(f) for f in files]): 989 | smis = [] 990 | for f in files: 991 | try: 992 | obmol = next(pybel.readfile('xyz', f)) 993 | smi = obmol.write("smi").split()[0] 994 | smi = smi.replace('=[CH]=C', '=C[C]') 995 | smi = smi.replace('N(=O)=O', 'N(=O)[O]') 996 | smi = smi.replace('[NH]([CH2])(O)[O]', 997 | '[NH+]([CH2])(O)[O-]') 998 | smis.append(smi) 999 | except NameError: 1000 | print('Could not generate smiles for {n}'.format(n=m.name)) 1001 | # end try 1002 | # end for 1003 | else: 1004 | raise FileNotFoundError(f'No xyz file found for well {m.name} (' 1005 | f'{m.smi})') 1006 | return smis 1007 | # end def 1008 | 1009 | def reaction_smi(): 1010 | """Given a list of transition states and barrierless channels 1011 | write a reaction_smi.out file that contains the reactions as, e.g.: 1012 | OOC[CH2] = [OH] + O1CC1i 0.0 10.72 -16.11 1013 | The three numbers are the energy of the reactant, transition state and product. 1014 | The barrier height for barrierless reactions is given as max(Ereact,Eprod) 1015 | """ 1016 | 1017 | with open('reaction_smi.out', 'w') as f: 1018 | for t in tss: 1019 | if not t.reactant.smi: 1020 | t.reactant.smi = get_smis(t.reactant, [], t.reactant.xyz_files) 1021 | elif isinstance(t.reactant.smi, str): 1022 | t.reactant.smi = [t.reactant.smi] 1023 | f.write(t.reactant.smi[0]) 1024 | for r in t.reactant.smi[1:]: 1025 | f.write(' + ') 1026 | f.write(r) 1027 | f.write(' = ') 1028 | if not t.product.smi: 1029 | t.product.smi = get_smis(t.product, [], t.product.xyz_files) 1030 | elif isinstance(t.product.smi, str): 1031 | t.product.smi = [t.product.smi] 1032 | f.write(t.product.smi[0]) 1033 | for p in t.product.smi[1:]: 1034 | f.write(' + ') 1035 | f.write(p) 1036 | f.write(' {:.2f} {:.2f} {:.2f}'.format(t.reactant.energy, t.energy, t.product.energy)) 1037 | f.write('\n') 1038 | 1039 | for b in barrierlesss: 1040 | if not b.reactant.smi: 1041 | b.reactant.smi = get_smis(b.reactant, [], b.reactant.xyz_files) 1042 | elif isinstance(b.reactant.smi, str): 1043 | b.reactant.smi = [b.reactant.smi] 1044 | f.write(b.reactant.smi[0]) 1045 | for r in b.reactant.smi: 1046 | f.write(' + ') 1047 | f.write(r) 1048 | f.write(' = ') 1049 | if not b.product.smi: 1050 | b.product.smi = get_smis(b.product, [], b.product.xyz_files) 1051 | elif isinstance(b.product.smi, str): 1052 | b.product.smi = [b.product.smi] 1053 | f.write(b.product.smi[0]) 1054 | for p in b.product.smi[1:]: 1055 | f.write(' + ') 1056 | f.write(p) 1057 | f.write(' {:.2f} {:.2f} {:.2f}'.format(b.reactant.energy, max(b.reactant.energy, b.product.energy), b.product.energy)) 1058 | f.write('\n') 1059 | 1060 | def generate_2d(m, smis): 1061 | 1062 | png_filename = '{id}_2d/{name}_2d{confid}.png' 1063 | if len(smis) == 0: 1064 | print('Could not generate 2d for {name}'.format(name=m.name)) 1065 | return 1066 | smi = '.'.join(smis) 1067 | if os.path.isfile(png_filename.format(id=options['id'], name=m.name, 1068 | confid='')): 1069 | return 1070 | try: 1071 | if options['reso_2d']: 1072 | try: 1073 | reson_mols = gen_reso_structs(smi, min_rads=True) 1074 | except RuntimeError: 1075 | print(f'Warning: Unable to generate resonant structure for ' 1076 | f'{smi}.') 1077 | options['reso_2d'] = 0 1078 | else: 1079 | mol = Chem.MolFromSmiles(smi, sanitize=False) 1080 | mol.UpdatePropertyCache(strict=False) 1081 | Chem.SanitizeMol(mol, Chem.SanitizeFlags.SANITIZE_FINDRADICALS 1082 | | Chem.SanitizeFlags.SANITIZE_KEKULIZE 1083 | | Chem.SanitizeFlags.SANITIZE_SETAROMATICITY 1084 | | Chem.SanitizeFlags.SANITIZE_SETCONJUGATION 1085 | | Chem.SanitizeFlags.SANITIZE_SETHYBRIDIZATION 1086 | | Chem.SanitizeFlags.SANITIZE_SYMMRINGS, 1087 | catchErrors=True) 1088 | reson_mols = [mol] 1089 | 1090 | resol = 5 1091 | size_x = 100 * resol 1092 | 1093 | # NEW METHOD 1094 | # opts = Draw.rdMolDraw2D.MolDrawOptions() # New way 1095 | # opts.minFontSize = 30 * resol 1096 | # opts.maxFontSize = 9 * resol 1097 | # opts.bondLineWidth = 1 * resol 1098 | # opts.padding = 0.15 1099 | # opts.noAtomLabels = True 1100 | 1101 | # OLD METHOD (cannot use Draw.MolToImage or Draw.MolToFile): 1102 | opts = Draw.DrawingOptions() 1103 | opts.dotsPerAngstrom = 15 * resol 1104 | opts.atomLabelFontSize = 9 * resol 1105 | opts.bondLineWidth = 1 * resol 1106 | opts.radicalSymbol = '•' 1107 | for i, mol in enumerate(reson_mols): 1108 | AllChem.Compute2DCoords(mol) 1109 | cc = mol.GetConformer() 1110 | xx = [] 1111 | yy = [] 1112 | for j in range(cc.GetNumAtoms()): 1113 | pos = cc.GetAtomPosition(j) 1114 | xx.append(pos.x) 1115 | yy.append(pos.y) 1116 | sc = 50 1117 | dx = round(max(xx) - min(xx)) * sc 1118 | dy = round(max(yy) - min(yy)) * sc 1119 | size_x = round(size_x * (1 + (max(dx, dy) - 200) / 500)) 1120 | size = (size_x,) * 2 1121 | # NEW METHOD 1122 | # img = Draw.MolToImage(mol, kekulize=False, wedgeBonds=False, 1123 | # options=opts, size=size) 1124 | 1125 | # OLD METHOD (cannot use Draw.MolToImage or Draw.MolToFile): 1126 | img = Image.new("RGBA", tuple(size)) 1127 | canvas = Canvas(img) 1128 | drawer = Draw.MolDrawing(canvas=canvas, drawingOptions=opts) 1129 | drawer.AddMol(mol) 1130 | canvas.flush() 1131 | 1132 | # Convert each white pixel to transparent. 1133 | pixels = img.getdata() 1134 | new_pixels = [] 1135 | for pix in pixels: 1136 | if pix == (255, 255, 255, 255): 1137 | new_pixels.append((255, 255, 255, 0)) 1138 | else: 1139 | new_pixels.append(pix) 1140 | img.putdata(new_pixels) 1141 | 1142 | if 'IRC' in m.name: 1143 | img = Image.new("RGB", (1,1), (255, 255, 255, 0)) 1144 | 1145 | if i == 0: 1146 | img.save(png_filename.format(id=options['id'], name=m.name, 1147 | confid='')) 1148 | else: 1149 | img.save(png_filename.format(id=options['id'], name=m.name, 1150 | confid=f'_{i}')) 1151 | except (NameError, RuntimeError): 1152 | try: 1153 | options['rdkit4depict'] = 0 1154 | obmol = pybel.readstring("smi", smi) 1155 | obmol.draw(show=False, filename=png_filename.format(id=options['id'], 1156 | name=m.name, 1157 | confid='')) 1158 | img = Image.open(png_filename.format(id=options['id'], 1159 | name=m.name, 1160 | confid='')) 1161 | new_size = (280, 280) 1162 | im_new = Image.new("RGB", new_size, 'white') 1163 | im_new.paste(img, ((new_size[0] - img.size[0]) // 2, 1164 | (new_size[1] - img.size[1]) // 2)) 1165 | im_new.save(png_filename.format(id=options['id'], name=m.name, 1166 | confid='')) 1167 | except NameError: 1168 | print('Could not generate 2d for {n}'.format(n=m.name)) 1169 | return 1170 | 1171 | # end def 1172 | # make the directory with the 2d depictions, if not yet available 1173 | dir = options['id'] + '_2d' 1174 | try: 1175 | os.stat(dir) 1176 | except FileNotFoundError: 1177 | os.mkdir(dir) 1178 | for w in wells: 1179 | smis = [] 1180 | if w.smi is not None: 1181 | smis.append(w.smi) 1182 | else: 1183 | smis = get_smis(w, smis, w.xyz_files) 1184 | generate_2d(w, smis) 1185 | # end for 1186 | for b in bimolecs: 1187 | generate_2d(b, get_smis(b, b.smi, b.xyz_files)) 1188 | # end for 1189 | 1190 | reaction_smi() # write file with reaction string 1191 | 1192 | # end def 1193 | 1194 | 1195 | def updateplot(struct, x_change): 1196 | """ 1197 | Update the plot after the drag event of a stationary point (struct), 1198 | move all related objects by x_change in the x direction, 1199 | and regenerate the corresponding lines 1200 | """ 1201 | global xlow, xhigh, xmargin, ylow, yhigh, ymargin, xlen 1202 | xlow_old = xlow 1203 | xhigh_old = xhigh 1204 | # set the new sizes of the figure 1205 | get_sizes() 1206 | plt.gca().set_xlim([xlow-xmargin, xhigh+xmargin]) 1207 | plt.gca().set_ylim([ylow-ymargin, yhigh+ymargin]) 1208 | ratio = (xhigh - xlow) / (xhigh_old - xlow_old) 1209 | # generate new coordinates for the images 1210 | if struct in imgsd: 1211 | old_extent = imgsd[struct].get_extent() 1212 | extent_change = (x_change, x_change, 0, 0) 1213 | extent = [old_extent[i] + extent_change[i] for i in range(0, 4)] 1214 | imgsd[struct].set_extent(extent=extent) 1215 | # need to scale all other images as well, but only doing it if 1216 | # the overall width has changed 1217 | if ratio != 1: 1218 | for key in imgsd: 1219 | oe = imgsd[key].get_extent() # old extent 1220 | extent = [oe[0], oe[0] + (oe[1] - oe[0]) * ratio, oe[2], oe[3]] 1221 | imgsd[key].set_extent(extent=extent) 1222 | # generate new coordinates for the text 1223 | if struct in textd: 1224 | old_pos = textd[struct].get_position() 1225 | new_pos = (old_pos[0]+x_change, old_pos[1]) 1226 | textd[struct].set_position(new_pos) 1227 | # generate new coordinates for the lines 1228 | for t in tss: 1229 | if (struct == t or struct == t.reactant or struct == t.product): 1230 | t.lines[0] = line(t.x, 1231 | t.y, 1232 | t.reactant.x, 1233 | t.reactant.y, 1234 | [t, t.reactant], 1235 | col=t.color) 1236 | t.lines[1] = line(t.x, 1237 | t.y, 1238 | t.product.x, 1239 | t.product.y, 1240 | [t, t.product], 1241 | col=t.color) 1242 | for i in range(0, 2): 1243 | li = t.lines[i] 1244 | if li.straight_line: 1245 | print('straight line') 1246 | else: 1247 | if options['linear_lines']: 1248 | xlist = [li.xmin, li.xmin + xlen / 2, 1249 | li.xmax - xlen / 2, li.xmax] 1250 | y = [li.y1, li.y1, li.y2, li.y2] 1251 | else: 1252 | xlist = np.arange(li.xmin, 1253 | li.xmax, 1254 | (li.xmax-li.xmin) / 1000) 1255 | a = li.coeffs 1256 | y = a[0]*xlist**3 + a[1]*xlist**2 + a[2]*xlist + a[3] 1257 | linesd[t][i][0].set_xdata(xlist) 1258 | linesd[t][i][0].set_ydata(y) 1259 | # end if 1260 | # end for 1261 | # end if 1262 | # end for 1263 | for b in barrierlesss: 1264 | if (struct == b.reactant or struct == b.product): 1265 | b.line = line(b.reactant.x, 1266 | b.reactant.y, 1267 | b.product.x, 1268 | b.product.y, 1269 | [b.reactant, b.product], 1270 | col=b.color) 1271 | li = b.line 1272 | if li.straight_line: 1273 | print('straight line') 1274 | else: 1275 | if options['linear_lines']: 1276 | xlist = [li.xmin, li.xmin + xlen / 2, 1277 | li.xmax - xlen / 2, li.xmax] 1278 | y = [li.y1, li.y1, li.y2, li.y2] 1279 | else: 1280 | xlist = np.arange(li.xmin, li.xmax, (li.xmax-li.xmin) / 1000) 1281 | a = li.coeffs 1282 | y = a[0]*xlist**3 + a[1]*xlist**2 + a[2]*xlist + a[3] 1283 | linesd[b][0][0].set_xdata(xlist) 1284 | linesd[b][0][0].set_ydata(y) 1285 | # end if 1286 | # end if 1287 | # end for 1288 | plt.draw() 1289 | # end def 1290 | 1291 | 1292 | def highlight_structure(struct=None): 1293 | """ 1294 | for all the lines, text and structures that are not 1295 | directly connected to struct, set alpha to 0.15 1296 | """ 1297 | if struct is None: 1298 | alpha = 1. 1299 | else: 1300 | alpha = 0.15 1301 | # end if 1302 | # get all the tss and barrierlesss with this struct 1303 | highlight = [] 1304 | lines = [] 1305 | for t in tss: 1306 | if t == struct or t.reactant == struct or t.product == struct: 1307 | highlight.append(t) 1308 | if t.reactant not in highlight: 1309 | highlight.append(t.reactant) 1310 | if t.product not in highlight: 1311 | highlight.append(t.product) 1312 | lines = lines + linesd[t] 1313 | # end if 1314 | # end for 1315 | for b in barrierlesss: 1316 | if b.reactant == struct or b.product == struct: 1317 | highlight.append(b) 1318 | if b.reactant not in highlight: 1319 | highlight.append(b.reactant) 1320 | if b.product not in highlight: 1321 | highlight.append(b.product) 1322 | lines = lines + linesd[b] 1323 | # end if 1324 | # end for 1325 | for struct in linesd: 1326 | for li in linesd[struct]: 1327 | if li in lines: 1328 | li[0].set_alpha(1.) 1329 | else: 1330 | li[0].set_alpha(alpha) 1331 | # end if 1332 | # end for 1333 | # end for 1334 | for struct in textd: 1335 | if struct in highlight: 1336 | textd[struct].set_alpha(1.) 1337 | else: 1338 | textd[struct].set_alpha(alpha) 1339 | # end if 1340 | # end for 1341 | for struct in imgsd: 1342 | if struct in highlight: 1343 | imgsd[struct].set_alpha(1.) 1344 | else: 1345 | imgsd[struct].set_alpha(alpha) 1346 | # end if 1347 | # end for 1348 | plt.draw() 1349 | # end def 1350 | 1351 | 1352 | def save_x_values(): 1353 | """ 1354 | save the x values of the stationary points to an external file 1355 | """ 1356 | fi = open('{id}_xval.txt'.format(id=options['id']), 'w') 1357 | if len(wells) > 0: 1358 | lines = ['{n} {v:.2f}'.format(n=w.name, v=w.x) for w in wells] 1359 | fi.write('\n'.join(lines)+'\n') 1360 | if len(bimolecs) > 0: 1361 | lines = ['{n} {v:.2f}'.format(n=b.name, v=b.x) for b in bimolecs] 1362 | fi.write('\n'.join(lines)+'\n') 1363 | if len(tss) > 0: 1364 | lines = ['{n} {v:.2f}'.format(n=t.name, v=t.x) for t in tss] 1365 | fi.write('\n'.join(lines)) 1366 | fi.close() 1367 | # end def 1368 | 1369 | 1370 | def save_im_extent(): 1371 | """Save the x values of the stationary points to an external file""" 1372 | fi = open(f'{options["id"]}_im_extent.txt', 'w') 1373 | for key in imgsd: 1374 | e = imgsd[key].get_extent() 1375 | vals = '{:.2f} {:.2f} {:.2f} {:.2f}'.format(e[0], e[1], e[2], e[3]) 1376 | fi.write('{name} {vals}\n'.format(name=key.name, vals=vals)) 1377 | fi.close() 1378 | # end def 1379 | 1380 | 1381 | def read_im_extent(): 1382 | """Read the extents of the images if they are present in a file_name.""" 1383 | fname = f'{options["id"]}_im_extent.txt' 1384 | if os.path.exists(fname): 1385 | fi = open(fname, 'r') 1386 | a = fi.read() 1387 | fi.close() 1388 | a = a.split('\n') 1389 | for entry in a: 1390 | pieces = entry.split(' ') 1391 | if len(pieces) == 5: 1392 | extsd[pieces[0]] = [eval(pieces[i]) for i in range(1, 5)] 1393 | # end if 1394 | # end for 1395 | # end if 1396 | # end def 1397 | 1398 | def convert_units(energy): 1399 | """Converts energy from 'units' to 'display_units' and apply 'energy_shift'""" 1400 | # apply shift in original units 1401 | energy += options['energy_shift'] 1402 | # convert to kJ/mol first 1403 | if options['units'] == 'kcal/mol': 1404 | energy = energy * 4.184 1405 | elif options['units'] == 'eV': 1406 | energy = energy * 96.4852912 1407 | elif options['units'] == 'Hartree': 1408 | energy = energy * 2625.498413 1409 | # convert to display_units 1410 | if options['display_units'] == 'kcal/mol': 1411 | energy = energy / 4.184 1412 | elif options['display_units'] == 'eV': 1413 | energy = energy / 96.4852912 1414 | elif options['display_units'] == 'Hartree': 1415 | energy = energy / 2625.498413 1416 | return energy 1417 | 1418 | 1419 | def create_interactive_graph(meps): 1420 | """Create an interactive graph with pyvis.""" 1421 | 1422 | g = net.Network(height='1000px', width='90%', heading='', directed=True) 1423 | 1424 | base_energy = next((species.energy for species in wells + bimolecs 1425 | if species.name == options['rescale']), 0) 1426 | for well in wells: 1427 | rel_energy = round(well.energy - base_energy, options['rounding']) 1428 | if np.isnan(rel_energy): 1429 | label = '❌' 1430 | else: 1431 | label = str(rel_energy) 1432 | if not well.energy2 is None: 1433 | rel_energy2 = round(well.energy2 - base_energy, options["rounding"]) 1434 | if np.isnan(rel_energy2): 1435 | label += ' ❌' 1436 | else: 1437 | label += f' {rel_energy2}' 1438 | g.add_node(well.name, label=label, borderWidth=3, title=f'{well.name}', 1439 | shape='circularImage', image=f'{options["id"]}_2d/{well.name}_2d.png', 1440 | size=80, font='30', 1441 | color={'background': '#FFFFFF', 'border': 'black', 1442 | 'highlight': {'border': '#FF00FF', 'background': '#FFFFFF'}}) 1443 | for bim in bimolecs: 1444 | border_color = options['graph_bimolec_color'] 1445 | rel_energy = round(bim.energy - base_energy, options['rounding']) 1446 | if np.isnan(rel_energy): 1447 | label = '❌' 1448 | else: 1449 | label = str(rel_energy) 1450 | if not bim.energy2 is None: 1451 | rel_energy2 = round(bim.energy2 - base_energy, options["rounding"]) 1452 | if np.isnan(rel_energy2): 1453 | label += ' ❌' 1454 | else: 1455 | label += f' {rel_energy2}' 1456 | g.add_node(bim.name, label=label, borderWidth=3, title=f'{bim.name}', 1457 | shape='circularImage', image=f'{options["id"]}_2d/{bim.name}_2d.png', 1458 | size=80, font='30', 1459 | color={'background': '#FFFFFF', 'border': border_color, 1460 | 'highlight': {'border': '#FF00FF', 1461 | 'background': '#FFFFFF'}}) 1462 | 1463 | min_ts_energy = min([ts.energy for ts in tss]) 1464 | max_ts_energy = max([ts.energy for ts in tss]) 1465 | ts_energy_range = max_ts_energy - min_ts_energy 1466 | cmap = plt.get_cmap('viridis') 1467 | 1468 | for ts in tss: 1469 | norm_energy = (ts.energy - min_ts_energy) / ts_energy_range 1470 | if ts in [mep['bottle_neck'] for mep in meps]: 1471 | color = '#E9C46A' 1472 | elif ts in [rxn for mep in meps for rxn in mep['rxns']]: 1473 | color = '#2A9D8F' 1474 | elif options['graph_edge_color'] == 'energy': 1475 | red, green, blue = np.array(cmap.colors[int(norm_energy * 255)]) * 255 1476 | color = f'rgb({red},{green},{blue})' 1477 | else: 1478 | color = ts.color 1479 | rel_energy = round(ts.energy - base_energy, options["rounding"]) 1480 | g.add_edge(ts.reactant.name, ts.product.name, 1481 | title=f'{rel_energy} {options["display_units"]}', 1482 | color={'highlight': '#FF00FF', 'color': color}, 1483 | width=(1 - norm_energy) * 20 + 1, arrows='') 1484 | 1485 | for bless in barrierlesss: 1486 | norm_energy = (bless.product.energy - min_ts_energy) / ts_energy_range 1487 | if options['graph_edge_color'] == 'energy': 1488 | red, green, blue = np.array(cmap.colors[int(norm_energy * 255)]) * 255 1489 | color = f'rgb({red},{green},{blue})' 1490 | else: 1491 | color = bless.color 1492 | rel_energy = round(bless.product.energy - base_energy, options["rounding"]) 1493 | g.add_edge(bless.reactant.name, bless.product.name, 1494 | title=f'{rel_energy} {options["display_units"]}', 1495 | color={"highlight": "#FF00FF", 'color': color}, 1496 | width=(1 - norm_energy) * 20 + 1, arrows='') 1497 | 1498 | g.set_edge_smooth('dynamic') 1499 | g.show_buttons(filter_=['physics']) 1500 | g.save_graph(f'{options["id"]}.html') 1501 | 1502 | return 0 1503 | 1504 | 1505 | def is_path_valid(path): 1506 | return all(['_' not in name for name in path[1:-1]]) 1507 | 1508 | 1509 | def write_section(f, input_lines, stopsign, start, path): 1510 | for ll, line in enumerate(input_lines[start:]): 1511 | if not line.startswith(stopsign): 1512 | if stopsign == '> ': 1513 | if line.startswith('> '): 1514 | f.write(f'> aux_{path[0]}_{path[-1]}\n') 1515 | elif line.startswith('plot'): 1516 | f.write(f'plot 1\n') 1517 | elif line.startswith('save'): 1518 | f.write('save 0\n') 1519 | elif line.startswith('path_report') or line.startswith('search_cutoff'): 1520 | continue 1521 | else: 1522 | f.write(f'{line}\n') 1523 | elif stopsign == '> ' or stopsign == '> ': 1524 | if line.split()[0] in path: 1525 | f.write(f'{line}\n') 1526 | elif line.startswith('>'): 1527 | f.write(f'{line}\n') 1528 | elif stopsign == '> ': 1529 | if len(line.split()) < 4: 1530 | f.write(f'{line}\n') 1531 | elif line.split()[2] in path and line.split()[3] in path: 1532 | if abs(path.index(line.split()[2]) - path.index(line.split()[3])) != 1: 1533 | continue 1534 | else: 1535 | f.write(f'{line}\n') 1536 | elif stopsign == '> ': 1537 | if len(line.split()) < 3: 1538 | f.write(f'{line}\n') 1539 | elif line.split()[1] in path and line.split()[2] in path: 1540 | if abs(path.index(line.split()[1]) - path.index(line.split()[2])) != 1: 1541 | continue 1542 | else: 1543 | f.write(f'{line}\n') 1544 | else: 1545 | f.write(f'{line}\n') 1546 | else: 1547 | return ll+start 1548 | 1549 | 1550 | def gen_graph(): 1551 | """Generate a networkx graph object to work with 1552 | 1553 | Returns: 1554 | networkx.Graph: A Graph object representation of the PES. 1555 | """ 1556 | graph = nx.Graph() 1557 | base_energy = next((species.energy for species in wells + bimolecs 1558 | if species.name == options['rescale']), 0) 1559 | 1560 | for reac in tss + barrierlesss: 1561 | rname = reac.reactant.name 1562 | pname = reac.product.name 1563 | graph.add_edge(rname, pname) 1564 | graph[rname][pname]['energy'] = round(reac.energy - base_energy, 1) 1565 | 1566 | return graph 1567 | 1568 | 1569 | def find_mep(graph, user_input): 1570 | """Find the minimum energy path between two species in a PES. 1571 | 1572 | Args: 1573 | graph (networkx.Graph): A graph object containing the information of 1574 | the PES 1575 | user_input (str): The input file passed to pesviewer. 1576 | 1577 | Returns: 1578 | NoneType: None 1579 | """ 1580 | meps = [] 1581 | for species_pair in options['path_report']: 1582 | meps.append({}) 1583 | current_mep = meps[-1] 1584 | spec_1 = species_pair[0] 1585 | spec_2 = species_pair[1] 1586 | max_length = options['search_cutoff'] 1587 | paths = nx.all_simple_paths(graph, spec_1, spec_2, cutoff=max_length) 1588 | max_barr = np.inf 1589 | for valid_path in [path for path in paths if is_path_valid(path)]: 1590 | path_energies = [graph[valid_path[i]][valid_path[i+1]]['energy'] 1591 | for i in range(len(valid_path)-1)] 1592 | if (max_barr == max(path_energies) and len(valid_path) < len(mep_species)) \ 1593 | or max_barr > max(path_energies): 1594 | max_barr = max(path_energies) # the bottle neck 1595 | mep_species = valid_path 1596 | bottle_neck_idx = path_energies.index(max_barr) 1597 | current_mep['species'] = mep_species 1598 | current_mep['energies'] = path_energies 1599 | current_mep['rxns'] = [] 1600 | for i, sp in enumerate(mep_species[:-1]): 1601 | for rxn in tss + barrierlesss: 1602 | rname = rxn.reactant.name 1603 | pname = rxn.product.name 1604 | if (rname == sp and pname == mep_species[i+1]) \ 1605 | or (pname == sp and rname == mep_species[i+1]): 1606 | current_mep['rxns'].append(rxn) 1607 | current_mep['bottle_neck'] = current_mep['rxns'][bottle_neck_idx] 1608 | 1609 | print(f'The bottle neck barrier between {spec_1} and {spec_2} with a ' \ 1610 | f'{options["search_cutoff"]} depth search is {max_barr} ' \ 1611 | f'{options["display_units"]} high.') 1612 | 1613 | # write new pesviewer input for just this path 1614 | input_lines = user_input.split('\n') 1615 | with open(f'{mep_species[0]}_{mep_species[-1]}.inp', 'w') as f: 1616 | stop = write_section(f, input_lines, '> ', 0, mep_species) 1617 | stop = write_section(f, input_lines, '> ', stop, mep_species) 1618 | stop = write_section(f, input_lines, '> ', stop, mep_species) 1619 | stop = write_section(f, input_lines, '> ', stop, mep_species) 1620 | stop = write_section(f, input_lines, '> ', stop, mep_species) 1621 | 1622 | return meps 1623 | 1624 | 1625 | def main(fname=None): 1626 | """Main method to run the PESViewer""" 1627 | if fname is None and len(sys.argv) > 1: 1628 | fname = sys.argv[1] 1629 | options['save'] = 0 1630 | options['save_from_command_line'] = 0 1631 | if len(sys.argv) > 2 and sys.argv[2] == 'save': # Save the plot. 1632 | options['save'] = 1 1633 | options['save_from_command_line'] = 1 1634 | elif fname is None and len(sys.argv) == 1: 1635 | print('To use the pesviewer, supply an input file as argument.') 1636 | sys.exit(-1) 1637 | user_input = read_input(fname) # read the input file 1638 | # initialize the dictionaries 1639 | for w in wells: 1640 | linesd[w] = [] 1641 | 1642 | for b in bimolecs: 1643 | linesd[b] = [] 1644 | 1645 | for t in tss: 1646 | linesd[t] = [] 1647 | 1648 | for b in barrierlesss: 1649 | linesd[b] = [] 1650 | 1651 | read_im_extent() # read the position of the images, if known 1652 | position() # find initial positions for all the species on the graph 1653 | generate_lines() # generate all the line 1654 | # generate 2d depiction from the smiles or 3D structure, 1655 | # store them in join(input_id, '_2d') 1656 | generate_2d_depiction() 1657 | graph = gen_graph() 1658 | meps = find_mep(graph, user_input) 1659 | if options['plot']: 1660 | plot() 1661 | if meps: 1662 | print('To draw 2D plots for the individual MEPs type:') 1663 | for mep in meps: 1664 | print(f'\tpesviewer {mep["species"][0]}_{mep["species"][-1]}.inp') 1665 | 1666 | if options['graph_plot']: 1667 | create_interactive_graph(meps) 1668 | 1669 | 1670 | def pesviewer(fname=None): 1671 | options['save'] = 0 1672 | options['save_from_command_line'] = 0 1673 | main(fname) 1674 | 1675 | 1676 | if __name__ == "__main__": 1677 | main() 1678 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools.packages.find] 6 | exclude = ["build", "dist", ".idea", ".vscode", "PESViewer.egg-info"] 7 | 8 | [project] 9 | name = "pesviewer" 10 | version = "1.1.0" 11 | description = "Potential Energy Surface Visualizer" 12 | readme = "README.md" 13 | requires-python = ">=3.6.0" 14 | license = {file = "LICENSE"} 15 | 16 | authors = [ 17 | {name="Ruben Van de Vijver", email="ruben.vandevijver@ugent.be"}, 18 | {name="Judit Zádor", email="jzador@sandia.gov"}, 19 | {name="Carles Martí", email="cmartia@sandia.gov"}, 20 | ] 21 | maintainers = [ 22 | {name="Ruben Van de Vijver", email="ruben.vandevijver@ugent.be"}, 23 | {name="Judit Zádor", email="jzador@sandia.gov"}, 24 | {name="Carles Martí", email="cmartia@sandia.gov"}, 25 | ] 26 | classifiers = [ 27 | "Environment :: Console", 28 | "Intended Audience :: Science/Research", 29 | "Natural Language :: English", 30 | "License :: OSI Approved :: BSD License", 31 | "Operating System :: OS Independent", 32 | "Programming Language :: Python :: 3.6", 33 | "Programming Language :: Python :: 3.7", 34 | "Programming Language :: Python :: 3.8", 35 | "Programming Language :: Python :: 3.9", 36 | "Programming Language :: Python :: 3.10", 37 | "Topic :: Scientific/Engineering :: Chemistry" 38 | ] 39 | dependencies = [ 40 | "numpy>=1.17.0", 41 | "matplotlib", 42 | "networkx", 43 | "pillow", 44 | "pyvis", 45 | ] 46 | 47 | [project.urls] 48 | homepage = "https://github.com/zadorlab/PESViewer" 49 | documentation = "https://github.com/zadorlab/PESViewer/wiki" 50 | 51 | [project.scripts] 52 | pesviewer = "pesviewer.pesviewer:main" 53 | --------------------------------------------------------------------------------