Source code for xpecgen.xpecgenGUI
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""xpecgenGUI.py: A GUI for the xpecgen module"""
from __future__ import print_function
from tkinter import *
from tkinter.ttk import *
import tkinter.filedialog
from tkinter import messagebox
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
from matplotlib.figure import Figure
import threading
import queue
from traceback import print_exc
import os
from glob import glob
import re # Regular expressions, used for sorting
from . import xpecgen as xg
__author__ = "Dih5"
_elements = ['Nihil', 'Hydrogen', 'Helium', 'Lithium', 'Beryllium', 'Boron', 'Carbon, Graphite', 'Nitrogen', 'Oxygen',
'Fluorine', 'Neon', 'Sodium', 'Magnesium', 'Aluminum', 'Silicon', 'Phosphorus', 'Sulfur', 'Chlorine',
'Argon', 'Potassium', 'Calcium', 'Scandium', 'Titanium', 'Vanadium', 'Chromium', 'Manganese', 'Iron',
'Cobalt', 'Nickel', 'Copper', 'Zinc', 'Gallium', 'Germanium', 'Arsenic', 'Selenium', 'Bromine', 'Krypton',
'Rubidium', 'Strontium', 'Yttrium', 'Zirconium', 'Niobium', 'Molybdenum', 'Technetium', 'Ruthenium',
'Rhodium', 'Palladium', 'Silver', 'Cadmium', 'Indium', 'Tin', 'Antimony', 'Tellurium', 'Iodine', 'Xenon',
'Cesium', 'Barium', 'Lanthanum', 'Cerium', 'Praseodymium', 'Neodymium', 'Promethium', 'Samarium',
'Europium', 'Gadolinium', 'Terbium', 'Dysprosium', 'Holmium', 'Erbium', 'Thulium', 'Ytterbium', 'Lutetium',
'Hafnium', 'Tantalum', 'Tungsten', 'Rhenium', 'Osmium', 'Iridium', 'Platinum', 'Gold', 'Mercury',
'Thallium', 'Lead', 'Bismuth', 'Polonium', 'Astatine', 'Radon', 'Francium', 'Radium', 'Actinium',
'Thorium', 'Protactinium', 'Uranium']
def _add_element_name(material):
"""Check if the string is a integer. If so, convert to element name, e.g., 13-> 13: Aluminum"""
try:
int(material)
return "%d: %s" % (int(material), _elements[int(material)])
except ValueError:
return material
def _remove_element_name(material):
"""Revert _add_element_name"""
if re.match("[0-9]+: .*", material):
return material.split(":")[0]
else:
return material
[docs]class CreateToolTip(object):
"""
A tooltip for a given widget.
"""
# Based on the content from this post:
# http://stackoverflow.com/questions/3221956/what-is-the-simplest-way-to-make-tooltips-in-tkinter
def __init__(self, widget, text, color="#ffe14c"):
"""
Create a tooltip for an existent widget.
Args:
widget: The widget the tooltip is applied to.
text (str): The text of the tooltip.
color: The color of the tooltip.
"""
self.waittime = 500 # miliseconds
self.wraplength = 180 # pixels
self.widget = widget
self.text = text
self.color = color
self.widget.bind("<Enter>", self.enter)
self.widget.bind("<Leave>", self.leave)
self.widget.bind("<ButtonPress>", self.leave)
self.id = None
self.tw = None
def enter(self, event=None):
self.schedule()
def leave(self, event=None):
self.unschedule()
self.hidetip()
def schedule(self):
self.unschedule()
self.id = self.widget.after(self.waittime, self.showtip)
def unschedule(self):
id = self.id
self.id = None
if id:
self.widget.after_cancel(id)
def showtip(self, event=None):
x, y, cx, cy = self.widget.bbox("insert")
x += self.widget.winfo_rootx() + 25
y += self.widget.winfo_rooty() + 20
# creates a toplevel window
self.tw = Toplevel(self.widget)
# Leaves only the label and removes the app window
self.tw.wm_overrideredirect(True)
self.tw.wm_geometry("+%d+%d" % (x, y))
label = Label(self.tw, text=self.text, justify='left',
background=self.color, relief='solid', borderwidth=1,
wraplength=self.wraplength)
label.pack(ipadx=1)
def hidetip(self):
tw = self.tw
self.tw = None
if tw:
tw.destroy()
[docs]class ParBox:
"""A parameter entry with labels preceding and succeeding it and an optional tooltip"""
def __init__(self, master=None, textvariable=0, lblText="", unitsTxt="", helpTxt="", row=0, read_only=False):
"""
Create the parameter box.
Args:
master: the master widget.
textvariable (:obj:`tkinter.Variable`): The variable associated with the parameter.
lblText (str): The text preceding the text entry.
unitsTxt (str): The text succeeding the text entry, typically the units.
helpTxt (str): The help text to show in the tooltip. If "", no tooltip is shown.
row (int): The row where the widgets are set in the grid.
read_only (bool): Whether the entry is read_only.
"""
self.lbl = Label(master, text=lblText)
self.lbl.grid(row=row, column=0, sticky=W)
self.txt = Entry(master, textvariable=textvariable)
self.txt.grid(row=row, column=1, sticky=W + E)
self.units = Label(master, text=unitsTxt, anchor=W)
self.units.grid(row=row, column=2, sticky=W)
if helpTxt != "":
self.lblTT = CreateToolTip(self.lbl, helpTxt)
self.txtTT = CreateToolTip(self.txt, helpTxt)
if read_only:
self.txt["state"] = "readonly"
def _human_order_key(text):
"""
Key function to sort in human order.
"""
# This is based in http://nedbatchelder.com/blog/200712/human_sorting.html
return [int(c) if c.isdigit() else c for c in re.split('(\d+)', text)]
[docs]class XpecgenGUI(Notebook):
"""Tk-based GUI for the xpecgen package."""
def __init__(self, master=None):
"""
Create the GUI.
Args:
master: the tk master of the GUI.
"""
Notebook.__init__(self, master)
Grid.rowconfigure(master, 0, weight=1)
Grid.columnconfigure(master, 0, weight=1)
self.grid(row=0, column=0, sticky=N + S + E + W)
self.master.title("".join(('xpecgen v', xg.__version__, " GUI")))
# self.master.minsize(800, 600)
self.spectra = []
self.spectra_labels = []
self.active_spec = 0 # The active spectrum from the list
# Interpolations used to calculate HVL
self.fluence_to_dose = xg.get_fluence_to_dose()
self.mu_Al = xg.get_mu(13)
self.mu_Cu = xg.get_mu(29)
self.initVariables()
self.createWidgets()
self.history_poll()
[docs] def history_poll(self):
"""
Polling method to update changes in spectrum list.
"""
# Note the Tk manual advised for polling instead of binding all methods that are able to change a listbox
try:
now = self.lstHistory.curselection()[0]
if now != self.active_spec:
self.active_spec = now
self.update_plot()
except IndexError:
pass
self.after(150, self.history_poll)
[docs] def initVariables(self):
"""Create and initialize interface variables"""
# Calculation-related variables
self.E0 = DoubleVar()
self.E0.set(100.0)
self.Theta = DoubleVar()
self.Theta.set(12.0)
self.Phi = DoubleVar()
self.Phi.set(0.0)
self.Z = StringVar()
self.Z.set(_add_element_name("74"))
self.EMin = DoubleVar()
self.EMin.set(3.0)
self.NumE = IntVar()
self.NumE.set(50)
self.Eps = DoubleVar()
self.Eps.set(0.5)
# Operation-related variables
self.AttenMaterial = StringVar()
self.AttenMaterial.set(_add_element_name("13"))
self.AttenThick = DoubleVar()
self.AttenThick.set(0.1)
self.NormCriterion = StringVar()
self.NormCriterion.set("Number")
self.NormValue = DoubleVar()
self.NormValue.set(1.0)
# Output HVLs
self.HVL1 = StringVar()
self.HVL1.set("0")
self.HVL2 = StringVar()
self.HVL2.set("0")
self.HVL3 = StringVar()
self.HVL3.set("0")
self.HVL4 = StringVar()
self.HVL4.set("0")
# Output Norms
self.number = StringVar()
self.number.set("0")
self.energy = StringVar()
self.energy.set("0")
self.dose = StringVar()
self.dose.set("0")
[docs] def createWidgets(self):
"""Create the widgets in the GUI"""
self.frmCalc = Frame(self)
self.frmAnal = Frame(self)
self.add(self.frmCalc, text='Calculate')
self.add(self.frmAnal, text='Analyze')
# self.frmCalc.grid(sticky=N+S+E+W, column=0, row=0)
# Calculate Tab
# -Physical parameters
self.frmPhysPar = LabelFrame(self.frmCalc, text="Physical parameters")
self.frmPhysPar.grid(row=0, column=0, sticky=N + S + E + W)
self.ParE0 = ParBox(self.frmPhysPar, self.E0, lblText="Electron Energy (E0)",
unitsTxt="keV", helpTxt="Electron kinetic energy in keV.", row=0)
self.ParTheta = ParBox(self.frmPhysPar, self.Theta, lblText=u"Angle (\u03b8)",
unitsTxt="º", helpTxt="X-rays emission angle. The anode's normal is at 90º.", row=1)
self.ParPhi = ParBox(self.frmPhysPar, self.Phi, lblText=u"Elevation angle (\u03c6)",
unitsTxt="º", helpTxt="X-rays emission altitude. The anode's normal is at 0º.", row=2)
self.lblZ = Label(self.frmPhysPar, text="Target atomic number")
self.lblZTT = CreateToolTip(self.lblZ,
"Atomic number of the target. IMPORTANT: Only used in the cross-section and distance scaling. Fluence uses a tugsten model, but the range is increased in lower Z materials. Besides, characteristic radiation is only calculated for tugsten.")
self.lblZ.grid(row=3, column=0, sticky=W)
self.cmbZ = Combobox(self.frmPhysPar, textvariable=self.Z)
self.cmbZ.grid(row=3, column=1, sticky=W + E)
self.cmbZTT = CreateToolTip(self.cmbZ,
"Atomic number of the target. IMPORTANT: Only used in the cross-section and distance scaling. Fluence uses a tugsten model, but the range is increased in lower Z materials. Besides, characteristic radiation is only calculated for tugsten.")
# Available cross-section data
target_list = list(map(lambda x: (os.path.split(x)[1]).split(
".csv")[0], glob(os.path.join(xg.data_path, "cs", "*.csv"))))
target_list.remove("grid")
# Available csda-data
csda_list = list(map(lambda x: (os.path.split(x)[1]).split(
".csv")[0], glob(os.path.join(xg.data_path, "csda", "*.csv"))))
# Available attenuation data
mu_list = list(map(lambda x: (os.path.split(x)[1]).split(
".csv")[0], glob(os.path.join(xg.data_path, "mu", "*.csv"))))
mu_list.sort(key=_human_order_key) # Used later
available_list = list(set(target_list) & set(csda_list) & set(mu_list))
available_list.sort(key=_human_order_key)
self.cmbZ["values"] = list(map(_add_element_name, available_list))
Grid.columnconfigure(self.frmPhysPar, 0, weight=0)
Grid.columnconfigure(self.frmPhysPar, 1, weight=1)
Grid.columnconfigure(self.frmPhysPar, 2, weight=1)
Grid.columnconfigure(self.frmPhysPar, 3, weight=0)
# -Numerical Parameters
self.frmNumPar = LabelFrame(self.frmCalc, text="Numerical parameters")
self.frmNumPar.grid(row=0, column=1, sticky=N + S + E + W)
self.ParEMin = ParBox(self.frmNumPar, self.EMin,
lblText="Min energy", unitsTxt="keV",
helpTxt="Minimum kinetic energy in the bremsstrahlung calculation. Note this might influence the characteristic peaks prediction.",
row=0)
self.ParNumE = ParBox(self.frmNumPar, self.NumE,
lblText="Number of points", unitsTxt="",
helpTxt="Amount of points for the mesh were the bremsstrahlung spectrum is calculated.\nBremsstrahlung component is extended by interpolation.",
row=1)
self.ParEps = ParBox(self.frmNumPar, self.Eps, lblText="Integrating tolerance", unitsTxt="",
helpTxt="A numerical tolerance parameter used in numerical integration. Values around 0.5 provide fast and accurate calculations. If you want insanely accurate (and physically irrelevant) numerical integration you can reduce this value, increasing computation time.",
row=2)
Grid.columnconfigure(self.frmNumPar, 0, weight=0)
Grid.columnconfigure(self.frmNumPar, 1, weight=1)
Grid.columnconfigure(self.frmNumPar, 2, weight=0)
# -Buttons, status bar...
self.cmdCalculate = Button(self.frmCalc, text="Calculate")
self.cmdCalculate["command"] = self.calculate
self.cmdCalculate.bind('<Return>', lambda event: self.calculate())
self.cmdCalculate.bind(
'<KP_Enter>', lambda event: self.calculate()) # Enter (num. kb)
self.cmdCalculate.grid(row=1, column=0, sticky=E + W)
self.barProgress = Progressbar(
self.frmCalc, orient="horizontal", length=100, mode="determinate")
self.barProgress.grid(row=1, column=1, columnspan=1, sticky=E + W)
Grid.columnconfigure(self.frmCalc, 0, weight=1)
Grid.columnconfigure(self.frmCalc, 1, weight=1)
Grid.rowconfigure(self.frmCalc, 0, weight=1)
Grid.rowconfigure(self.frmCalc, 1, weight=0)
# Analyze tab
# -History frame
self.frmHist = LabelFrame(self.frmAnal, text="History")
self.frmHist.grid(row=0, column=0, sticky=N + S + E + W)
self.lstHistory = Listbox(self.frmHist, selectmode=BROWSE)
self.lstHistory.grid(row=0, column=0, sticky=N + S + E + W)
self.scrollHistory = Scrollbar(self.frmHist, orient=VERTICAL)
self.scrollHistory.grid(row=0, column=1, sticky=N + S)
self.lstHistory.config(yscrollcommand=self.scrollHistory.set)
self.scrollHistory.config(command=self.lstHistory.yview)
self.cmdCleanHistory = Button(self.frmHist, text="Revert to selected", state=DISABLED)
self.cmdCleanHistory["command"] = self.clean_history
self.cmdCleanHistory.grid(row=1, column=0, columnspan=2, sticky=E + W)
self.cmdExport = Button(self.frmHist, text="Export selected", state=DISABLED)
self.cmdExport["command"] = self.export
self.cmdExport.grid(row=2, column=0, columnspan=2, sticky=E + W)
Grid.rowconfigure(self.frmHist, 0, weight=1)
Grid.columnconfigure(self.frmHist, 0, weight=1)
Grid.columnconfigure(self.frmHist, 1, weight=0)
# -Operations frame
self.frmOper = LabelFrame(self.frmAnal, text="Spectrum operations")
self.frmOper.grid(row=1, column=0, sticky=N + S + E + W)
# --Attenuation
self.frmOperAtten = LabelFrame(self.frmOper, text="Attenuate")
self.frmOperAtten.grid(row=0, column=0, sticky=N + S + E + W)
self.lblAttenMaterial = Label(self.frmOperAtten, text="Material")
self.lblAttenMaterial.grid()
self.cmbAttenMaterial = Combobox(self.frmOperAtten, textvariable=self.AttenMaterial)
self.cmbAttenMaterial["values"] = list(map(_add_element_name, mu_list))
self.cmbAttenMaterial.grid(row=0, column=1, sticky=E + W)
self.ParAttenThick = ParBox(
self.frmOperAtten, self.AttenThick, lblText="Thickness", unitsTxt="cm", row=1)
self.cmdAtten = Button(self.frmOperAtten, text="Add attenuation", state=DISABLED)
self.cmdAtten["command"] = self.attenuate
self.cmdAtten.grid(row=2, column=0, columnspan=3, sticky=E + W)
Grid.columnconfigure(self.frmOperAtten, 0, weight=0)
Grid.columnconfigure(self.frmOperAtten, 1, weight=1)
Grid.columnconfigure(self.frmOperAtten, 2, weight=0)
# --Normalize
self.frmOperNorm = LabelFrame(self.frmOper, text="Normalize")
self.frmOperNorm.grid(row=1, column=0, sticky=N + S + E + W)
self.lblNormCriterion = Label(self.frmOperNorm, text="Criterion")
self.lblNormCriterion.grid()
self.cmbNormCriterion = Combobox(
self.frmOperNorm, textvariable=self.NormCriterion)
self.criteriaList = ["Number", "Energy (keV)", "Dose (mGy)"]
self.cmbNormCriterion["values"] = self.criteriaList
self.cmbNormCriterion.grid(row=0, column=1, sticky=E + W)
self.ParNormValue = ParBox(
self.frmOperNorm, self.NormValue, lblText="Value", unitsTxt="", row=1)
self.cmdNorm = Button(self.frmOperNorm, text="Normalize", state=DISABLED)
self.cmdNorm["command"] = self.normalize
self.cmdNorm.grid(row=2, column=0, columnspan=3, sticky=E + W)
Grid.columnconfigure(self.frmOperNorm, 0, weight=0)
Grid.columnconfigure(self.frmOperNorm, 1, weight=1)
Grid.columnconfigure(self.frmOperNorm, 2, weight=0)
Grid.columnconfigure(self.frmOper, 0, weight=1)
Grid.rowconfigure(self.frmOper, 0, weight=1)
# -Plot frame
self.frmPlot = Frame(self.frmAnal)
try:
self.fig = Figure(figsize=(5, 4), dpi=100,
facecolor=self.master["bg"])
self.subfig = self.fig.add_subplot(111)
self.canvas = FigureCanvasTkAgg(self.fig, master=self.frmPlot)
self.canvas.show()
self.canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=1)
self.canvasToolbar = NavigationToolbar2TkAgg(
self.canvas, self.frmPlot)
self.canvasToolbar.update()
self.canvas._tkcanvas.pack(side=TOP, fill=BOTH, expand=1)
self.frmPlot.grid(row=0, column=1, rowspan=3, sticky=N + S + E + W)
self.matplotlib_embedded = True
except Exception:
self.matplotlib_embedded = False
# self.cmdShowPlot = Button(self.frmPlot,text="Open plot window")
# self.cmdShowPlot["command"] = self.update_plot
# self.cmdShowPlot.grid(row=0,column=0)
print("WARNING: Matplotlib couldn't be embedded in TkAgg.\nUsing independent window instead",
file=sys.stderr)
# --Spectral parameters frame
self.frmSpectralParameters = LabelFrame(self.frmAnal, text="Spectral parameters")
self.frmSpectralParameters.grid(row=2, column=0, sticky=S + E + W)
self.ParHVL1 = ParBox(self.frmSpectralParameters, self.HVL1, lblText="1HVL Al", unitsTxt="cm", row=0,
read_only=True,
helpTxt="Thickness of Al at which the dose produced by the spectrum is halved, according to the exponential attenuation model.")
self.ParHVL2 = ParBox(self.frmSpectralParameters, self.HVL2, lblText="2HVL Al", unitsTxt="cm", row=1,
read_only=True,
helpTxt="Thickness of Al at which the dose produced by the spectrum after crossing a HVL is halved again, according to the exponential attenuation model.")
self.ParHVL3 = ParBox(self.frmSpectralParameters, self.HVL3, lblText="1HVL Cu", unitsTxt="cm", row=2,
read_only=True,
helpTxt="Thickness of Cu at which the dose produced by the spectrum is halved, according to the exponential attenuation model.")
self.ParHVL4 = ParBox(self.frmSpectralParameters, self.HVL4, lblText="2HVL Cu", unitsTxt="cm", row=3,
read_only=True,
helpTxt="Thickness of Cu at which the dose produced by the spectrum after crossing a HVL is halved again, according to the exponential attenuation model.")
self.ParNorm = ParBox(self.frmSpectralParameters, self.number, lblText="Photon number", unitsTxt="", row=4,
read_only=True, helpTxt="Number of photons in the spectrum.")
self.ParEnergy = ParBox(self.frmSpectralParameters, self.energy, lblText="Energy", unitsTxt="keV", row=5,
read_only=True, helpTxt="Total energy in the spectrum.")
self.ParDose = ParBox(self.frmSpectralParameters, self.dose, lblText="Dose", unitsTxt="mGy", row=6,
read_only=True,
helpTxt="Dose produced in air by the spectrum, assuming it is describing the differential fluence in particles/keV/cm^2.")
Grid.columnconfigure(self.frmAnal, 0, weight=1)
if self.matplotlib_embedded:
# If not embedding, use the whole window
Grid.columnconfigure(self.frmAnal, 1, weight=3)
Grid.rowconfigure(self.frmAnal, 0, weight=1)
Grid.rowconfigure(self.frmAnal, 1, weight=1)
[docs] def enable_analyze_buttons(self):
"""
Enable widgets requiring a calculated spectrum to work.
"""
self.cmdCleanHistory["state"] = "normal"
self.cmdExport["state"] = "normal"
self.cmdAtten["state"] = "normal"
self.cmdNorm["state"] = "normal"
[docs] def update_plot(self):
"""
Update the canvas after plotting something.
If matplotlib is not embedded, show it in an independent window.
"""
if self.matplotlib_embedded:
self.subfig.clear()
self.spectra[self.active_spec].get_plot(self.subfig)
self.canvas.draw()
self.canvasToolbar.update()
self.fig.tight_layout()
else:
# FIXME: Update if independent window is opened
self.spectra[self.active_spec].show_plot(block=False)
self.update_param()
[docs] def update_param(self):
"""
Update parameters calculated from the active spectrum.
"""
hvlAl = self.spectra[self.active_spec].hvl(0.5, self.fluence_to_dose, self.mu_Al)
qvlAl = self.spectra[self.active_spec].hvl(0.25, self.fluence_to_dose, self.mu_Al)
hvlCu = self.spectra[self.active_spec].hvl(0.5, self.fluence_to_dose, self.mu_Cu)
qvlCu = self.spectra[self.active_spec].hvl(0.25, self.fluence_to_dose, self.mu_Cu)
# TODO: (?) cache the results
self.HVL1.set('%s' % float('%.3g' % hvlAl))
self.HVL2.set('%s' % float('%.3g' % (qvlAl - hvlAl)))
self.HVL3.set('%s' % float('%.3g' % hvlCu))
self.HVL4.set('%s' % float('%.3g' % (qvlCu - hvlCu)))
self.number.set('%s' % float('%.3g' % (self.spectra[self.active_spec].get_norm())))
self.energy.set('%s' % float('%.3g' % (self.spectra[self.active_spec].get_norm(lambda x: x))))
self.dose.set('%s' % float('%.3g' % (self.spectra[self.active_spec].get_norm(self.fluence_to_dose))))
[docs] def monitor_bar(self, a, b):
"""
Update the progress bar.
Args:
a (int): The number of items already calculated.
b (int): The total number of items to calculate.
"""
self.barProgress["value"] = a
self.barProgress["maximum"] = b
[docs] def clean_history(self):
"""
Clean the spectra history.
"""
try:
now = int(self.lstHistory.curselection()[0])
if now == len(self.spectra) - 1: # No need to slice
return
self.spectra = self.spectra[0:now + 1]
self.lstHistory.delete(now + 1, END)
except IndexError: # Ignore if nothing selected
pass
[docs] def export(self):
"""
Export the selected spectrum in xlsx format, showing a file dialog to choose the route.
"""
if self.lstHistory.curselection() == ():
selection = -1
else:
selection = int(self.lstHistory.curselection()[0])
file_opt = options = {}
options['defaultextension'] = '.xlsx'
options['filetypes'] = [('Excel Spreadsheet', '.xlsx'), ('Comma-separated values', '.csv')]
options['initialfile'] = 'spectrum.xlsx'
options['parent'] = self
options['title'] = 'Export spectrum'
filename = tkinter.filedialog.asksaveasfilename(**file_opt)
if not filename: # Ignore if canceled
return
ext = filename.split(".")[-1]
if ext == "xlsx":
self.spectra[selection].export_xlsx(filename)
elif ext == "csv":
self.spectra[selection].export_csv(filename)
else:
messagebox.showerror("Error",
"Unknown file extension: " + ext + "\nUse the file types from the dialog to export.")
[docs] def calculate(self):
"""
Calculates a new spectrum using the parameters in the GUI.
"""
# If a calculation was being held, abort it instead
try:
if self.calc_thread.is_alive():
self.abort_calculation = True
self.cmdCalculate["text"] = "(Aborting)"
self.cmdCalculate["state"] = "disabled"
return
except AttributeError: # If there is no calculation thread, there is nothing to worry about
pass
self.calculation_count = 0
self.calculation_total = self.NumE.get()
z = int(_remove_element_name(self.Z.get()))
def monitor(a, b):
# Will be executed in calculation thread. Values are only collected,
# Tk must be updated from main thread only.
self.calculation_count = a
self.calculation_total = b
if self.abort_calculation:
self.queue_calculation.put(False)
exit(1)
def callback(): # Carry the calculation in a different thread to avoid blocking
try:
s = xg.calculate_spectrum(self.E0.get(), self.Theta.get(), self.EMin.get(), self.NumE.get(), phi=self.Phi.get(), epsrel=self.Eps.get(), monitor=monitor, z=z)
self.spectra = [s]
self.queue_calculation.put(True)
except Exception as e:
print_exc()
self.queue_calculation.put(False)
messagebox.showerror("Error", "An error occurred during the calculation:\n%s\nCheck the parameters are valid."%str(e))
self.queue_calculation = queue.Queue(maxsize=1)
# The child will fill the queue with a value indicating whether an error occured.
self.abort_calculation = False # Ask the calculation thread to end (when monitor is executed)
self.calc_thread = threading.Thread(target=callback)
self.calc_thread.setDaemon(True)
self.calc_thread.start()
self.cmdCalculate["text"] = "Abort"
self.after(250, self.wait_for_calculation)
[docs] def wait_for_calculation(self):
"""
Polling method to wait for the calculation thread to finish. Also updates monitor_bar.
"""
self.monitor_bar(self.calculation_count, self.calculation_total)
if self.queue_calculation.full(): # Calculation ended
self.cmdCalculate["text"] = "Calculate"
self.cmdCalculate["state"] = "normal"
self.monitor_bar(0, 0)
if self.queue_calculation.get_nowait(): # Calculation ended successfully
self.lstHistory.delete(0, END)
self.lstHistory.insert(END, "Calculated")
self.enable_analyze_buttons()
self.active_spec = 0
self.update_plot()
self.select(1) # Open analyse tab
else:
pass
else:
self.after(250, self.wait_for_calculation)
[docs] def attenuate(self):
"""
Attenuate the active spectrum according to the parameters in the GUI.
"""
s2 = self.spectra[-1].clone()
s2.attenuate(self.AttenThick.get(),
xg.get_mu(_remove_element_name(self.AttenMaterial.get())))
self.spectra.append(s2)
self.lstHistory.insert(
END, "Attenuated: " + str(self.AttenThick.get()) + "cm of " + self.AttenMaterial.get())
self.lstHistory.selection_clear(0, len(self.spectra) - 2)
self.lstHistory.selection_set(len(self.spectra) - 1)
self.update_plot()
pass
[docs] def normalize(self):
"""
Normalize the active spectrum according to the parameters in the GUI.
"""
value = self.NormValue.get()
crit = self.NormCriterion.get()
if value <= 0:
messagebox.showerror("Error", "The norm of a spectrum must be a positive number.")
return
if crit not in self.criteriaList:
messagebox.showerror("Error", "An unknown criterion was selected.")
return
s2 = self.spectra[-1].clone()
if crit == self.criteriaList[0]:
s2.set_norm(value)
elif crit == self.criteriaList[1]:
s2.set_norm(value, lambda x: x)
else: # criteriaList[2]
s2.set_norm(value, self.fluence_to_dose)
self.spectra.append(s2)
self.lstHistory.insert(
END, "Normalized: " + crit + " = " + str(value))
self.lstHistory.selection_clear(0, len(self.spectra) - 2)
self.lstHistory.selection_set(len(self.spectra) - 1)
self.update_plot()
pass
[docs]def main():
"""
Start an instance of the GUI.
"""
root = Tk()
app = XpecgenGUI(master=root)
app.mainloop()
if __name__ == "__main__":
main()