1

I know that several questions have been created with people asking about non-responsive GUIs and the ultimate answer is that Tkinter is not thread safe. However, it is my understanding that queues can be utilized to overcome this problem. Therefore, I have been looking into using the multiprocessing module with queues such that my code can be utilized on hyperthreaded and multicore systems.
What I would like to do is to try and do a very complex least squares fitting of multiple imported spectra in different tabs whenever a button is pressed. The problem is that my code is still hanging up on the long process that I initialize by a button in my GUI. I have knocked the code down to something that still may run and has most of the objects of my original program, yet still suffers from the problem of not being responsive. I believe my problem is in the multiprocessing portion of my program.

Therefore my question is regarding the multiprocessing portion of the code and if there is a better way to organize the process_spectra() function shown here:

def process_spectra(self):
process_list = []
queue = mp.Queue()
for tab in self.tab_list:
    process_list.append(mp.Process(target=Deconvolution(tab).deconvolute(), args=(queue,)))
    process_list[-1].start()
    process_list[-1].join()
return

At the moment it appears that this is not actually making the deconvolution process into a different thread. I would like the process_spectra function to process all of the spectra with the deconvolution function simultaneously while still being able to interact with and see the changes in the spectra and GUI.

Here is the full code which can be run as a .py file directly to reproduce my problem:

from Tkinter import *
import Tkinter
import tkFileDialog
import matplotlib
from matplotlib import *
matplotlib.use('TKAgg')
from matplotlib import pyplot, figure, backends
import numpy as np
import lmfit
import multiprocessing as mp

# lots of different peaks can appear
class peak:
    def __init__(self, n, m):
        self.n = n
        self.m = m
    def location(self, i):
        location = i*self.m/self.n
        return location
    def NM(self):
        return str(self.n) + str(self.m)

# The main function that is given by the user has X and Y data and peak data
class Spectra:
    def __init__(self, spectra_name, X, Y):
        self.spectra_name = spectra_name
        self.X = X
        self.Y = Y
        self.Y_model = Y*0
        self.Y_background_model = Y*0
        self.Y_without_background_model = Y*0
        self.dYdX = np.diff(self.Y)/np.diff(self.X)

        self.peak_list = self.initialize_peaks(3, 60)

        self.params = lmfit.Parameters()

    def peak_amplitude_dictionary(self):
        peak_amplitude_dict = {}
        for peak in self.peak_list:
            peak_amplitude_dict[peak] = self.params['P' + peak.NM() + '_1_amp'].value
        return peak_amplitude_dict

    def peak_percentage_dictionary(self):
        peak_percentage_dict = {}
        for peak in self.peak_list:
            peak_percentage_dict[peak] = self.peak_amplitude_dictionary()[peak]/np.sum(self.peak_amplitude_dictionary().values())
        return peak_percentage_dict

    # Function to create all of the peaks and store them in a list
    def initialize_peaks(self, lowestNM, highestNM):
        peaks=[]
        for n in range(0,highestNM+1):
            for m in range(0,highestNM+1):
                if(n<lowestNM and m<lowestNM): break
                elif(n<m): break
                else: peaks.append(peak(n,m))
        return peaks
# This is just a whole bunch of GUI stuff      
class Spectra_Tab(Frame):
    def __init__(self, parent, spectra):
        self.spectra = spectra
        self.parent = parent
        Frame.__init__(self, parent)
        self.tab_name = spectra.spectra_name

        self.canvas_frame = Frame(self, bd=3, bg= 'WHITE', relief=SUNKEN)
        self.canvas_frame.pack(side=LEFT, fill=BOTH, padx=0, pady=0, expand=1)
        self.results_frame = Frame(self, bd=3, bg= 'WHITE', relief=SUNKEN, width=600)
        self.results_frame.pack(side=RIGHT, fill=BOTH, padx=0, pady=0, expand=1)

        self.top_canvas_frame = Frame(self.canvas_frame, bd=0, bg= 'WHITE', relief=SUNKEN)
        self.top_canvas_frame.pack(side=TOP, fill=BOTH, padx=0, pady=0, expand=1)

        self.original_frame = Frame(self.top_canvas_frame, bd=1, relief=SUNKEN)
        self.original_frame.pack(side=LEFT, fill=BOTH, padx=0, pady=0, expand=1)

        self.scrollbar = Scrollbar(self.results_frame)
        self.scrollbar.pack(side=RIGHT, fill=BOTH,expand=1)
        self.sidebar = Listbox(self.results_frame)
        self.sidebar.pack(fill=BOTH, expand=1)
        self.sidebar.config(yscrollcommand=self.scrollbar.set)
        self.scrollbar.config(command=self.sidebar.yview)

        self.original_fig = figure.Figure()
        self.original_plot = self.original_fig.add_subplot(111)

        init_values = np.zeros(len(self.spectra.Y))
        self.original_line, = self.original_plot.plot(self.spectra.X, self.spectra.Y, 'r-')
        self.original_background_line, = self.original_plot.plot(self.spectra.X, init_values, 'k-', animated=True)

        self.original_canvas = backends.backend_tkagg.FigureCanvasTkAgg(self.original_fig, master=self.original_frame)
        self.original_canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=1)
        self.original_canvas._tkcanvas.pack(side=TOP, fill=BOTH, expand=1)
        self.original_canvas.show()
        self.original_canvas.draw()

        self.original_canvas_BBox = self.original_plot.figure.canvas.copy_from_bbox(self.original_plot.bbox)

        ax1 = self.original_plot.figure.axes[0]
        ax1.set_xlim(self.spectra.X.min(), self.spectra.X.max())
        ax1.set_ylim(0, self.spectra.Y.max() + .05*self.spectra.Y.max())

        self.step=0
        self.update()
    # This just refreshes the GUI stuff everytime that the parameters are fit in the least squares method
    def refreshFigure(self):
        self.step=self.step+1
        if(self.step==1):
            self.original_canvas_BBox = self.original_plot.figure.canvas.copy_from_bbox(self.original_plot.bbox)

        self.original_plot.figure.canvas.restore_region(self.original_canvas_BBox)

        self.original_background_line.set_data(self.spectra.X, self.spectra.Y_background_model)

        self.original_plot.draw_artist(self.original_line)
        self.original_plot.draw_artist(self.original_background_line)
        self.original_plot.figure.canvas.blit(self.original_plot.bbox)
        # show percentage of peaks on the side bar
        self.sidebar.delete(0, Tkinter.END)
        peak_dict = self.spectra.peak_percentage_dictionary()
        for peak in sorted(peak_dict.iterkeys()):
            self.sidebar.insert(0, peak.NM() + '        ' + str(peak_dict[peak]) + '%' )
        return
# just a tab bar 
class TabBar(Frame):
    def __init__(self, master=None):
        Frame.__init__(self, master)
        self.tabs = {}
        self.buttons = {}
        self.current_tab = None
    def show(self):
        self.pack(side=BOTTOM, expand=0, fill=X)
    def add(self, tab):
        tab.pack_forget()
        self.tabs[tab.tab_name] = tab
        b = Button(self, text=tab.tab_name, relief=RAISED, command=(lambda name=tab.tab_name: self.switch_tab(name)))
        b.pack(side=LEFT)
        self.buttons[tab.tab_name] = b
    def switch_tab(self, name):
        if self.current_tab:
            self.buttons[self.current_tab].config(relief=RAISED)
            self.tabs[self.current_tab].pack_forget()
        self.tabs[name].pack(side=BOTTOM)
        self.current_tab = name
        self.buttons[name].config(relief=SUNKEN)

class Deconvolution:
    def __init__(self, spectra_tab):
        self.spectra_tab = spectra_tab
        self.spectra = spectra_tab.spectra

        self.model = [0 for x in self.spectra.X]
        self.model_without_background = [0 for x in self.spectra.X]
        self.residual_array = [0 for x in self.spectra.X]

        # Amplitudes for backgrounds
        self.pi_plasmon_amp = np.interp(4.3, self.spectra.X, self.spectra.Y)
        self.graphite_amp = np.interp(5, self.spectra.X, self.spectra.Y)

        self.spectra.params.add('PPAmp', value=self.pi_plasmon_amp, vary=True, min=0.0, max=None)
        self.spectra.params.add('PPCenter', value=4.3, vary=True)
        self.spectra.params.add('PPFWHM', value=.4, vary=True)
        self.spectra.params.add('GLAmp', value=self.graphite_amp, vary=True, min=0.0, max=None)
        self.spectra.params.add('GLCenter', value=5, vary=True)
        self.spectra.params.add('GLFWHM', value=.4, vary=True)

        self.background_model = self.pseudoVoigt(self.spectra.X, self.spectra.params['PPAmp'].value, self.spectra.params['PPCenter'].value, self.spectra.params['PPFWHM'].value, 1)+\
                                self.pseudoVoigt(self.spectra.X, self.spectra.params['GLAmp'].value, self.spectra.params['GLCenter'].value, self.spectra.params['GLFWHM'].value, 1)

        for peak in self.spectra.peak_list:
            for i in range(1,4):
                param_prefix = 'P' + peak.NM() + '_' + str(i)
                center = peak.location(i)
                amp = np.interp(center, self.spectra.X, self.spectra.Y - self.background_model)
                width = 0.02
                self.spectra.params.add(param_prefix + '_amp', value = 0.8*amp, vary=False, min=0.0, max=None)
                self.spectra.params.add(param_prefix + '_center', value = center, vary=False, min=0.0, max=None)
                self.spectra.params.add(param_prefix + '_width', value = width, vary=False, min=0.0, max=None)
                self.model_without_background += self.pseudoVoigt(self.spectra.X, self.spectra.params[param_prefix + '_amp'].value, self.spectra.params[param_prefix + '_center'].value, self.spectra.params[param_prefix + '_width'].value, 1)

    def deconvolute(self):
        for State in range(0,3):
            # Make each voigt profile for each tube
            for peak in self.spectra.peak_list:
                for i in range(1,4):
                    param_prefix = 'P' + peak.NM() + '_' + str(i)
                    if(State==1):
                        self.spectra.params[param_prefix + '_amp'].vary = True
                    if(State==2):
                        self.spectra.params[param_prefix + '_width'].vary = True

            result = lmfit.Minimizer(self.residual, self.spectra.params, fcn_args=(State,))
            result.prepare_fit()
            result.leastsq()#lbfgsb()

    def residual(self, params, State):
        self.model = self.background_model
        if(State>0):
            self.model += self.model_without_background
        for x in range(0, len(self.spectra.X)):
            if(self.background_model[x]>self.spectra.Y[x]):
                self.residual_array[x] = -999999.-9999.*(self.spectra.Y[x]-self.background_model[x])
            else:
                self.residual_array[x] = self.spectra.Y[x]-self.model[x]
        self.spectra.Y_model = self.model
        self.spectra.Y_background_model = self.background_model
        self.spectra.Y_without_background_model = self.model_without_background
        self.spectra_tab.refreshFigure()
        return self.residual_array

    def pseudoVoigt(self, x, amp, center, width, shapeFactor):
        LorentzPortion = (width**2/((x-center)**2+width**2))
        GaussianPortion = 1/(np.sqrt(2*np.pi*width**2))*np.e**(-(x-center)**2/(2*width**2))
        try:
            Voigt = amp*(shapeFactor*LorentzPortion+(1-shapeFactor)*GaussianPortion)
        except ZeroDivisionError:
            width = width+0.01
            LorentzPortion = (width**2/((x-center)**2+width**2))
            GaussianPortion = 1/(np.sqrt(2*np.pi*width**2))*np.e**(-(x-center)**2/(2*width**2))
            Voigt = amp*(shapeFactor*LorentzPortion+(1-shapeFactor)*GaussianPortion)
        return Voigt

class MainWindow(Tk):
    def __init__(self, parent):
        Tk.__init__(self, parent)
        self.parent = parent
        self.wm_state('zoomed')
        self.spectra_list = []
        self.tab_list = []
        self.button_frame = Frame(self, bd=3, relief=SUNKEN)
        self.button_frame.pack(side=TOP, fill=BOTH)
        self.tab_frame = Frame(self, bd=3, relief=SUNKEN)
        self.tab_frame.pack(side=BOTTOM, fill=BOTH, expand=1)
        open_spectra_button = Button(self.button_frame, text='open spectra', command=self.open_spectra)
        open_spectra_button.pack(side=LEFT, fill=Y)
        process_spectra_button = Button(self.button_frame, text='process spectra', command=self.process_spectra)
        process_spectra_button.pack(side=LEFT, fill=Y)
        self.tab_bar = TabBar(self.tab_frame)
        self.tab_bar.show()
        self.resizable(True,False)
        self.update()
    def open_spectra(self):
        # This will prompt user for file input later, but here is an example
        file_name_list = ['spectra_1', 'spectra_2']
        for file_name in file_name_list:
            # Just make up functions that may be imported
            X_values = np.arange(1240.0/1350.0, 1240./200., 0.01)
            if(file_name=='spectra_1'):
                Y_values = np.array(np.e**.2*X_values + np.sin(10*X_values)+np.cos(4*X_values))
            if(file_name=='spectra_2'):
                Y_values = np.array(np.e**.2*X_values + np.sin(10*X_values)+np.cos(3*X_values)+.3*np.cos(.5*X_values))
            self.spectra_list.append(Spectra(file_name, X_values, Y_values))
            self.tab_list.append(Spectra_Tab(self.tab_frame, self.spectra_list[-1]))
            self.tab_bar.add(self.tab_list[-1])
        self.tab_bar.switch_tab(self.spectra_list[0].spectra_name)
        self.tab_bar.show()
        return  
    def process_spectra(self):
        process_list = []
        queue = mp.Queue()
        for tab in self.tab_list:
            process_list.append(mp.Process(target=Deconvolution(tab).deconvolute(), args=(queue,)))
            process_list[-1].start()
            process_list[-1].join()
        return
if __name__ == "__main__":
    root = MainWindow(None)
    root.mainloop()

EDIT: I am editing this question because I realized that my question did not regard the real problem. I think the code I have supplied has problems with having a Tkinter Frame passed as a parameter to something that needs to be pickled, ? and it can't because it's not thread safe?? It gives a pickle error that points to Tkinter in some way.
However, I am not sure how to reorganize this code such that the only part that is pickled is the data part since the threads or processes must access the Tkinter frames in order to update them via refreshFigure().

Does anyone have any ideas regarding how to do this? I have researched it but everyone's examples are usually simple with only one figure or that only refreshes after the process is completed.

chase
  • 3,592
  • 8
  • 37
  • 58

1 Answers1

2

The segment target=Deconvolution(tab).deconvolute() will actually be evaluated instead of passed to a subprocess. You could replace this with a wrapper function

def mp_deconvolute(tab):
    return Deconvolution(tab).deconvolute()

I'm not sure if your queue is actually be used at all but I believe that would be more appropriate for a worker Pool scenario.

Edit:

Oh, and you would call it like so

process_list.append(mp.Process(target=mp_deconvolute, args=(tab)))

Edit again:

You could just define that as a lambda function too unless you to to add more complexity

mp_deconv = lambda x: Deconvolution(tab).deconvolute()
process_list.append(mp.Process(target=mp_deconv, args=(tab)))
m.brindley
  • 1,218
  • 10
  • 19
  • So is it a reflection of bad architecture of my code if adding the lambda function in that way gives me a pickle error? `PicklingError: Can't pickle at 0x03ECDA70>: it's not found as __main__.` – chase Feb 04 '13 at 07:11
  • 1
    Lambdas can't be pickled - using the named function would be fine in this case. You can apparently use `marshal` to serialize lambdas and their closures but I've never tried. – m.brindley Feb 04 '13 at 07:19
  • Oh ok, I didn't realize that lamdas and named functions had any real difference other than syntax. I tried the named function but still ended up with an error. However, This has helped me because I think I have found several other errors in my code from this. – chase Feb 04 '13 at 17:12
  • Apparently the code was too Object Oriented according to [this](http://stackoverflow.com/questions/1816958/cant-pickle-type-instancemethod-when-using-pythons-multiprocessing-pool-ma). Also, I think there may be a problem using a Tkinter Frame as a variable? I say this because after making the deconvolution just a function (not within the Deconvolution object) I get `PicklingError: Can't pickle 'tkapp' object: ` – chase Feb 04 '13 at 17:21