├── 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 |  
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 | 
21 |
22 | ### Output graphs
23 |
24 | #### Example 1
25 |
26 | 
27 |
28 | #### Example 2
29 |
30 | 
31 |
32 | #### Example 3
33 |
34 | 
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 | |  |  |
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 | [](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 |
86 |
--------------------------------------------------------------------------------
/mechasvg/supl/example_3.svg:
--------------------------------------------------------------------------------
1 |
2 |
88 |
--------------------------------------------------------------------------------
/mechasvg/supl/example_2.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 | '');
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 |
--------------------------------------------------------------------------------