├── mechasvg ├── __init__.py ├── supl │ ├── __init__.py │ ├── xlsx.gif │ ├── image.png │ ├── mechasvg.gif │ ├── README.txt │ ├── example_1.svg │ ├── example_3.svg │ └── example_2.svg └── __main__.py ├── .gitignore ├── setup.py ├── LICENSE.txt └── README.md /mechasvg/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mechasvg/supl/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | **/__pychache__ 3 | .E_profile.svg 4 | *.ssf -------------------------------------------------------------------------------- /mechasvg/supl/xlsx.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricalmang/mechaSVG/HEAD/mechasvg/supl/xlsx.gif -------------------------------------------------------------------------------- /mechasvg/supl/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricalmang/mechaSVG/HEAD/mechasvg/supl/image.png -------------------------------------------------------------------------------- /mechasvg/supl/mechasvg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricalmang/mechaSVG/HEAD/mechasvg/supl/mechasvg.gif -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | with open("mechasvg\\supl\\README.txt", "r") as fh: 3 | long_description = fh.read() 4 | setuptools.setup( 5 | name="mechaSVG", 6 | version="0.1.1", 7 | author="Ricardo Almir Angnes", 8 | author_email="ricardo_almir@hotmail.com", 9 | description="mechaSVG is a python & tk application for creating good-looking energy profile diagrams as Scalable Vector Graphics.", 10 | long_description=long_description, 11 | license="MIT", 12 | url="https://github.com/ricalmang/mechaSVG", 13 | keywords = ['chemistry'], 14 | install_requires = ["openpyxl"], 15 | packages=setuptools.find_packages(), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | ], 21 | python_requires='>=3.6', 22 | ) -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ricardo Almir Angnes 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. -------------------------------------------------------------------------------- /mechasvg/supl/README.txt: -------------------------------------------------------------------------------- 1 | **mechaSVG** 2 | 3 | mechaSVG is a python & tk application for creating good-looking energy profile diagrams as *Scalable Vector Graphics* or **'.svg'** files with various aesthetic options. The produced graphics can also be easily edited afterwards via an svg editor like Inkscape. Extra analysis for catalytic cycle diagrams include: Finding energy span, Turn Over Frequency (TOF) estimation from both energy span and *catalytic-flux law*, and estimating degree of TOF control of both intermediates and transition states. 4 | 5 | **Latest updates** 6 | 7 | -Various graph styles and personalization options added. 8 | 9 | -Make energy comparisons betwen structures. 10 | 11 | -Plot up to 30 structures 2D PES (Potential Energy Surface). 12 | 13 | -Slight modifications to the user interface. 14 | 15 | -Support for ".txt" and ".xlsx" imports and exports added. Please refer to section "**Importing .txt and .xlsx**" for information about importing these file types. 16 | 17 | **Installing & Running** 18 | 19 | The recommended installaion requires python 3.6 or above. 20 | 21 | Fresh instalation can be done via: 22 | 23 | ```python3 -m pip install mechasvg``` 24 | 25 | Upgrading from older versions can be done via: 26 | 27 | ```python3 -m pip install -U mechasvg``` 28 | 29 | The program can be run via: 30 | 31 | ```python3 -m mechasvg``` 32 | 33 | Alternatively, a windons 10 executable can be downloaded on github: 34 | 35 | github.com/ricalmang/mechasvg 36 | 37 | **How to cite** 38 | 39 | I don't actually require users to cite the software at all, so you can take any liberty in how or whether you cite it. However, a citation would greatly help to spread the visibility and adoption of this project which is my main goal. 40 | 41 | If you intend to cite it, the following citation should suffice: 42 | 43 | Angnes, R. A. mechaSVG, GitHub repository, 2020, doi: 10.5281/zenodo.3970267. 44 | 45 | 46 | **Acknowledgments** 47 | 48 | This project was funded by São Paulo Research Foundation (FAPESP) under grant 2019/02052-4. 49 | 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mechaSVG 2 | 3 | mechaSVG is a python & tk application for creating good-looking energy profile diagrams as *Scalable Vector Graphics* or **'.svg'** files with various aesthetic options. The produced graphics can also be easily edited afterwards via an svg editor like Inkscape. Extra analysis for catalytic cycle diagrams include: Finding energy span, Turn Over Frequency (TOF) estimation from both energy span and *catalytic-flux law*, and estimating degree of TOF control of both intermediates and transition states.¹\ 4 | [Watch on Youtube](https://youtu.be/0FfNRQJCJAs)\ 5 | ![GitHub All Releases](https://img.shields.io/github/downloads/ricalmang/mechasvg/total?color=lgreen&label=GitHub%20downloads) ![PyPI - Downloads](https://img.shields.io/pypi/dw/mechasvg?color=lgreen&label=PyPI%20downloads) 6 | 7 | ## Latest updates 8 | –Various graph styles and personalization options added.\ 9 | –Make energy comparisons betwen structures.\ 10 | –Plot up to 30 structures per 2D PES (Potential Energy Surface).\ 11 | –Slight modifications to the user interface.\ 12 | –Support for ".txt" and ".xlsx" imports and exports added. Please refer to section "**Importing .txt and .xlsx**" for information about importing these file types. 13 | 14 | ## Preview 15 | 16 | The following pictures ilustrate the usage of the graphical user interface and the corresponding output 17 | 18 | ### Graphical interface 19 | 20 | ![Interface](mechasvg/supl/image.png) 21 | 22 | ### Output graphs 23 | 24 | #### Example 1 25 | 26 | ![Graph 1](mechasvg/supl/example_1.svg) 27 | 28 | #### Example 2 29 | 30 | ![Graph 2](mechasvg/supl/example_2.svg) 31 | 32 | #### Example 3 33 | 34 | ![Graph 2](mechasvg/supl/example_3.svg) 35 | 36 | These svg graphs can be easily post-edited to fit your needs using freely available svg editors like [Inkscape](https://inkscape.org/). 37 | Moreover, **Inkscape** can save them in many different image formats including **".emf"**, which in turn can be imported into some of the most popular chemical structure drawing softwares with **minimal loss in image quality**. 38 | 39 | ## Installing & Running 40 | 41 | The recommended installaion requires python 3.6 or above. 42 | 43 | Fresh instalation can be done via: 44 | ```bash 45 | python3 -m pip install setuptools mechasvg 46 | ``` 47 | Upgrading from older versions can be done via: 48 | ```bash 49 | python3 -m pip install -U mechasvg 50 | ``` 51 | The program can be run via: 52 | ```bash 53 | python3 -m mechasvg 54 | ``` 55 | 56 | Alternatively, a Windons 10 executable can be downloaded on the following link:\ 57 | [**mechaSVG-v011.exe**](https://github.com/ricalmang/mechaSVG/releases/download/v0.1.1/mechaSVG-v011.exe) 58 | 59 | ## Importing .txt and .xlsx 60 | 61 | Data for mechaSVG can be inserted directly onto the user interface or be imported from xlsx and txt files. 62 | Importing files can be done with the **Open** button on the user interface or by running the following command: 63 | ```bash 64 | python3 -m mechasvg /path/to/file/example.txt 65 | ``` 66 | ### .txt files 67 | 68 | Data for .txt imports should be writen in a plain text file like in the following example: 69 | ```bash 70 | Structure_A_Name Free_energy Entalphy 71 | Structure_B_Name Free_energy Entalphy 72 | Structure_C_Name Free_energy 73 | Structure_D_Name Free_energy 74 | · 75 | · 76 | · 77 | ``` 78 | Please notice that no spaces are allowed on structure names or energies.\ 79 | The following example ilustrates how to populate data into multiple path tabs with a single txt file. 80 | ```bash 81 | Path_A_Structure_A Free_energy Entalphy 82 | Path_A_Structure_B Free_energy Entalphy 83 | Path_A_Structure_C Free_energy Entalphy 84 | Path_A_Structure_D Free_energy 85 | #A 86 | Path_B_Structure_A Free_energy 87 | Path_B_Structure_B Free_energy 88 | #B 89 | · 90 | · 91 | · 92 | 93 | ``` 94 | Please note that only the following tab flags area available: 95 | \#A, #B, #C, #D, #E, #F, #G and #H 96 | 97 | ### .xlsx files 98 | 99 | Data for .xlsx imports should be writen starting at the top left cell (i.e. A1 cell). Every sheet will be consecutively atributed to a reaction path tab on mechaSVG. 100 | 101 | | .xlsx | mechaSVG | 102 | | :-------------------------: | :-------------------------:| 103 | | ![.xlsx](mechasvg/supl/xlsx.gif) | ![mechaSVG](mechasvg/supl/mechasvg.gif) | 104 | 105 | .xlsx files can be generated using [LibreOffice Calc](https://www.libreoffice.org/) or Microsoft Excel. 106 | 107 | 108 | ## How to cite 109 | 110 | I don't actually require users to cite the software at all, so you can take any liberty in how or whether you cite it. However, a citation would greatly help to spread the visibility and adoption of this project which is my main goal. 111 | 112 | If you intend to cite it, the following citation should suffice: 113 | 114 | Angnes, R. A. mechaSVG, GitHub repository, 2020, doi: 10.5281/zenodo.4065333. 115 | 116 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.4065333.svg)](https://doi.org/10.5281/zenodo.4065333) 117 | 118 | ## Acknowledgments 119 | 120 | This project was funded by São Paulo Research Foundation (FAPESP) under grant 2019/02052-4. 121 | 122 | ## References 123 | 124 | ¹Kozuch, S. & Shaik, S. *Acc. Chem. Res.* **2011**, *44*, 101. [(Paper)](https://pubs.acs.org/doi/10.1021/ar1000956) -------------------------------------------------------------------------------- /mechasvg/supl/example_1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Main Title 6 | ΔG in kcal·mol⁻¹ 7 | Reaction coordinate 8 | 9 | 10 | 11 | 24.0 12 | 13 | 20.0 14 | 15 | 16.0 16 | 17 | 12.0 18 | 19 | 8.0 20 | 21 | 4.0 22 | 23 | 0.0 24 | 25 | -4.0 26 | 27 | -8.0 28 | 29 | -12.0 30 | 31 | A 32 | 9.8 33 | 34 | B 35 | -10.2 36 | 37 | 38 | C 39 | 7.8 40 | 41 | 42 | D 43 | 21.1 44 | 45 | 46 | E 47 | 15.6 48 | 49 | 50 | F 51 | 6.8 52 | 53 | 54 | G 55 | 24.4 56 | 57 | 58 | H 59 | 8.8 60 | 61 | 62 | I 63 | 1.4 64 | 65 | 66 | J 67 | 12.9 68 | 69 | 70 | A' 71 | -11.5 72 | 73 | 74 | 75 | 76 | 77 | 34.6 78 | 79 | TDI 80 | 81 | TDTS 82 | 83 | Delta = -21.39 84 | Span = 34.62 85 | 86 | -------------------------------------------------------------------------------- /mechasvg/supl/example_3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Curtin–Hammett principle 5 | ΔG in kcal·mol⁻¹ 6 | Reaction coordinate 7 | 8 | 9 | 10 | 11 | 12 | 13 | 28.0 14 | 15 | 24.0 16 | 17 | 20.0 18 | 19 | 16.0 20 | 21 | 12.0 22 | 23 | 8.0 24 | 25 | 4.0 26 | 27 | 0.0 28 | 29 | -4.0 30 | 31 | -8.0 32 | 33 | -12.0 34 | 35 | -16.0 36 | 37 | C 38 | -5.2 39 | 40 | TS(AC) 41 | 23.2 42 | 43 | 44 | A 45 | 3.5 46 | 47 | 48 | TS(AB) 49 | 8.2 50 | 51 | 52 | B 53 | 0.0 54 | 55 | 56 | TS(BD) 57 | 26.7 58 | 59 | 60 | D 61 | -13.8 62 | 63 | 64 | 65 | 66 | 67 | 3.5 68 | 69 | 70 | 71 | 72 | 73 | 8.7 74 | 75 | 76 | 77 | 78 | 79 | 13.8 80 | 81 | 82 | 83 | 84 | 85 | 3.5 86 | 87 | 88 | -------------------------------------------------------------------------------- /mechasvg/supl/example_2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Main Title 6 | ΔG in kcal·mol⁻¹ 7 | Reaction coordinate 8 | 9 | 10 | 11 | 32.0 12 | 13 | 28.0 14 | 15 | 24.0 16 | 17 | 20.0 18 | 19 | 16.0 20 | 21 | 12.0 22 | 23 | 8.0 24 | 25 | 4.0 26 | 27 | 0.0 28 | 29 | -4.0 30 | 31 | -8.0 32 | 33 | -12.0 34 | 35 | -16.0 36 | 37 | A 38 | 9.8 39 | [10.8] 40 | 41 | B 42 | -10.2 43 | [-9.8] 44 | 45 | 46 | C 47 | 7.8 48 | [8.5] 49 | 50 | 51 | D 52 | 21.1 53 | [21.4] 54 | 55 | 56 | E 57 | 15.6 58 | [16.2] 59 | 60 | 61 | F 62 | 6.8 63 | [6.4] 64 | 65 | 66 | G 67 | 24.4 68 | [24.1] 69 | 70 | 71 | H 72 | 8.8 73 | [8.4] 74 | 75 | 76 | I 77 | 1.4 78 | [1.9] 79 | 80 | 81 | J 82 | 12.9 83 | [12.0] 84 | 85 | 86 | A' 87 | -11.5 88 | [-12.0] 89 | 90 | 91 | A 92 | 15.8 93 | [15.9] 94 | 95 | B 96 | -5.2 97 | [-4.4] 98 | 99 | 100 | C 101 | 7.8 102 | [8.8] 103 | 104 | 105 | D 106 | 24.1 107 | [24.4] 108 | 109 | 110 | E 111 | 17.6 112 | [17.9] 113 | 114 | 115 | F 116 | 11.8 117 | [12.6] 118 | 119 | 120 | G 121 | 29.4 122 | [29.9] 123 | 124 | 125 | H 126 | 10.8 127 | [11.4] 128 | 129 | 130 | I 131 | 4.4 132 | [5.0] 133 | 134 | 135 | J 136 | 14.9 137 | [15.2] 138 | 139 | 140 | A' 141 | -11.5 142 | [-11.1] 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 17.0 151 | 152 | 153 | -------------------------------------------------------------------------------- /mechasvg/__main__.py: -------------------------------------------------------------------------------- 1 | import os, random, datetime, functools, math, sys, subprocess 2 | try: 3 | import tkinter as tk 4 | from tkinter import ttk, filedialog, messagebox 5 | except ImportError as message: 6 | print(message) 7 | print("Please make sure you have python3, tkinter and ttk installed") 8 | input("Press enter to leave") 9 | exit() 10 | 11 | class Preferences: 12 | def __init__(self): 13 | ################################ RELATIVELY STRAIGHTFORWARD MODIFICATIONS ###################################### 14 | # number of structures for each path 15 | self.n_structures = 30 16 | self.n_connectors = 8 17 | self.n_comparers = 5 18 | #run command 19 | self.windons_command = "start inkscape.exe ./.E_profile.svg" # Please note .E_profile.svg is a hidden file! 20 | self.linux_command = "inkscape ./.E_profile.svg" # Please note .E_profile.svg is a hidden file! 21 | self.command_line = self.windons_command if os.name == "nt" else self.linux_command 22 | # SVG colors 23 | self.menu_a = ["grey","black","blue","darkblue","red","darkred","green","darkgreen"] 24 | #SVG line widths 25 | self.menu_b = ["2","3","4","5","6"] 26 | #SVG line stiles 27 | self.svg_repl = {"full": "","dashed":'stroke-dasharray="10,10"',"dashed1":'stroke-dasharray="6,6"',"dashed2":'stroke-dasharray="4,4"',"dashed3":'stroke-dasharray="2,2"'} 28 | # Random PES generator 29 | self.trickster = True # Include random PES generator? 30 | self.name = "MechaSVG v 0.1.1" 31 | ######################## YOU ARE PROBABLY BETTER OFF NOT MESSING WITH THE FOLLOWING ############################ 32 | self.menu_c = list(self.svg_repl.keys()) 33 | # TDI and TDTS placement corrections 34 | self.placement = { 35 | "Top" :[[-37,-22,-7],[-22,-7,0]], 36 | "Middle" :[[-5,15,30],[-5,15,30]], 37 | "Bottom" :[[17,32,47],[17,32,0]] 38 | } 39 | self.alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZðƿþæœȝ" 40 | self.menu_d = list(self.placement.keys()) 41 | self.menu_e = [self.alphabet[n] for n in range(8)] #Will change the number of Paths available 42 | self.menu_f = ["opt_{}".format(a.lower()) for a in self.menu_e] 43 | self.menu_g = ["tab_{}".format(a.lower()) for a in self.menu_e] 44 | self.menu_h = ["Path {}".format(a) for a in self.menu_e] 45 | self.menu_i = ["#{}".format(a) for a in self.menu_e] 46 | self.menu_z = [" ","TS","INT"] 47 | try: 48 | import openpyxl 49 | self.xlsx = True 50 | except ImportError: 51 | self.xlsx = False 52 | if self.xlsx: 53 | self.allowed_extensions = { 54 | "title": "Save state", 55 | "defaultextension": ".xlsx", 56 | "filetypes": [("Spreadsheet", ".xlsx"), ("Text file", ".txt")]} 57 | else: 58 | self.allowed_extensions = { 59 | "title": "Save state", 60 | "defaultextension": ".txt", 61 | "filetypes": [("Text file", ".txt")]} 62 | filename = sys.argv[-1] 63 | ext = [".xlsx",".txt"] if self.xlsx else [".txt"] 64 | if os.path.isfile(filename) and any(filename.endswith(a) for a in ext): 65 | self.filename = filename 66 | 67 | i = [] 68 | labels = ["XY", "Y only", "X,Y","Frame", "X only", "Borderless"] 69 | i.append("^ #^ #^ #┌───────────┐# # ") 70 | i.append("│_ _ _ #│_ _ _ #│_ _ _ #│_ _ _ │# _ _ _ # _ _ _ ") 71 | i.append("│ \_/ \_/ \_ #│ \_/ \_/ \_ #│ \_/ \_/ \_ #│ \_/ \_/ \_│# \_/ \_/ \_ # \_/ \_/ \_ ") 72 | i.append("└───────────>#˅ #˅ <───────> #└───────────┘# <─────────> # ") 73 | self.image = {} 74 | for idx,label in zip(range(len(i[0].split("#"))),labels): 75 | "XY", "X,Y", "X only", "Y only", "Frame", "Borderless" 76 | self.image[label] = "\n".join([[a.split("#") for a in i][n][idx] for n in range(4)]) 77 | self.hints = [ 78 | "Hint: svg graphs can be easily eddited with freely available svg editors like Inkscape.", 79 | "Hint: mechaSVG can read and save .xlsx files for MS excel or libreoffice.", 80 | "Hint: mechaSVG can read and save .txt files.", 81 | "Hint: Are labels overlaid? Try changing the 'Alignment' settings or 'Vertical' displacements.", 82 | "Hint: On the 'Horizontal' tab you can edit the horizontal length of the graph.", 83 | "Hint: The 'TS mark' option on 'Labels' only modifies labels if the corresponding structure 'Type' is set to 'TS'.", 84 | "Hint: On the 'Main' tab you can chosse wheter to plot Free energy or Enthalpy via the option 'Use Enthalpy instead of free energy'.", 85 | "Hint: mechaSVG ignores the type of data that is not being plotted i.e., if you are plotting Free energy, the values for Henthalpy --need not-- be numeric.", 86 | "Hint: Inkscape allows one to save '.svg' files as '.emf' files. '.emf' files can be imported to some of the mainstream chemical structure drawing programs with minimal loss in image quality.", 87 | "Hint: When you click on the preview buttons 'Command' or 'Default', mechaSVG will save a hidden file on the current directory named '.E_profile.svg'. Thereafter, it will either execute the command inside the textbox (Command) or try to open the svg file with the default .svg reader (Default).", 88 | "Hint: All options under the 'Span' tab are based on the following reference: ¹Kozuch, S. & Shaik, S. Acc. Chem. Res. 2011, 44, 101." 89 | ] 90 | 91 | def is_str_float(i): 92 | try: float(i); return True 93 | except ValueError: return False 94 | 95 | class Note(ttk.Notebook): 96 | def __init__(self,parent,*args,**kwargs): 97 | ttk.Notebook.__init__(self,parent,*args,**kwargs) 98 | for a,b in zip(pref.menu_g,pref.menu_h): 99 | setattr(self, a,TabFramePaths(self,name=b)) 100 | self.tab_comparers = TabFrameComparers(self, name="Comparers") 101 | self.tab_connections = TabFrameConnections(self,name="Connections") 102 | self.pack(fill="both", expand=True) 103 | 104 | class ScrollingFrame(tk.Frame): 105 | # Adapted from stackoverflow.com/a/3092341/13702856 106 | def __init__(self, parent): 107 | tk.Frame.__init__(self, parent) 108 | self.canvas = tk.Canvas(self) 109 | self.frame = tk.Frame(self.canvas) 110 | self.vsb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview) 111 | self.canvas.configure(yscrollcommand=self.vsb.set) 112 | self.vsb.pack(side="right", fill="y") 113 | self.canvas.pack(side="left", fill="both", expand=True) 114 | self.canvas.create_window((4,4), window=self.frame, anchor="nw", tags="self.frame") 115 | self.frame.bind("", self.onFrameConfigure) 116 | self.pack(side="bottom") 117 | def onFrameConfigure(self, event): 118 | '''Reset the scroll region to encompass the inner frame''' 119 | self.canvas.configure(scrollregion=self.canvas.bbox("all")) 120 | 121 | class TabFramePaths(ttk.Frame): 122 | def __init__(self,parent,name,*args,**kwargs): 123 | ttk.Frame.__init__(self,parent,*args,**kwargs) 124 | self.parent = parent 125 | self.parent.add(self, text=name) 126 | g = tk.Frame(self) 127 | z=tk.Frame(g) 128 | z.pack(side="left",fill="x") 129 | g.pack(side="top",fill="x") 130 | y = ScrollingFrame(self) 131 | y.pack(side="bottom",fill="both",expand=True) 132 | x = y.frame 133 | ######### 134 | self._build_options(z) 135 | ######### 136 | self.widths = [3,3,16,10,10,1,1,6] 137 | for a,b,c in zip(range(len(self.widths)),[" ","Type",'Structure Name','Free Energy','Enthalpy',"Move","",'Alignment'],self.widths): 138 | if a == 6: continue 139 | label = tk.Label(x, text=b,width=4 if a == 5 else c) 140 | if a == 5: label.grid(column=a, row=9, rowspan=1, columnspan = 2,sticky="news") 141 | else: label.grid(column=a, row=9, rowspan=1,sticky="news") 142 | ######### 143 | self.data = [[None,None,None,None,None] for _ in range(pref.n_structures)] 144 | ######### 145 | for n in range(pref.n_structures): 146 | label = tk.Label(x, text='#{}'.format(n+1),width=self.widths[0]) 147 | label.grid(column=0, row=10+n, rowspan=1) 148 | for b in [1,2,3]: 149 | self.data[n][b] = tk.Entry(x,justify=tk.CENTER,bd=2,width=self.widths[1+b]) 150 | self.data[n][b].insert(0,"") 151 | self.data[n][b].grid(column=1+b, row=10+n,padx="0",sticky="news") 152 | if not n == 0: 153 | button = tk.Button(x, text=u'\u2191', command=lambda x = n: self._move(x,x-1), padx="1") 154 | button.config(width=self.widths[5]) 155 | button.grid(column=5, row=10+n) 156 | if not n+1 == pref.n_structures: 157 | button = tk.Button(x, text=u'\u2193', command=lambda x = n: self._move(x,x+1), padx="1") 158 | button.config(width=self.widths[6]) 159 | button.grid(column=6, row=10+n) 160 | self.data[n][0] = tk.StringVar() 161 | menu = tk.OptionMenu(x,self.data[n][0],*pref.menu_z) 162 | self.data[n][0].set(pref.menu_z[0]) 163 | menu.config(width=self.widths[1]) 164 | menu.grid(column=1, row=10+n) 165 | self.data[n][4] = tk.StringVar() 166 | menu = tk.OptionMenu(x,self.data[n][4],*pref.menu_d) 167 | self.data[n][4].set(pref.menu_d[1]) 168 | menu.config(width=self.widths[7]) 169 | menu.grid(column=7, row=10+n) 170 | def _build_options(self,x): 171 | self.option_menu = ttk.Frame(x) 172 | setattr(self.option_menu,"line_opt_data",[[None,None,None],[None,None,None]]) 173 | for n,name in enumerate(["Main[Color/Width/Strike]","Link[Color/Width/Strike]"]): 174 | box = self.boxify(self.option_menu,name=name,column=n) 175 | for a,b in zip([0,1,2],[pref.menu_a,pref.menu_b,pref.menu_c]): 176 | self.option_menu.line_opt_data[n][a] = tk.StringVar() 177 | color_menu = tk.OptionMenu(box, self.option_menu.line_opt_data[n][a],*b) 178 | self.option_menu.line_opt_data[n][a].set(b[[1,1,0][a] if n == 0 else [1,0,2][a]]) 179 | color_menu.config(width=["9","1","7"][a]) 180 | color_menu.grid(column=a,row=0,sticky="news") 181 | self.option_menu.grid(column=0,row=0,columnspan = 8,rowspan=8,sticky="news") 182 | def _move(self,n,x): 183 | line_n = [a.get() for a in self.data[n]] 184 | other = [a.get() for a in self.data[x]] 185 | for i, a in enumerate(other): 186 | if type(self.data[n][i]) is tk.Entry: 187 | self.data[n][i].delete(0,tk.END) 188 | self.data[n][i].insert(0,a) 189 | elif type(self.data[n][i]) is tk.StringVar: 190 | self.data[n][i].set(a) 191 | for i, a in enumerate(line_n): 192 | if type(self.data[x][i]) is tk.Entry: 193 | self.data[x][i].delete(0,tk.END) 194 | self.data[x][i].insert(0,a) 195 | elif type(self.data[x][i]) is tk.StringVar: 196 | self.data[x][i].set(a) 197 | @staticmethod 198 | def boxify(other, name, column): 199 | box = ttk.LabelFrame(other, text=name) 200 | box.grid(column=column, row=0, sticky="news") 201 | return box 202 | class TabFrameConnections(ttk.Frame): 203 | def __init__(self,parent,name,*args,**kwargs): 204 | ttk.Frame.__init__(self,parent,*args,**kwargs) 205 | self.parent = parent 206 | self.parent.add(self, text=name) 207 | self.data = [[None,None,None,None,None,None,None] for _ in range(pref.n_connectors)] 208 | for n in range(pref.n_connectors): 209 | con = tk.LabelFrame(self,text="Conector {}".format(n+1)) 210 | con.grid(column=0, row=n*2, columnspan=5, pady="0",padx="2", rowspan=2,sticky="news") 211 | for b in range(2): 212 | label = tk.Label(con, text="From path" if b ==0 else "to path") 213 | label.grid(column=b*4+0, row=0, sticky="w") 214 | self.data[n][b*2] = tk.StringVar() 215 | menu = tk.OptionMenu(con,self.data[n][b*2],"",*pref.menu_e) 216 | menu.config(width="2") 217 | menu.grid(column=b*4+1, row=0,sticky = "e") 218 | label = tk.Label(con, text=", number" if b ==0 else ", number") 219 | label.grid(column=b * 4 + 2, row=0, sticky="w") 220 | self.data[n][b*2+1] = tk.StringVar() 221 | menu = tk.OptionMenu(con,self.data[n][b*2+1],"",*list(range(1,pref.n_structures+1))) 222 | menu.config(width="2") 223 | menu.grid(column=b*4+3, row=0,sticky = "e") 224 | label = tk.Label(con,text="Color/Width/Strike:") 225 | label.grid(column=0,row=1,columnspan=2) 226 | for a, b in zip([0, 1, 2], [pref.menu_a, pref.menu_b, pref.menu_c]): 227 | self.data[n][a+4] = tk.StringVar() 228 | self.color_menu = tk.OptionMenu(con, self.data[n][a+4], *b) 229 | self.data[n][a+4].set(b[0]) 230 | self.color_menu.config(width="12") 231 | self.color_menu.grid(column=2 + a*2,columnspan =2, row=1) 232 | class TabFrameComparers(ttk.Frame): 233 | def __init__(self,parent,name,*args,**kwargs): 234 | ttk.Frame.__init__(self,parent,*args,**kwargs) 235 | self.parent = parent 236 | self.parent.add(self, text=name) 237 | self.data = [[None for _ in range(10)] for _ in range(pref.n_connectors)] 238 | for n in range(pref.n_comparers): 239 | con = tk.LabelFrame(self,text="Comparer {}".format(n+1)) 240 | con.grid(column=0, row=n*2, columnspan=5, pady="0",padx="2", rowspan=2,sticky="news") 241 | for b in range(2): 242 | label = tk.Label(con, text="Path:" if b ==0 else ", vs. path:") 243 | label.grid(column=b*4+0, row=0, sticky="w") 244 | self.data[n][b*2] = tk.StringVar() 245 | menu = tk.OptionMenu(con,self.data[n][b*2],"",*pref.menu_e) 246 | menu.config(width="2") 247 | menu.grid(column=b*4+1, row=0,sticky = "e") 248 | label = tk.Label(con, text=", number" if b ==0 else ", number") 249 | label.grid(column=b * 4 + 2, row=0, sticky="w") 250 | self.data[n][b*2+1] = tk.StringVar() 251 | menu = tk.OptionMenu(con,self.data[n][b*2+1],"",*list(range(1,pref.n_structures+1))) 252 | menu.config(width="2") 253 | menu.grid(column=b*4+3, row=0,sticky = "e") 254 | label = tk.Label(con,text="Color/Width/Strike:") 255 | label.grid(column=0,row=1,columnspan=2) 256 | for a, b in zip([0, 1, 2], [pref.menu_a, ["1","1.5","2","3"], pref.menu_c]): 257 | self.data[n][a+4] = tk.StringVar() 258 | self.color_menu = tk.OptionMenu(con, self.data[n][a+4], *b) 259 | self.data[n][a+4].set(b[0]) 260 | self.color_menu.config(width="12") 261 | self.color_menu.grid(column=2 + a*2,columnspan =2, row=1) 262 | self.data[n][4].set("green") 263 | self.data[n][5].set("1.5") 264 | self.data[n][6].set("dashed3") 265 | label = tk.Label(con,text="Vertical/Horizontal/Label:") 266 | label.grid(column=0,row=2,columnspan=2) 267 | x = ["normal","reverse"] 268 | y = ["left","right","midle","p_left","xp_left","p_right","xp_right","average"] 269 | z = ["left","right","fliped_left","fliped_right"] 270 | for a, b in zip([0, 1, 2], [x, y, z]): 271 | self.data[n][a+4+3] = tk.StringVar() 272 | self.color_menu = tk.OptionMenu(con, self.data[n][a+4+3], *b) 273 | self.data[n][a+4+3].set(b[{0:0,1:6,2:0}[a]]) 274 | self.color_menu.config(width="12") 275 | self.color_menu.grid(column=2 + a*2,columnspan =2, row=2) 276 | 277 | class GeneralMenu(tk.LabelFrame): 278 | def __init__(self,parent,name,*args,**kwargs): 279 | ttk.LabelFrame.__init__(self,parent,text=name,*args,**kwargs) 280 | self.titles = ["Main Title", "ΔE in kcal·mol⁻¹", "Reaction coordinate"] 281 | self.main = [] 282 | self.span = [] 283 | self.style = [] 284 | self.horizontal = [] 285 | self.vertical = [] 286 | self.labels = [] 287 | self.plot = [] 288 | self.command = "" 289 | self.aesthetics = [] 290 | ###BUILD 291 | self._build_all() 292 | if hasattr(pref,"filename"): 293 | self.f = pref.filename 294 | self.load_state(getattr(pref,"filename")) 295 | def _build_all(self): 296 | self.note = ttk.Notebook(self.boxify("Advanced options", 2)) 297 | self.note.grid(column=0, row=0, sticky="news") 298 | self.note.grid_columnconfigure(0, weight=1) 299 | self._change_win_title("Unsaved") 300 | self._build_main_opt() 301 | self._build_span_opt() 302 | self._build_style_sel() 303 | self._buid_horizontal_sel() 304 | self._buid_vertical_sel() 305 | self._buid_labels_sel() 306 | self._build_plot_sel() 307 | 308 | self._build_titles(6) 309 | self._build_loadsave(7) 310 | if pref.trickster: self._build_generator(8) 311 | self._build_preview(9) 312 | self._build_message(10) 313 | 314 | def _build_main_opt(self): 315 | box = self.framefy("Main") 316 | options = ["Plot and use enthalpy instead of free energy","Use comma as decimal separator","Include complementary data (H or G values)"] 317 | for i,a in enumerate(options): 318 | self.main.append(tk.IntVar(value=0)) 319 | c1 = tk.Checkbutton(box, text=a, variable=self.main[i], onvalue=1, offvalue=0) 320 | c1.grid(column=0,row=i,sticky="w") 321 | for n in range(3): 322 | box.grid_rowconfigure(n, weight=1) 323 | def _build_span_opt(self): 324 | box = self.framefy("Span") 325 | box.grid_columnconfigure(1, weight=1) 326 | options = ["Atempt span calculation","Ignore structure type (TS/INT)","Use big indicator arrows"] 327 | for i,a in enumerate(options): 328 | self.span.append(tk.IntVar(value=0)) 329 | c1 = tk.Checkbutton(box, text=a, variable=self.span[i], onvalue=1, offvalue=0) 330 | c1.grid(column=0,row=i,columnspan=1,sticky="w") 331 | label = tk.Label(box, text="Input units:") 332 | label.grid(column=1, row=1,sticky="news") 333 | self.span.append(tk.StringVar()) 334 | options = ["kcal/mol","kJ/mol","eV"] 335 | menu = tk.OptionMenu(box, self.span[-1], *options) 336 | self.span[-1].set(options[0]) 337 | menu.config(width="8") 338 | menu.grid(column=3, row=1,sticky="w") 339 | label = tk.Label(box, text="Temperature (°C):") 340 | label.grid(column=1, row=2, columnspan = 2) 341 | self.span.append(tk.Entry(box, justify=tk.CENTER, bd=3, width=6)) 342 | self.span[-1].insert(0, "25") 343 | self.span[-1].grid(column=3, row=2, padx="3",pady="4", sticky="news") 344 | for n in range(3): 345 | box.grid_rowconfigure(n, weight=1) 346 | def _build_style_sel(self): 347 | options = list(pref.image.keys()) 348 | box = self.framefy("Graph Style") 349 | label = tk.Label(box, text=">-- Graph Style preview -->") 350 | label.grid(column=0, row=0, columnspan=2, sticky="news") 351 | 352 | self.preview = tk.Message(box, text=pref.image[options[0]], font="TkFixedFont", relief = tk.RIDGE) 353 | self.preview.grid(column=3, row=0, rowspan = 3, sticky="e") 354 | 355 | label = tk.Label(box, text="Graph Style:") 356 | label.grid(column=0, row=1, sticky="w") 357 | self.style.append(tk.StringVar()) 358 | self.style[-1].set(options[0]) 359 | 360 | update = lambda x: self.preview.configure(text=pref.image[x]) 361 | menu = tk.OptionMenu(box, self.style[-1], *options, command=update) 362 | menu.config(width="10") 363 | menu.grid(column=1, row=1,sticky="w") 364 | 365 | options = ["0","1","2"] 366 | label = tk.Label(box, text="Grid decimal places:") 367 | label.grid(column=0, row=2, sticky="w") 368 | self.style.append(tk.StringVar()) 369 | self.style[-1].set(options[1]) 370 | 371 | menu = tk.OptionMenu(box, self.style[-1], *options) 372 | menu.config(width="10") 373 | menu.grid(column=1, row=2, sticky="w") 374 | 375 | box.grid_columnconfigure(2, weight=1) 376 | for n in range(3): 377 | box.grid_rowconfigure(n, weight=1) 378 | def _buid_horizontal_sel(self): 379 | box = self.framefy("Horizontal") 380 | a = [2 * n for n in range(11)] 381 | b = [10 * n for n in range(11)] 382 | c = [5 * n + 60 for n in range(11)] 383 | options = [a,b,c] 384 | for i, x , y in zip(range(3), [["Plateau width:"], ["Starting offset (X):","Ending offset (X)"], ["Plateau spacing:"]], options ): 385 | for idx,z in enumerate(x): 386 | label = tk.Label(box, text=z) 387 | label.grid(column=idx*2, row=i, sticky="w") 388 | self.horizontal.append(tk.StringVar()) 389 | menu = tk.OptionMenu(box, self.horizontal[-1], *y) 390 | menu.config(width="8") 391 | menu.grid(column=1+idx*2, row=i, sticky="e") 392 | box.grid_columnconfigure(3, weight=1, minsize="50") 393 | self.horizontal[0].set(a[5]) 394 | self.horizontal[1].set(b[5]) 395 | self.horizontal[2].set(b[5]) 396 | self.horizontal[3].set(c[5]) 397 | def _buid_vertical_sel(self): 398 | box = self.framefy("Vertical") 399 | a = [5 * n for n in range(11)] 400 | b = [5 * n for n in range(11)] 401 | options = [a,b] 402 | for i, x , y in zip(range(2), ["Top offset (Y)", "Bottom offset (Y)"], options): 403 | label = tk.Label(box, text=x) 404 | label.grid(column=0, row=i, sticky="w") 405 | self.vertical.append(tk.StringVar()) 406 | menu = tk.OptionMenu(box, self.vertical[i], *y) 407 | menu.config(width="8") 408 | menu.grid(column=1, row=i, sticky="e") 409 | box.grid_columnconfigure(3, weight=1, minsize="150") 410 | self.vertical[0].set(a[2]) 411 | self.vertical[1].set(a[0]) 412 | def _buid_labels_sel(self): 413 | box = self.framefy("Labels") 414 | a = [" ", "( )", "[ ]", r"{ }", '" "', "' '"] 415 | for i,x in enumerate(["G:", "H:"]): 416 | label = tk.Label(box, text=x) 417 | label.grid(column=2 * i, row=0,sticky="w") 418 | self.labels.append(tk.StringVar()) 419 | menu = tk.OptionMenu(box, self.labels[i], *a) 420 | menu.config(width="8") 421 | menu.grid(column=i * 2 + 1, row=0, sticky="e") 422 | self.labels[0].set(a[0]) 423 | self.labels[1].set(a[2]) 424 | 425 | #Decimal 426 | label = tk.Label(box, text="Decimal places:") 427 | label.grid(column=0, row=1,columnspan=2,sticky="w") 428 | self.labels.append(tk.StringVar()) 429 | menu = tk.OptionMenu(box, self.labels[-1], *["0","1","2"]) 430 | menu.config(width="8") 431 | menu.grid(column=2, row=1,columnspan=2,sticky="e") 432 | self.labels[-1].set("1") 433 | 434 | #TS MARK 435 | label = tk.Label(box, text="TS Mark:") 436 | label.grid(column=0, row=2,columnspan=2,sticky="w") 437 | self.labels.append(tk.StringVar()) 438 | menu = tk.OptionMenu(box, self.labels[-1], *[" ", "‡ (big)", "‡ (small)"]) 439 | menu.config(width="8") 440 | menu.grid(column=2, row=2,columnspan=2,sticky="e") 441 | box.grid_columnconfigure(5, weight=1,minsize="180") 442 | self.labels[-1].set(" ") 443 | 444 | #ADJUST GRID 445 | for n in range(3): 446 | box.grid_rowconfigure(n, weight=1) 447 | for n in range(4): 448 | box.grid_columnconfigure(n, weight=1) 449 | 450 | def _build_aesthetics(self): 451 | box = self.framefy("Aesthetics") 452 | a = [" ", "( )", "[ ]", r"{ }", '" "', "' '"] 453 | b = [a * 2 for a in range(11)] 454 | c = [1,2] 455 | e = [a,a,b,c] 456 | f = ["G:","H:","Width:","Decimal"] 457 | for a,(b,c) in enumerate(zip(f,e)): 458 | label = tk.Label(box,text=b) 459 | label.grid(column=2*a,row=0) 460 | self.aesthetics.append(tk.StringVar()) 461 | menu = tk.OptionMenu(box, self.aesthetics[a], *c) 462 | menu.config(width="2") 463 | menu.grid(column=a * 2 + 1, row=0) 464 | for i,a in enumerate([" ","( )",10,1]): 465 | self.aesthetics[i].set(a) 466 | a = [0 + a * 10 for a in range(11)] 467 | b = [60 + a * 5 for a in range(11)] 468 | c = [" ","‡ (big)","‡ (small)"] 469 | d = ["X offset:","X dist:","TS mark:"] 470 | e = [a,b,c] 471 | for a, (b, c) in enumerate(zip(d, e)): 472 | label = tk.Label(box, text=b) 473 | label.grid(column=0 if a ==0 else 2 * a +1, row=1, columnspan = 2 if a ==0 else 1) 474 | self.aesthetics.append(tk.StringVar()) 475 | menu = tk.OptionMenu(box, self.aesthetics[a+4], *c) 476 | menu.config(width="8" if a == 2 else "2") 477 | menu.grid(column=a * 2 + 2, row=1, columnspan = 3 if a == 2 else 1, sticky="news" if a==2 else "") 478 | for i, a in enumerate([40, 80, " "]): 479 | self.aesthetics[i+4].set(a) 480 | def _build_plot_sel(self): 481 | box = self.framefy("Plot") 482 | for i,a in enumerate(pref.menu_h): 483 | self.plot.append(tk.IntVar(value=1)) 484 | c1 = tk.Checkbutton(box, text=a, variable=self.plot[i], onvalue=1, offvalue=0) 485 | c1.grid(column=i%4,row=i//4, sticky="w") 486 | n = len(pref.menu_h) 487 | self.plot.append(tk.IntVar(value=1)) 488 | c1 = tk.Checkbutton(box, text="Comparers", variable=self.plot[n], onvalue=1, offvalue=0) 489 | c1.grid(column=0,row=((n-1)//4)+1,columnspan = 2, sticky="w") 490 | self.plot.append(tk.IntVar(value=1)) 491 | c1 = tk.Checkbutton(box, text="Connections", variable=self.plot[n+1], onvalue=1, offvalue=0) 492 | c1.grid(column=2,row=((n-1)//4)+1,columnspan = 2, sticky="w") 493 | for n in range(4): 494 | box.grid_columnconfigure(n, weight=1) 495 | for n in range(3): 496 | box.grid_rowconfigure(n, weight=1) 497 | def _build_titles(self,idx): 498 | box = self.boxify("Titles",idx) 499 | for a,b,c in zip([0,1,2],self.titles,["Main:","y:","x:"]): 500 | label = tk.Label(box, text=c,width=10) 501 | label.grid(column=0, row=a) 502 | self.titles[a] = tk.Entry(box, justify=tk.CENTER, bd=2, width=50) 503 | self.titles[a].insert(0, b) 504 | self.titles[a].grid(column=1, row=a, padx="0", sticky="news") 505 | def _build_loadsave(self,idx): 506 | box = self.boxify("Close, Load & Save Data", idx) 507 | label = tk.Label(box, text="Path's and connection's info") 508 | label.grid(column=0,row=0,sticky="w") 509 | button = tk.Button(box, text="Close", command=self._blank_state, padx="1") 510 | button.config(width=7) 511 | button.grid(column=1, row=0, sticky="e") 512 | button = tk.Button(box, text="Load", command=self.load_state, padx="1") 513 | button.config(width=7) 514 | button.grid(column=2,row=0,sticky="e") 515 | button = tk.Button(box, text="Save as", command=self._save_as, padx="1") 516 | button.config(width=7) 517 | button.grid(column=3,row=0,sticky="e") 518 | button = tk.Button(box, text="Save", command=self._save, padx="1") 519 | button.config(width=7) 520 | button.grid(column=4,row=0,sticky="e") 521 | box.grid_columnconfigure(0, weight=1) 522 | def _build_generator(self,idx): 523 | box = self.boxify("Generate random PES", idx) 524 | label = tk.Label(box, text="Random PES generator") 525 | label.pack(side=tk.LEFT) 526 | button = tk.Button(box, text="Fill in data", command=self._ask_confirmation, padx="1") 527 | button.config(width=10) 528 | button.pack(side=tk.RIGHT) 529 | def _build_preview(self,idx): 530 | box = self.boxify("Preview with either command or default manager, or save svg file", idx) 531 | self.command = tk.Entry(box, justify=tk.CENTER, bd=4) 532 | self.command.insert(0, pref.command_line) 533 | self.command.grid(column=0,row=0,sticky="news") 534 | button = tk.Button(box, text="Command", command=self.run_data_a, padx="1") 535 | button.config(width=8) 536 | button.grid(column=1,row=0,sticky="e") 537 | button = tk.Button(box, text="Default", command=self.run_data_b, padx="1") 538 | button.config(width=8) 539 | button.grid(column=2,row=0,sticky="e") 540 | button = tk.Button(box, text="Save svg", command=self.return_svg, padx="1") 541 | button.config(width=8) 542 | button.grid(column=3,row=0,sticky="e") 543 | box.grid_columnconfigure(0, weight=1) 544 | def _build_message(self,idx): 545 | box = self.boxify("Message",idx) 546 | m = tk.Message(box) 547 | scrollbar = tk.Scrollbar(box) 548 | scrollbar.grid(column=1,row=0,sticky="nes") 549 | self.msg = tk.Text(box, 550 | width=40, 551 | yscrollcommand=scrollbar.set, 552 | height=12 if pref.trickster else 18, 553 | state="disabled", 554 | background=m.cget("background"), 555 | relief="flat", 556 | wrap=tk.WORD, 557 | font=('TkFixedFont',8)) 558 | m.destroy() 559 | self.msg.grid(column=0,row=0,sticky="news") 560 | if not pref.xlsx: 561 | self.message("Welcome!\n\nTo enable .xlsx file support, please install openpyxl python library via the shell command:\npython3 -m pip install openpyxl") 562 | else: 563 | self.message("Welcome!\n\n{}".format(random.choice(pref.hints))) 564 | box.grid_rowconfigure(0, weight=1) 565 | box.grid_columnconfigure(0, weight=1) 566 | self.grid_rowconfigure(idx, weight=1) 567 | def _save(self): 568 | try: 569 | if self.f.endswith(".xlsx") and pref.xlsx: 570 | from openpyxl import Workbook 571 | wb = Workbook() 572 | wb.remove(wb.active) 573 | for a,b in zip(self.gen_data(type=".xlsx"),pref.menu_e): 574 | sheet = wb.create_sheet(title=f'Path {b}') 575 | for i,c in enumerate(a,start=1): 576 | sheet.append(c[1:4]) 577 | try: 578 | wb.save(self.f) 579 | except PermissionError: 580 | self.message(f"Error while saving file!\nIs the file:\n'{self.f}' already open?") 581 | else: 582 | try: 583 | with open(self.f,"w") as out: 584 | if self.f.endswith(".txt"): 585 | txt = "\n".join(a for a in self.gen_data(type=".txt") if len(a.split()) >= 1) 586 | out.write(txt) 587 | except PermissionError: 588 | self.message(f"Error while saving file!\nIs the file:\n'{self.f}' already open?") 589 | except AttributeError: self._save_as() 590 | except FileNotFoundError: self._save_as() 591 | def _save_as(self): 592 | self.f = tk.filedialog.asksaveasfilename(**pref.allowed_extensions) 593 | self._change_win_title(self.f) 594 | if any(self.f.endswith(a) for a in (".sff",".txt",".xlsx")):self._save() 595 | 596 | def load_state(self,file_n=None): 597 | if file_n is None: 598 | file_n = tk.filedialog.askopenfilename(**pref.allowed_extensions) 599 | try: 600 | if file_n.endswith(".xlsx") and pref.xlsx: 601 | self._blank_state(ask=False) 602 | import openpyxl 603 | try: 604 | wb = openpyxl.load_workbook(file_n) 605 | except: 606 | self.message(f"Could not read {file_n} as xlsx file!\nAre you sure this is a proper xlsx file?") 607 | return 608 | notes = [getattr(note, a) for a in pref.menu_g] 609 | exceeded = False 610 | for a,b in zip(wb.sheetnames, notes): 611 | sheet = wb[a] 612 | for n in range(1,pref.n_structures+10): 613 | if n > pref.n_structures: 614 | if any(sheet.cell(row=n,column=i).value is None for i in range(1,4)): 615 | exceeded = True 616 | continue 617 | for i in range(1,4): 618 | if sheet.cell(row=n,column=i).value is None: continue 619 | b.data[n-1][i].insert(0,str(sheet.cell(row=n,column=i).value)) 620 | if exceeded: 621 | self.message("Exceeding number of structures") 622 | elif file_n.endswith(".txt"): 623 | with open(file_n, mode="r") as file: 624 | self._blank_state(ask=False) 625 | all_tabs = {} 626 | tab_data = [] 627 | for line in file.read().splitlines(): 628 | line = line.split() 629 | non_hash = True if len(line) == 1 and not line[0].startswith("#") else False 630 | if len(line) >= 2 or non_hash: 631 | tab_data.append(line) 632 | elif len(line) == 1 and any(line[0] == f"#{a}" for a in pref.menu_e): 633 | all_tabs[line[0]] = tab_data 634 | tab_data = [] 635 | if len(all_tabs) == 0 and len(tab_data) != 0: 636 | all_tabs["#A"] = tab_data 637 | missing = [b for b in [f"#{a}" for a in pref.menu_e] if b not in all_tabs.keys()] 638 | for a in missing: all_tabs[a] = [] 639 | notes = [getattr(note, a) for a in pref.menu_g] 640 | exceeded = False 641 | for a,b in zip(notes,sorted(all_tabs.keys())): 642 | for i,c in enumerate(all_tabs[b]): 643 | if i >= pref.n_structures: 644 | exceeded = True 645 | continue 646 | for n in range(3): 647 | try: a.data[i][n+1].insert(0,c[n]) 648 | except IndexError: pass 649 | if exceeded: 650 | self.message("Exceeding number of structures") 651 | else: 652 | self.message(f"Unrecognized file {file_n}") 653 | except FileNotFoundError: 654 | self.message("File not found!") 655 | return 656 | finally: 657 | self._change_win_title(file_n) 658 | self.f = file_n 659 | 660 | def fill_in(self): 661 | size = random.random()+0.5 662 | max_value = min(len(pref.alphabet), pref.n_structures) 663 | lenght = random.randint(5,12) 664 | tab = getattr(note,[a for a in pref.menu_g][note.index(note.select())]) 665 | for i,n in zip(range(max_value),pref.alphabet): 666 | value = size*random.randrange(-20,20)-i*size*2 667 | for idx in range(len(tab.data[i])): 668 | if idx == 1: 669 | tab.data[i][idx].delete(0, tk.END) 670 | if i+1 < lenght: tab.data[i][idx].insert(0, n) 671 | elif i + 1 == lenght: tab.data[i][idx].insert(0, "A'") 672 | elif idx == 2: 673 | tab.data[i][idx].delete(0, tk.END) 674 | if i < lenght: tab.data[i][idx].insert(0,"{:.2f}".format(value)) 675 | elif idx == 3: 676 | tab.data[i][idx].delete(0, tk.END) 677 | if i < lenght: tab.data[i][idx].insert(0,"{:.2f}".format(value + random.choice([-random.random(), +random.random()]))) 678 | elif idx == 4: 679 | tab.data[i][idx].set(pref.menu_d[1]) 680 | max_v, min_v = None, None 681 | for i in range(max_value): 682 | if max_v is None: max_v = [i,tab.data[i][2].get()] 683 | if min_v is None: min_v = [i,tab.data[i][2].get()] 684 | if i < lenght and float(tab.data[i][2].get()) > float(max_v[1]): max_v = [i, tab.data[i][2].get()] 685 | if i < lenght and float(tab.data[i][2].get()) < float(min_v[1]) : min_v = [i, tab.data[i][2].get()] 686 | if i == 0: tab.data[i][0].set("INT") 687 | elif i+1 == lenght: tab.data[i][0].set("INT") 688 | elif i >= lenght: tab.data[i][0].set(" ") 689 | else: 690 | if float(tab.data[i-1][2].get()) < float(tab.data[i][2].get()) > float(tab.data[i+1][2].get()): 691 | tab.data[i][0].set("TS") 692 | else: 693 | tab.data[i][0].set("INT") 694 | def _ask_confirmation(self): 695 | if note.index(tk.END) -2 <= note.index(note.select()): 696 | self.message("Cannot fill in data for connection and comparer tabs!\n") 697 | return 698 | msgbox = tk.messagebox.askquestion( 699 | f'Fill in random PES cycle at {pref.menu_h[note.index(note.select())]}', 700 | 'Are you sure? All unsaved data will be lost!', icon='warning') 701 | if msgbox == "yes": 702 | self.fill_in() 703 | self._change_win_title("Unsaved") 704 | if hasattr(self,"f"): del(self.f) 705 | def _change_win_title(self,path): 706 | window.title(f"{pref.name} @ {path}") 707 | def _blank_state(self,ask=True): 708 | if ask: 709 | msgbox = tk.messagebox.askquestion('Close document', 'Are you sure? All unsaved data will be lost!', icon='warning') 710 | if msgbox != "yes":return 711 | self._change_win_title("Unsaved") 712 | if hasattr(self,"f"): del(self.f) 713 | for a in [getattr(note,a) for a in pref.menu_g]: 714 | for i in range(pref.n_structures): 715 | for idx in range(5): 716 | if idx == 0: a.data[i][idx].set(pref.menu_z[0]) 717 | if idx in [1,2,3]: a.data[i][idx].delete(0, tk.END) 718 | if idx == 4: a.data[i][idx].set(pref.menu_d[1]) 719 | for a in [getattr(note,a) for a in pref.menu_g]: 720 | for n in range(2): 721 | for idx, b in zip([0, 1, 2], [pref.menu_a, pref.menu_b, pref.menu_c]): 722 | a.option_menu.line_opt_data[n][idx].set(b[[1,1,0][idx] if n == 0 else [1,0,2][idx]]) 723 | for a in range(pref.n_connectors): 724 | for b in range(4): 725 | note.tab_connections.data[a][b].set("") 726 | for b,c in zip(range(3),[pref.menu_a, pref.menu_b, pref.menu_c]): 727 | note.tab_connections.data[a][b+4].set(c[0]) 728 | def message(self,text): 729 | now = datetime.datetime.now() 730 | self.msg.configure(state="normal") 731 | self.msg.tag_add("start", "0.0", tk.END) 732 | self.msg.tag_config("start", foreground="grey") 733 | if type(text) == str: text = [text] 734 | for txt in text: 735 | self.msg.insert("1.0",txt+"\n") 736 | self.msg.insert("1.0", "[" + ":".join(["{:02d}".format(a) for a in [now.hour, now.minute, now.second]]) + "] "+"\n") 737 | self.msg.configure(state="disabled") 738 | def boxify(self,name,row): 739 | box = ttk.LabelFrame(self, text=name) 740 | box.grid(column=0, row=row, sticky="news") 741 | box.grid_columnconfigure(0, weight=1) 742 | return box 743 | def framefy(self,name): 744 | x = tk.Frame() 745 | x.grid(column=0, row=0, sticky="news") 746 | x.grid_columnconfigure(0, weight=1) 747 | self.note.add(x,text=name) 748 | return x 749 | def print_data(self): 750 | notes = [getattr(note,a) for a in pref.menu_g] 751 | for a,b in zip(notes,pref.menu_e): 752 | print(f"NOTE {b}") 753 | for idx,line in enumerate(getattr(a,"data")): 754 | if any(c.get().strip() != "" for c in line[:-1]): 755 | print(f"#{idx+1}",[n.get() for n in line]) 756 | print("NOTE CONNECTIONS") 757 | for idx,a in enumerate(note.tab_connections.data): 758 | if any(c.get().strip() != "" for c in a[:-3]): 759 | print(f"#{idx+1}", [n.get() for n in a]) 760 | def gen_data(self,type): 761 | notes = [getattr(note,a) for a in pref.menu_g] 762 | txt_data = [] 763 | xlsx_data = [] 764 | for a,b in zip(notes,pref.menu_e): 765 | xlsx = [] 766 | for idx,line in enumerate(getattr(a,"data")): 767 | c = [n.get() for n in line] 768 | txt_data.append("{:<20} {:>10} {:>10}".format(*c[1:4])) 769 | xlsx.append(c) 770 | txt_data.append("#{}".format(b)) 771 | xlsx_data.append(xlsx) 772 | if type == ".txt": 773 | return txt_data 774 | elif type == ".xlsx": 775 | return xlsx_data 776 | def save_svg_as(self): 777 | return tk.filedialog.asksaveasfilename(defaultextension=".svg", title="Save svg", filetypes=[("Scalable Vector Graphics", ".svg")]) 778 | def run_data_a(self): 779 | self.return_svg(promp=False); os.system(self.command.get()) 780 | def run_data_b(self): 781 | self.return_svg(promp=False) 782 | filename = os.path.join(os.getcwd(), ".E_profile.svg") 783 | if sys.platform == "win32" or os.name == "nt": 784 | os.startfile(filename) 785 | else: 786 | opener = "open" if sys.platform == "darwin" else "xdg-open" 787 | subprocess.call([opener, filename]) 788 | 789 | def return_svg(self,promp=True): 790 | svg_name = None if promp == False else self.save_svg_as() 791 | msg = SvgGenEsp(self) 792 | msg = msg.save_svg(svg_name) 793 | if not msg is None: self.message(msg) 794 | 795 | class SvgGenEsp: 796 | def __init__(self,options): 797 | self.options = options # Please only use this at __init__ 798 | 799 | #MAIN 800 | m_options = ["energy", "comma", "plot"] 801 | self.main = {a: b.get() for a, b in zip(m_options, getattr(self.options,"main"))} 802 | self.e_source = 4 if getattr(self.options,"main")[0].get() == 1 else 3 803 | self.e_complement = 3 if self.e_source == 4 else 4 804 | self.comma = True if self.main["comma"] == 1 else False 805 | self.plot_np = True if self.main["plot"] == 1 else False 806 | 807 | #SPAN 808 | s_options = ["span","irrespective","big_arrow","units","temperature"] 809 | self.span = {a:b.get() for a,b in zip(s_options,getattr(self.options,"span"))} 810 | self.span_worthy = True 811 | self.span_request = True if self.span["span"] == 1 else False 812 | self.big_arrow = True if self.span["big_arrow"] == 1 else False 813 | 814 | #GRAPHIC STYLE 815 | self.frame = getattr(self.options,"style")[0].get() 816 | self.grid_decimal = getattr(self.options, "style")[1].get() 817 | 818 | #HORIZONTAL 819 | self.wide = [int(getattr(self.options,"horizontal")[0].get()) * a + b for a,b in zip([-1,1],[20,40])] 820 | self.x_start_offset = int(getattr(self.options,"horizontal")[1].get()) 821 | self.x_end_offset = int(getattr(self.options, "horizontal")[2].get()) 822 | self.x_space = int(getattr(self.options,"horizontal")[3].get()) 823 | 824 | #VERTICAL 825 | self.top_height = int(getattr(self.options,"vertical")[0].get()) 826 | self.bottom_height = 400 - int(getattr(self.options,"vertical")[1].get()) 827 | 828 | #LABELS 829 | self.g_h_labels = {a: getattr(self.options,"labels")[b].get() for a,b in zip(["g","h"],[0,1])} 830 | self.e_decimal = getattr(self.options,"labels")[2].get() 831 | self.ts_mark = getattr(self.options,"labels")[3].get() 832 | 833 | #PLOT 834 | self.plot = [a.get() for a in getattr(self.options, "plot")] 835 | self.plot_path = {a:bool(b) for a,b in zip(pref.menu_e,self.plot)} 836 | 837 | #TITLE 838 | self.main_title = getattr(self.options,"titles")[0].get() 839 | self.y_title = getattr(self.options,"titles")[1].get() 840 | self.x_title = getattr(self.options, "titles")[2].get() 841 | 842 | # RETURN 843 | self.svg_code = [''] 844 | self.msg = [] 845 | 846 | if self.span_request: 847 | self.temperature = self._verify_temp(self.span["temperature"]) 848 | 849 | # CONECTORS 850 | self.conectors = [[b.get() for b in a] for a in note.tab_connections.data] 851 | # COMPARERS 852 | x = ("A","1","B","2","S1","S2","S3","S4","S5","S6") 853 | self.comparers = [{l:note.tab_comparers.data[n][i].get() for i,l in zip(range(10),x)} for n in range(pref.n_comparers)] 854 | 855 | 856 | 857 | # DATA OPTIONS 858 | fc = lambda a: getattr(note, "tab_{}".format(a.lower())).option_menu.line_opt_data 859 | self.path_options = {a: [[c.get() for c in b] for b in fc(a)] for a in pref.menu_e} 860 | # DATA 861 | dt = lambda a: enumerate(getattr(note,a).data) 862 | fa = lambda idx,c: float(c.get().replace(",",".")) if idx == self.e_source-1 else c.get() 863 | fb = lambda b: is_str_float(b[self.e_source-1].get().replace(",",".")) 864 | self.raw_crt = [[[i+1,*[fa(idx,c) for idx,c in enumerate(b)]] for i,b in dt(a) if fb(b)] for a in pref.menu_g] 865 | self.raw_crt_dict = {a: b for a, b in zip(pref.menu_e, self.raw_crt) if self.plot_path[a] and b} 866 | self.paths = self.set_height() 867 | self.data_dict = {a: b for a, b in zip(pref.menu_e, self.paths)} 868 | self.plot_dict = {a:b for a,b in self.data_dict.items() if self.plot_path[a] and b} 869 | 870 | def _verify_temp(self,value): 871 | if is_str_float(value.replace(",",".")): 872 | if float(value) <= -273.15: 873 | self.span_worthy = False 874 | text = "Temperature should not be smaller than or equal to {} °C\n" 875 | self.msg.append(text.format(self.commafy("-273.15"))) 876 | return None 877 | else: 878 | t = float(value) + 273.15 879 | text = "Temperature is set to {} K\n" 880 | self.msg.append(text.format(self.commafy("{:.2f}".format(t)))) 881 | return t 882 | else: 883 | self.span_worthy = False 884 | text = "Unrecognized temperature: {}\n" 885 | self.msg.append(text.format(str(value))) 886 | def commafy(self,item): 887 | return str(item).replace(".", ",") if self.comma else str(item).replace(",", ".") 888 | @functools.lru_cache(maxsize=1) 889 | def max_value(self): 890 | return max(max(a[self.e_source] for a in path) for path in list(self.raw_crt_dict.values()) if path) 891 | @functools.lru_cache(maxsize=1) 892 | def min_value(self): 893 | return min(min(a[self.e_source] for a in path) for path in list(self.raw_crt_dict.values()) if path) 894 | @functools.lru_cache(maxsize=1) 895 | def delta_value(self): 896 | return self.max_value()-self.min_value() 897 | @functools.lru_cache(maxsize=1) 898 | def n_col(self): 899 | try: x = max(max(a[0] for a in path) for path in list(self.plot_dict.values()) if path) 900 | except ValueError: x = 0 901 | return x 902 | @functools.lru_cache(maxsize=1) 903 | def set_height(self): 904 | paths = [] 905 | for idx_a, a in enumerate(self.raw_crt): 906 | path = [] 907 | for idx_b, b in enumerate(a): # For every structure 908 | try: 909 | height = 1 - (b[self.e_source] - self.min_value())/ self.delta_value() 910 | height = int(round(abs(height*(self.bottom_height - self.top_height) + self.top_height))) 911 | except ZeroDivisionError: height = int(self.top_height/2 + self.bottom_height/2) 912 | path.append([*b,height]) 913 | paths.append(path) 914 | return paths 915 | def set_single_height(self,value): 916 | try: 917 | height = 1 - (value - self.min_value()) / self.delta_value() 918 | height = int(height * (self.bottom_height - self.top_height) + self.top_height +50) 919 | except ZeroDivisionError: 920 | height = int(self.top_height / 2 + self.bottom_height / 2) 921 | return height 922 | def set_horizontal(self,mult,add): 923 | value = (float(mult)-1)*int(self.x_space) 924 | value += int(self.x_start_offset) + int(add) + 80 925 | return int(value) 926 | def char_care(self,text): 927 | return str(text).encode("ascii", "xmlcharrefreplace").decode("utf-8") 928 | def is_avail(self,name,path_a,num_a,path_b,num_b,ignore_same=False): 929 | try: 930 | first = int(num_a) in [a[0] for a in self.data_dict[path_a]] 931 | second = int(num_b) in [a[0] for a in self.data_dict[path_b]] 932 | if int(num_a) == int(num_b) and path_a == path_b: 933 | text = f"{name}: Cannot conect items on same column and path" 934 | self.msg.append(text) 935 | return False 936 | elif int(num_a) == int(num_b) and not ignore_same: 937 | text = f"{name}: Cannot conect items on same column" 938 | self.msg.append(text) 939 | return False 940 | elif not self.plot_path[path_a]: 941 | return False 942 | elif not self.plot_path[path_b]: 943 | return False 944 | return first and second 945 | except KeyError: 946 | return False 947 | except ValueError: 948 | return False 949 | 950 | @functools.lru_cache(maxsize=1) 951 | def graph_frame(self): 952 | horizontal_end = self.set_horizontal(self.n_col(), self.x_end_offset) 953 | a = [ 954 | '', #0 FRAME 955 | ' ', #1 FULL VERTICAL LEFT LINE 956 | ' ', #2 FULL BOTTOM HORIZONTAL LINE 957 | ' {}', #3 MAIN TITLE 958 | ' {}', #4 Y AXIS TITLE 959 | ' {}', #5 X AXIS TITLE 960 | ' ', #6 TOP LEFT Y ARROW 961 | ' ', #7 BOTTOM RIGHT X ARROW 962 | ' ', #8 BOTTOM RIGHT Y ARROW 963 | ' ', #9 PARTIAL BOTTOM HORIZONTAL LINE 964 | ' ',# 10 BOTTOM LEFT X ARROW (PARTIAL) 965 | ' ',# 11 BOTTOM LEFT X ARROW (FULL) 966 | ' ',# 12 FULL VERTICAL RIGHT LINE 967 | ' ' #13 FULL TOP HORIZONTAL LINE 968 | ] 969 | a[0] = a[0].format(horizontal_end + 75) 970 | a[2] = a[2].format(horizontal_end + 55) 971 | a[3] = a[3].format(self.set_horizontal(self.n_col()/2,55), self.char_care(self.main_title)) 972 | a[4] = a[4].format('transform="rotate(-90)"', self.char_care(self.y_title)) 973 | a[5] = a[5].format(self.set_horizontal(self.n_col()/2,55), self.char_care(self.x_title)) 974 | a[6] = a[6] 975 | a[7] = a[7].format(horizontal_end + 65,horizontal_end + 55) 976 | a[8] = a[8] 977 | a[9] = a[9].format(horizontal_end + 55) 978 | a[10] = a[10] 979 | a[11] = a[11] 980 | a[12] = a[12].format(horizontal_end + 55) 981 | a[13] = a[13].format(horizontal_end + 55) 982 | 983 | code = {"Borderless":[a[n] for n in (0,3)], 984 | "Y only":[a[n] for n in (0,1,3,4,6,8)], 985 | "XY":[a[n] for n in (0,1,2,3,4,5,6,7)], 986 | "X only":[a[n] for n in (0,2,5,7,11)], 987 | "Frame":[a[n] for n in (0,1,2,3,4,5,12,13)], 988 | "X,Y":[a[n] for n in (0,1,3,4,5,6,7,8,9,10)] 989 | } 990 | 991 | self.svg_code.extend(code[self.frame]) 992 | 993 | @functools.lru_cache(maxsize=1) 994 | def graph_grid(self): 995 | if self.frame == "Borderless" or self.frame == "X only": return 996 | if self.delta_value() == 0: return 997 | step_size = float("{:.0E}".format(abs(self.delta_value())/10)) 998 | max_e = float("{:.0E}".format(abs(2*self.delta_value()+self.max_value()))) 999 | if step_size == 0: return 1000 | steps = [max_e-step_size*n for n in range(60)] 1001 | for item in steps: 1002 | value = int(self.set_single_height(item)) 1003 | if 50 < value < 465: 1004 | item = { 1005 | "0":"{:.0f}".format(item), 1006 | "1":"{:.1f}".format(item), 1007 | "2":"{:.02f}".format(item) 1008 | }[self.grid_decimal] 1009 | item = self.commafy(item) 1010 | c = [ 1011 | ' ', 1012 | ' {}'] 1013 | c[0] = c[0].format(value) 1014 | c[1] = c[1].format(value,item) 1015 | self.svg_code.extend(c) 1016 | 1017 | @functools.lru_cache(maxsize=1) 1018 | def graph_crt_points(self): 1019 | for key,value in self.data_dict.items(): 1020 | if not self.plot_path[key]: continue 1021 | opt_cri = self.path_options[key][0] 1022 | opt_con = self.path_options[key][1] 1023 | if not len(value) == len(set([a[0] for a in value])): 1024 | text = "WARNING: Two or more structures are occupying the same block lane!" 1025 | self.msg.append(text) 1026 | l_c = [0, 0, 0] # last collumn 1027 | for idx, item in enumerate(value): 1028 | c_p = [self.set_horizontal(item[0], self.wide[0]), 1029 | int(round(item[-1] + 50)), 1030 | self.set_horizontal(item[0], self.wide[1])] 1031 | # [x1,y1,x2], y1=y2 1032 | a = [ 1033 | ' ', 1034 | ' {}{}', 1035 | ' {}', 1036 | ' {}'] 1037 | x = pref.svg_repl[opt_cri[-1]] 1038 | z = pref.placement[item[-2]][0 if self.plot_np else 1] 1039 | trick_g = "g" if self.e_source == 3 else "h" 1040 | trick_h = "h" if self.e_source == 3 else "g" 1041 | item_rep = lambda x: float(item[x].replace(",",".") if type(item[x]) is str else item[x]) 1042 | digit_rounding = lambda x: {"0": "{:.0f}".format(item_rep(x)),"1": "{:.1f}".format(item_rep(x)),"2": "{:.2f}".format(item_rep(x))}[self.e_decimal] 1043 | 1044 | g = self.g_h_labels[trick_g][0] + self.commafy(digit_rounding(self.e_source)) + self.g_h_labels[trick_g][-1] 1045 | h = self.g_h_labels[trick_h][0] + self.commafy(digit_rounding(self.e_complement) if is_str_float(item[self.e_complement].replace(",",".")) else item[self.e_complement]) + self.g_h_labels[trick_h][-1] 1046 | ts_dict = { 1047 | " " : "", 1048 | "‡ (big)":'{}'.format(self.char_care("‡")), 1049 | "‡ (small)":'{}'.format(self.char_care("‡")) 1050 | } 1051 | ts_mark = ts_dict[self.ts_mark] if item[1] == "TS" else "" 1052 | a[0] = a[0].format(c_p[0], c_p[1], c_p[2], c_p[1], opt_cri[0],opt_cri[1],x) 1053 | a[1] = a[1].format(int((c_p[0] + c_p[2])/2), c_p[1] + z[0], opt_cri[0],self.char_care(item[2]),ts_mark) 1054 | a[2] = a[2].format(int((c_p[0] + c_p[2])/2), c_p[1] + z[1],opt_cri[0],self.char_care(g)) 1055 | a[3] = a[3].format(int((c_p[0] + c_p[2])/2), c_p[1] + z[2],opt_cri[0],self.char_care(h)) 1056 | self.svg_code.extend(a if self.plot_np else a[:-1]) 1057 | if not idx == 0: 1058 | b = ' ' 1059 | x = pref.svg_repl[opt_con[-1]] 1060 | b = b.format(l_c[2], l_c[1], c_p[0], c_p[1], opt_con[0],opt_con[1],x) 1061 | self.svg_code.append(b) 1062 | l_c = c_p 1063 | @functools.lru_cache(maxsize=1) 1064 | def graph_connectors(self): 1065 | if not self.plot[-1] == 1: return 1066 | for idx,i in enumerate(self.conectors): 1067 | if not self.is_avail(f"Connector {idx+1}",*i[0:4]):continue 1068 | i[1] = int(i[1]) 1069 | i[3] = int(i[3]) 1070 | start = next(n for n in self.data_dict[i[0]] if n[0] == i[1]) 1071 | end = next(n for n in self.data_dict[i[2]] if n[0] == i[3]) 1072 | con = [start,end] 1073 | if con[0][0] > con[1][0]: con.reverse() 1074 | if con[0][0] < con[1][0]: 1075 | x = pref.svg_repl[i[6]] 1076 | a = ' ' 1077 | a = a.format(self.set_horizontal(con[0][0], self.wide[1]), 1078 | con[0][-1] + 50, 1079 | self.set_horizontal(con[1][0], self.wide[0]), 1080 | con[1][-1] + 50, i[4], i[5],x) 1081 | self.svg_code.append(a) 1082 | @functools.lru_cache(maxsize=1) 1083 | def graph_comparers(self): 1084 | if not self.plot[-2] == 1: return 1085 | for i,a in enumerate(self.comparers): 1086 | if not self.is_avail(f"Comparer {i+1}",a["A"],a["1"],a["B"],a["2"],ignore_same=True): continue 1087 | start = next(n for n in self.data_dict[a["A"]] if n[0] == int(a["1"])) 1088 | end = next(n for n in self.data_dict[a["B"]] if n[0] == int(a["2"])) 1089 | com = [start,end] 1090 | ordered = com[0][0] < com[1][0] 1091 | if not ordered: 1092 | com = [end,start] 1093 | if a["S4"] == "reverse": 1094 | com.reverse() 1095 | y1 = com[0][6] + 50 1096 | y2 = com[1][6] + 50 1097 | color = a["S1"] 1098 | width = a["S2"] 1099 | dashed = pref.svg_repl[a["S3"]] 1100 | excess_y = 8 if com[0][-1] < com[1][-1] else -8 1101 | excess_yb = 2 if com[0][-1] < com[1][-1] else -2 1102 | text_pos = { 1103 | "left":[-5,"-90"], 1104 | "right":[15, "-90"], 1105 | "fliped_left": [-15, "90"], 1106 | "fliped_right": [5, "90"] 1107 | }[a["S6"]] 1108 | ordered = com[0][0] < com[1][0] 1109 | digit_rounding = {"0": "{:.0f}", "1": "{:.1f}", "2": "{:.2f}"}[self.e_decimal] 1110 | label = self.commafy(digit_rounding.format(abs(com[0][self.e_source] - com[1][self.e_source]))) 1111 | protruded = ["p_left", "xp_left", "p_right", "xp_right", "average"] 1112 | x2_mod = 5 1113 | if com[0][0] == com[1][0]: 1114 | excess_x = -10 if a["S5"] in ["p_left","xp_left"] else 10 1115 | x1 = { 1116 | "left":self.set_horizontal(com[1][0],self.wide[0]), 1117 | "right":self.set_horizontal(com[1][0],self.wide[1]), 1118 | "midle":self.set_horizontal(com[1][0],int(sum(self.wide)/2)), 1119 | "p_left":self.set_horizontal(com[1][0],self.wide[0]), 1120 | "xp_left": self.set_horizontal(com[1][0], self.wide[0]), 1121 | "p_right": self.set_horizontal(com[1][0], self.wide[1]), 1122 | "xp_right": self.set_horizontal(com[1][0], self.wide[1]), 1123 | "average": self.set_horizontal((com[1][0]+com[0][0])/2, int(sum(self.wide)/2)) 1124 | }[a["S5"]] 1125 | x2 = { 1126 | "left":self.set_horizontal(com[1][0],self.wide[0]+x2_mod), 1127 | "right":self.set_horizontal(com[1][0],self.wide[1]-x2_mod), 1128 | "midle":self.set_horizontal(com[1][0],int(sum(self.wide)/2)), 1129 | "p_left":self.set_horizontal(com[1][0],self.wide[0]-2*x2_mod), 1130 | "xp_left": self.set_horizontal(com[1][0], self.wide[0] - 4*x2_mod), 1131 | "p_right": self.set_horizontal(com[1][0], self.wide[1] + 2*x2_mod), 1132 | "xp_right": self.set_horizontal(com[1][0], self.wide[1] + 4*x2_mod), 1133 | "average": self.set_horizontal((com[1][0]+com[0][0])/2, int(sum(self.wide)/2)) 1134 | }[a["S5"]] 1135 | label = self.commafy(digit_rounding.format(abs(com[0][self.e_source] - com[1][self.e_source]))) 1136 | b = [ 1137 | ' ', # HORIZONTAL 1138 | ' ', # VERTICAL 1139 | ' ', # TOP Y ARROW 1140 | ' ', # BOTTOM Y ARROW 1141 | ' {}', # Y label 1142 | ' ' # HORIZONTAL SHORT 1143 | 1144 | ] 1145 | b[0] = b[0].format(x1, y1, x2 + excess_x, y1, color, width, dashed) 1146 | b[1] = b[1].format(x2, y1 + excess_y, x2, y2 - excess_y, color, width) 1147 | b[2] = b[2].format(x2, y1 + excess_yb, x2 - 4, y1 + excess_y, x2 + 4, y1 + excess_y, color, color) 1148 | b[3] = b[3].format(x2, y2 - excess_yb, x2 - 4, y2 - excess_y, x2 + 4, y2 - excess_y, color, color) 1149 | b[4] = b[4].format(x2 + text_pos[0], int((y2 + y1) / 2), 1150 | f'transform="rotate({text_pos[1]},{x2 + text_pos[0]},{int((y2 + y1) / 2)})"', 1151 | color, 1152 | self.char_care(label)) 1153 | b[5] = b[5].format(x1, y2, x2 + excess_x, y2, color, width, dashed) 1154 | protruded = ["p_left", "xp_left", "p_right", "xp_right", "average"] 1155 | self.svg_code.extend(b if a["S5"] in protruded else b[1:5]) 1156 | else: 1157 | excess_x = 10 if ordered else -10 1158 | x1 = self.set_horizontal(com[0][0],self.wide[1] if ordered else self.wide[0]) 1159 | x2 = { 1160 | "left":self.set_horizontal(com[1][0],self.wide[0]+x2_mod), 1161 | "right":self.set_horizontal(com[1][0],self.wide[1]-x2_mod), 1162 | "midle":self.set_horizontal(com[1][0],int(sum(self.wide)/2)), 1163 | "p_left":self.set_horizontal(com[1][0],self.wide[0]-2*x2_mod), 1164 | "xp_left": self.set_horizontal(com[1][0], self.wide[0] - 4*x2_mod), 1165 | "p_right": self.set_horizontal(com[1][0], self.wide[1] + 2*x2_mod), 1166 | "xp_right": self.set_horizontal(com[1][0], self.wide[1] + 4*x2_mod), 1167 | "average": self.set_horizontal((com[1][0]+com[0][0])/2, int(sum(self.wide)/2)) 1168 | }[a["S5"]] 1169 | 1170 | x3 = { 1171 | "left":None, 1172 | "right":None, 1173 | "midle":None, 1174 | "p_left":self.set_horizontal(com[1][0],self.wide[0]), 1175 | "xp_left": self.set_horizontal(com[1][0], self.wide[0]), 1176 | "p_right": self.set_horizontal(com[1][0], self.wide[1]), 1177 | "xp_right": self.set_horizontal(com[1][0], self.wide[1]), 1178 | "average": self.set_horizontal(com[1][0],self.wide[1] if a["S4"] == "reverse" else self.wide[0]) 1179 | }[a["S5"]] 1180 | 1181 | x4 = { 1182 | "left":None, 1183 | "right":None, 1184 | "midle":None, 1185 | "p_left":x2+excess_x if a["S4"] == "reverse" else x2-excess_x, 1186 | "xp_left": x2+excess_x if a["S4"] == "reverse" else x2-excess_x, 1187 | "p_right":x2-excess_x if a["S4"] == "reverse" else x2+excess_x, 1188 | "xp_right": x2-excess_x if a["S4"] == "reverse" else x2+excess_x, 1189 | "average": x2-excess_x if a["S4"] == "reverse" else x2-excess_x 1190 | }[a["S5"]] 1191 | 1192 | b = [ 1193 | ' ', # HORIZONTAL 1194 | ' ', # VERTICAL 1195 | ' ',# TOP Y ARROW 1196 | ' ',# BOTTOM Y ARROW 1197 | ' {}',#Y label 1198 | ' ' # HORIZONTAL SHORT 1199 | 1200 | ] 1201 | b[0] = b[0].format(x1,y1,x2+excess_x,y1,color,width,dashed) 1202 | b[1] = b[1].format(x2,y1+excess_y,x2,y2-excess_y,color,width) 1203 | b[2] = b[2].format(x2,y1+excess_yb,x2-4,y1+excess_y,x2+4,y1+excess_y,color,color) 1204 | b[3] = b[3].format(x2,y2-excess_yb,x2-4,y2-excess_y,x2+4,y2-excess_y,color,color) 1205 | b[4] = b[4].format(x2+text_pos[0],int((y2+y1)/2),f'transform="rotate({text_pos[1]},{x2+text_pos[0]},{int((y2+y1)/2)})"',color,self.char_care(label)) 1206 | b[5] = b[5].format(x4, y2, x3, y2, color, width, dashed) 1207 | self.svg_code.extend(b if a["S5"] in protruded else b[0:5]) 1208 | 1209 | @functools.lru_cache(maxsize=1) 1210 | def span_dg(self): 1211 | try: 1212 | if not self.span_worthy: return 1213 | if len(self.plot_dict) != 1: 1214 | self.span_worthy = False 1215 | self.msg.append("This software only performs span analysis if one and only one reaction path is ploted\n") 1216 | return 1217 | only_plot = list(self.plot_dict.values())[0] 1218 | r_const = {"kcal/mol": 0.0019872, "kJ/mol": 0.0019872 * 4.184, "eV": 0.0019872 * 0.04336}[self.span["units"]] 1219 | boltz_const = {"kcal/mol":3.29762e-27,"kJ/mol":3.29762e-27 * 4.184,"eV": 3.29762e-27 * 0.04336}[self.span["units"]] 1220 | planck_const = {"kcal/mol":1.58367e-37,"kJ/mol":1.58367e-37 * 4.184, "eV":1.58367e-37 * 0.04336}[self.span["units"]] 1221 | delta_e = only_plot[-1][self.e_source]-only_plot[0][self.e_source] 1222 | kcal_limit = 3.5 1223 | limit = {"kcal/mol": kcal_limit, "kJ/mol": kcal_limit * 4.184,"eV": kcal_limit * 0.04336}[self.span["units"]] 1224 | if not len(only_plot) > 2: 1225 | text = "Need more than two structures for span analysis\n" 1226 | self.msg.append(text) 1227 | self.span_worthy = False; return 1228 | 1229 | first = min(a[0] for a in only_plot) 1230 | last = max(a[0] for a in only_plot) 1231 | text = "Analysis assumes structures #{} and #{} have the same geometry," 1232 | text += " but are energeticaly distiguished by the {} of the reaction.\n" 1233 | self.msg.append(text.format(first, last, "exergonicity" if self.e_source == 3 else "exotermicity")) 1234 | if self.e_source != 3: 1235 | text = "WARNING: Data above should only be used after carefull consideration." 1236 | text += "Enthalpy values were employed in place of Gibbs Free energy\n" 1237 | self.msg.append(text) 1238 | # Is it a TS or INT? 1239 | if self.span["irrespective"] != 1: 1240 | give_up = False 1241 | if not only_plot[0][1] == only_plot[-1][1]: 1242 | text = "#{} and #{} must be the same type: TS or INT\n" 1243 | self.msg.append(text.format(only_plot[0][0],only_plot[-1][0])) 1244 | return 1245 | for i,a in enumerate(only_plot): 1246 | if i == 0 or i+1 == len(only_plot): 1247 | top = only_plot[1][self.e_source] < only_plot[0][self.e_source] and only_plot[-1][self.e_source] > only_plot[-2][self.e_source] 1248 | else: 1249 | top = only_plot[i-1][self.e_source] < a[self.e_source] > only_plot[i+1][self.e_source] 1250 | if top and a[1] == "TS": 1251 | continue 1252 | elif top and a[1] == "INT": 1253 | text = "Are you sure #{} is not a TS? It is directly connected to structures lower in both forward and backwards direction!\n" 1254 | self.msg.append(text.format(a[0])) 1255 | elif not top and a[1] == "TS": 1256 | text = "Are you sure #{} is not an INT? It is directly connected to structure(s) higher in energy!\n" 1257 | self.msg.append(text.format(a[0])) 1258 | elif a[1] not in ["TS","INT"]: 1259 | text = "#{} should be set as either TS or INT, otherwise it will be ploted but excluded from analysis\n" 1260 | self.msg.append(text.format(a[0])) 1261 | give_up = True 1262 | if give_up: 1263 | return 1264 | #TOF 1265 | all_it = [] 1266 | for idx_a, a in enumerate(only_plot[:-1]): 1267 | for idx_b,b in enumerate(only_plot[:-1]): 1268 | all_it.append([a, a[self.e_source] - b[self.e_source] - delta_e if idx_b < idx_a else a[self.e_source] - b[self.e_source] , b]) 1269 | 1270 | if self.span["irrespective"] != 1: 1271 | all_it = [a for a in all_it if all([a[0][0] != a[2][0],a[0][1] == "TS", a[2][1] == "INT"])] 1272 | if self.span["irrespective"] == 1: 1273 | text = "WARNING: Data above should only be used after carefull consideration." 1274 | text += "Equations were applied on the assumption that all structures are both intermediates and transition states simultaneously\n" 1275 | self.msg.append(text) 1276 | if all([self.span["irrespective"] != 1, all_it, self.e_source == 3]): 1277 | text = "Ref.: Kozuch, S., Shaik, S. Acc. Chem. Res. 2011, 44, 101.\n" 1278 | self.msg.append(text) 1279 | if all_it: 1280 | denominator = sum(math.exp(a[1] / (self.temperature * r_const)) for a in all_it) 1281 | tof = (((math.exp(-delta_e / (r_const * self.temperature)) - 1) / denominator) * self.temperature * boltz_const) / planck_const 1282 | all_ts = list(dict.fromkeys([a[0][0] for a in all_it])) 1283 | all_int = list(dict.fromkeys([a[2][0] for a in all_it])) 1284 | x_tof_i = [] 1285 | x_tof_ts = [] 1286 | for x in all_int: 1287 | a = sum(math.exp(a[1] / (self.temperature * r_const)) for a in all_it if x == a[2][0]) / denominator 1288 | x_tof_i.append([x, a * 100]) 1289 | for x in all_ts: 1290 | a = sum(math.exp(a[1] / (self.temperature * r_const)) for a in all_it if x == a[0][0]) / denominator 1291 | x_tof_ts.append([x, a * 100]) 1292 | self.msg.append("".join("#{:>5}: {:>7.2f}% \n".format(*a) for a in x_tof_i)) 1293 | self.msg.append("X(tof) for intermediates:") 1294 | self.msg.append("".join("#{:>5}: {:>7.2f}% \n".format(*a) for a in x_tof_ts)) 1295 | self.msg.append("X(tof) for transition states:") 1296 | self.msg.append("TOF as catalytic flux law: {:5e} /h\n".format(tof * 3600)) 1297 | if abs(tof) > 1e8: 1298 | self.msg.append("ALERT: Please consider the possibility of diffusion control rates\n") 1299 | #CONDITIONALS FOR SPAN 1300 | if delta_e >= 0: 1301 | self.msg.append("Reaction is {}! Span will not be computed!\n".format("endergonic" if self.e_source == 3 else "endotermic")) 1302 | self.span_worthy = False; return 1303 | if not all(a[1] in ["TS","INT"] for a in only_plot) and self.span["irrespective"] != 1: 1304 | text = "All structures have to be identified as either transition states or intermediates for a strict span analysis." 1305 | text += " Irrestrictive analysis may be caried out by checking the 'irrespective of type(TS/INT)' box." 1306 | text += " No span analysis will be conducted\n" 1307 | self.msg.append(text) 1308 | self.span_worthy = False; return 1309 | # SPAN 1310 | all_it = [] 1311 | for idx_a, a in enumerate(only_plot[:-1]): 1312 | for idx_b,b in enumerate(only_plot[:-1]): 1313 | all_it.append([a, a[self.e_source] - b[self.e_source] + delta_e if idx_a span[1] - limit and any(span[n][0] != a[n][0] for n in [0,2]): 1328 | text = "WARNING: Span from #{} to #{} is only {:.2f} {} lower".format(a[0][0],a[2][0],span[1]-a[1],self.span["units"]) 1329 | text += " than #{} to #{} and may influence the rate determining state\n".format(span[0][0],span[2][0]) 1330 | self.msg.append(text) 1331 | tof_span = (((math.exp(-span[1]/(r_const*self.temperature))))*self.temperature*boltz_const)/planck_const 1332 | self.msg.append("TOF from span: {:5e} /h\n".format(tof_span*3600)) 1333 | return span 1334 | except OverflowError: 1335 | self.msg.append("TOF calculation failed due to floating point overflow! Please review your data and units ({}).".format(self.span["units"])) 1336 | 1337 | @functools.lru_cache(maxsize=1) 1338 | def graph_span(self): 1339 | if not self.span_worthy: return 1340 | if len(self.plot_dict) != 1: self.span_worthy = False; return 1341 | only_plot = list(self.plot_dict.values())[0] 1342 | tdi_correct = {a: b for a,b in zip(pref.menu_d, [-35,-2,10] if self.plot_np else [-20,-2,10])} 1343 | tdts_correct = {a: b for a, b in zip(pref.menu_d, [-15, 18, 32] if self.plot_np else [-15,2,16])} 1344 | if self.span_dg() is None: self.span_worthy = False; return 1345 | if self.span_worthy: 1346 | data = [[self.span_dg()[0][n] for n in [0,-1]],[self.span_dg()[2][n] for n in [0,-1]]] 1347 | span = self.span_dg()[1] 1348 | data = sorted(data,key=lambda x: x[1]) 1349 | delta_e = only_plot[-1][self.e_source]-only_plot[0][self.e_source] 1350 | if self.big_arrow: 1351 | # TDI arrow big 1352 | # print(self.data) 1353 | x = tdi_correct[next(a for a in only_plot if a[0] == data[1][0])[5]] - 40 1354 | p = [self.set_horizontal(data[1][0], 30), data[1][1] + x] 1355 | a = [ 1356 | ' TDI', 1357 | ' '] 1358 | arrow_size = [10, -70, -10, -70, -10, -40, -20, -40, 0, -10, 20, -40, 10, -40] 1359 | a[0] = a[0].format(p[0], p[1]+35) 1360 | a[1] = a[1].format(*[int(j/3)+p[0] if i%2 == 0 else int(j/3)+p[1]+70 for i,j in enumerate(arrow_size)]) 1361 | self.svg_code.extend(a) 1362 | # TDTS arrow 1363 | x = tdts_correct[next(a for a in only_plot if a[0] == data[0][0])[5]] + 140 1364 | p = [self.set_horizontal(data[0][0], 30), data[0][1] + x] 1365 | a = [ 1366 | ' TDTS', 1367 | ' '] 1368 | arrow_size = [10, -10, -10, -10, -10, -40, -20, -40, 0, -70, 20, -40, 10, -40] 1369 | a[0] = a[0].format(p[0], p[1] -25) 1370 | a[1] = a[1].format(*[int(j/3)+p[0] if i%2 == 0 else int(j/3)+p[1]-43 for i,j in enumerate(arrow_size)]) 1371 | self.svg_code.extend(a) 1372 | else: 1373 | # TDI arrow 1374 | # print(self.data) 1375 | x = tdi_correct[next(a for a in only_plot if a[0] == data[1][0])[5]] - 40 1376 | p = [self.set_horizontal(data[1][0], 10), data[1][1] + x] 1377 | a = [ 1378 | ' TDI', 1379 | ' {}'] 1380 | a[0] = a[0].format(p[0] + 20, p[1] + 45) 1381 | a[1] = a[1].format(p[0] + 20, p[1] + 63,self.char_care("↓")) 1382 | self.svg_code.extend(a) 1383 | # TDTS arrow 1384 | x = tdts_correct[next(a for a in only_plot if a[0] == data[0][0])[5]] + 140 1385 | p = [self.set_horizontal(data[0][0], 50), data[0][1] + x] 1386 | a = [ 1387 | ' TDTS', 1388 | ' {}'] 1389 | a[0] = a[0].format(p[0] - 20, p[1] -30) 1390 | a[1] = a[1].format(p[0] - 20, p[1] -53,self.char_care("↑")) 1391 | self.svg_code.extend(a) 1392 | # dg and span anotations 1393 | a = [ 1394 | ' Delta = {}', 1395 | ' Span = {}'] 1396 | a[0] = a[0].format(self.commafy("{:.2f}".format(delta_e))) 1397 | a[1] = a[1].format(self.commafy("{:.2f}".format(span))) 1398 | self.svg_code.extend(a) 1399 | 1400 | @functools.lru_cache(maxsize=1) 1401 | def return_svg_code(self): 1402 | if not [a for a in self.data_dict if self.data_dict[a]]: 1403 | self.msg.append("No data!"); 1404 | self.graph_frame(); 1405 | self.svg_code.append(''); 1406 | return self.svg_code 1407 | self.graph_frame() 1408 | self.graph_grid() 1409 | self.graph_crt_points() 1410 | self.graph_connectors() 1411 | self.graph_comparers() 1412 | if self.span_request: self.span_dg() 1413 | if self.span_request: self.graph_span() 1414 | self.svg_code.append('') 1415 | return self.svg_code 1416 | def save_svg(self,svg_name): 1417 | svg_name = ".E_profile.svg" if svg_name is None else svg_name 1418 | try: 1419 | with open(os.path.join(os.getcwd(),svg_name), "w") as out_file: 1420 | for line in self.return_svg_code(): out_file.write(line + "\n") 1421 | if svg_name != ".E_profile.svg": 1422 | self.msg.append("Take a look at file {}!".format(svg_name)) 1423 | return self.msg 1424 | except FileNotFoundError: 1425 | pass 1426 | 1427 | def initialize(): 1428 | set_cw = lambda x: x.grid_columnconfigure(0, weight=1) 1429 | set_rw = lambda x: x.grid_rowconfigure(0, weight=1) 1430 | global pref, note, window, frame2 1431 | pref = Preferences() 1432 | window = tk.Tk() 1433 | #NOTEBOOK 1434 | frame1 = tk.Frame(master=window) 1435 | frame1.grid(column=0,row=0,rowspan=2,sticky="news") 1436 | set_cw(frame1) 1437 | set_rw(frame1) 1438 | note = Note(frame1) 1439 | #GENERAL 1440 | frame2 = tk.Frame(master=window) 1441 | frame2.grid(column=1,row=0,rowspan=1,sticky="news") 1442 | set_cw(frame2) 1443 | set_rw(frame2) 1444 | frame3 = tk.Frame(master=window) 1445 | label = tk.Label(frame3, text="github.com/ricalmang") 1446 | label.grid(column=0,row=0,stick="news") 1447 | frame3.grid(column=1,row=1,rowspan=1) 1448 | menu = GeneralMenu(frame2,name="Actions") 1449 | menu.grid(column=0, row=0, columnspan=1, pady="0",padx="0", rowspan=1,sticky="news") 1450 | set_cw(window) 1451 | set_rw(window) 1452 | w,h = 925 if sys.platform == "win32" or os.name == "nt" else 1000, 685 1453 | ws = window.winfo_screenwidth() # width of the screen 1454 | hs = window.winfo_screenheight() # height of the screen 1455 | window.minsize(w,h) 1456 | window.maxsize(ws,hs) 1457 | x = int(ws/2 - w/2) 1458 | y = int(hs/2 - h/2) 1459 | window.geometry(f"{w}x{h}+{x}+{y}") 1460 | window.mainloop() 1461 | initialize() 1462 | --------------------------------------------------------------------------------