Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MxMap error when running the same scan twice in a thread or process #1

Open
jbhopkins opened this issue Jul 16, 2018 · 1 comment
Open

Comments

@jbhopkins
Copy link
Contributor

Exception in thread Thread-1:
Traceback (most recent call last):
File "/home/biocat/miniconda2/lib/python2.7/threading.py", line 801, in __bootstrap_inner
self.run()
File "/home/biocat/miniconda2/lib/python2.7/threading.py", line 754, in run
self.__target(*self.__args, **self.__kwargs)
File "mxmap/utils/Scanner.py", line 76, in mx_main_loop
self.performScan()
File "mxmap/utils/Scanner.py", line 90, in performScan
all_records = [r.name for r in self.mx_database.get_all_records()]
File "/opt/mx/lib/mp/Mp.py", line 570, in get_all_records
while ( current_record.name != list_head_name ):
AttributeError: 'list' object has no attribute 'name'

@jbhopkins
Copy link
Contributor Author

Here's a not so minimal example that recreates the problem:

#! /usr/bin/env python
# coding: utf-8
#
#    Project: BioCAT staff beamline control software (CATCON)
#             https://github.com/biocatiit/beamline-control-staff
#
#
#    Principal author:       Jesse Hopkins
#
#    This is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This software is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this software.  If not, see <http://www.gnu.org/licenses/>.

from __future__ import absolute_import, division, print_function, unicode_literals
from builtins import object, range, map
from io import open

import queue
import tempfile
import os
import math
import threading
import time
import multiprocessing

import wx
import matplotlib
matplotlib.rcParams['backend'] = 'WxAgg'
from matplotlib.backends.backend_wxagg import NavigationToolbar2WxAgg
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg
import matplotlib.gridspec as gridspec

def get_mxdir():
    """Gets the top level install directory for MX."""
    try:
        mxdir = os.environ["MXDIR"]
    except:
        mxdir = "/opt/mx"   # This is the default location.

    return mxdir

def get_mpdir():
    """Construct the name of the Mp modules directory."""
    mxdir = get_mxdir()

    mp_modules_dir = os.path.join(mxdir, "lib", "mp")
    mp_modules_dir = os.path.normpath(mp_modules_dir)

    return mp_modules_dir

def set_mppath():
    """Puts the mp directory in the system path, if it isn't already."""
    path = os.environ['PATH']

    mp_dir = get_mpdir()

    if mp_dir not in path:
        os.environ["PATH"] = mp_dir+os.pathsep+os.environ["PATH"]

set_mppath()
import Mp as mp

def file_follow(the_file, stop_event):
    """
    This function follows a file that is continuously being written to and
    provides a generator that gives each new line written into the file.

    Modified from: http://www.dabeaz.com/generators/follow.py

    :param file the_file: The file object to read lines from.
    :param threading.Event stop_event: A stop event that will end the generator,
        allowing any loops iterating on the generator to exit.
    """
    while True:
        if stop_event.is_set():
            break
        line = the_file.readline()
        if not line:
            time.sleep(0.001)
            continue
        yield line


class ScanProcess(multiprocessing.Process):
    """
    This is a separate Process (as opposed to Thread) that runs the ``Mp``
    scan. It has to be a Process because even in a new Thread the scan
    eats all processing resources and essentially locks the GUI while it's
    running.
    """

    def __init__(self, command_queue, return_queue, abort_event):
        """
        Initializes the Process.

        :param multiprocessing.Manager.Queue command_queue: This queue is used
            to pass commands to the scan process.

        :param multiprocessing.Manager.Queue return_queue: This queue is used
            to return values from the scan process.

        :param multiprocessing.Manager.Event abort_event: This event is set when
            a scan needs to be aborted.
        """
        multiprocessing.Process.__init__(self)
        self.daemon = True

        self.command_queue = command_queue
        self.return_queue = return_queue
        self._abort_event = abort_event
        self._stop_event = multiprocessing.Event()

        mp.set_user_interrupt_function(self._stop_scan)

        self._commands = {'start_mxdb'      : self._start_mxdb,
                        'set_scan_params'   : self._set_scan_params,
                        'scan'              : self._scan,
                        }

    def run(self):
        """
        Runs the process. It waits for commands to show up in the command_queue,
        and then runs them. It is aborted if the abort_event is set. It is stopped
        when the stop_event is set, and that allows the process to end gracefully.
        """
        while True:
            try:
                cmd, args, kwargs = self.command_queue.get_nowait()
            except queue.Empty:
                cmd = None

            if self._abort_event.is_set():
                self._abort()
                cmd = None

            if self._stop_event.is_set():
                self._abort()
                break

            if cmd is not None:
                try:
                    self._commands[cmd](*args, **kwargs)
                except Exception as e:
                    print(e)

        if self._stop_event.is_set():
            self._stop_event.clear()
        else:
            self._abort()

    def _start_mxdb(self, db_path):
        """
        Starts the MX database

        :param str db_path: The path to the MX database.
        """
        self.db_path = db_path
        self.mx_database = mp.setup_database(self.db_path)
        self.mx_database.set_plot_enable(2)

    def _set_scan_params(self, device, start, stop, step, scalers,
        dwell_time, timer, detector=None, file_name=None, dir_path=None):
        """
        Sets the parameters for the scan.

        :param str device: The MX record name.
        :param float start: The absolute start position of the scan.
        :param float stop: The absolute stop position of the scan.
        :param float step: The step size of the scan.
        :param list scalers: A list of the scalers for the scan.
        :param float dwell_time: The count time at each point in the scan.
        :param str timer: The name of the timer to be used for the scan.
        :param str detector: Currently not used. The name of the detector
            to be used for the scan.
        :param str file_name: The scan name (and output name) for the scan.
            Currently not used.
        :param str dir_path: The directory path where the scan file will be
            saved. Currently not used.
        """
        self.out_path = dir_path
        self.out_name = file_name

        self.device = device
        self.start = start
        self.stop = stop
        self.step = step

        self.scalers = scalers
        self.dwell_time = dwell_time
        self.timer = timer
        self.detector = detector

    def _scan(self):
        """
        Constructs and MX scan record and then carries out the scan. It also
        communicates with the :mod:`ScanPanel` to send the filename for live
        plotting of the scan.
        """
        all_names = [r.name for r in self.mx_database.get_all_records()]

        if self.out_name is not None:
            scan_name = self.out_name
        else:
            scan_name = self.device
            i=1
            while scan_name in all_names:
                if i == 1:
                    scan_name = "{}_{}".format(scan_name, str(i).zfill(2))
                else:
                    scan_name = "{}_{}".format(scan_name[:-2], str(i).zfill(2))
                i=i+1

        description = ('{} scan linear_scan motor_scan "" "" '.format(scan_name))

        num_scans = 1
        num_motors = 1
        num_independent_variables = num_motors

        description = description + ("{} {} {} {} ".format(num_scans,
            num_independent_variables, num_motors, str(self.device)))

        scalers_detector = list(self.scalers)

        if self.detector is not None:
            scalers_detector.append(self.detector['name'])

        description = description + ("{} ".format(len(scalers_detector)))

        for j in range(len(scalers_detector)):
            description = description + ("{} ".format(scalers_detector[j]))

        scan_flags = 0x0
        settling_time = 0.0
        measurement_type = "preset_time"
        measurement_time = self.dwell_time

        description = description + (
                '%x %f %s "%f %s" ' % (scan_flags, settling_time, measurement_type, measurement_time, self.timer))

        if self.out_path is None:
            standard_paths = wx.StandardPaths.Get()
            tmpdir = standard_paths.GetTempDir()
            fname = tempfile.NamedTemporaryFile(dir=tmpdir).name
            fname=os.path.normpath('./{}'.format(os.path.split(fname)[-1]))
        else:
            fname = os.path.join(self.out_path, self.out_name)

        datafile_description = "sff"
        datafile_name = fname
        plot_description = "none"
        plot_arguments = "$f[0]"

        description = description + (
                "%s %s %s %s " % (datafile_description, datafile_name, plot_description, plot_arguments))

        description = description + ("{} {} ".format(self.start, self.step))

        num_measurements = int(abs(math.floor((self.stop - self.start)/self.step)))+1
        description = description + ("{} ".format(num_measurements))

        self.mx_database.create_record_from_description(description)

        scan = self.mx_database.get_record(scan_name)

        scan.finish_record_initialization()

        self.return_queue.put_nowait([datafile_name])

        scan.perform_scan()

        self.return_queue.put_nowait(['stop_live_plotting'])

    def _abort(self):
        """Clears the ``command_queue`` and aborts all current actions."""
        while True:
            try:
                self.command_queue.get_nowait()
            except queue.Empty:
                break

        self._abort_event.clear()

    def stop_thread(self):
        """Stops the thread cleanly."""
        self._stop_event.set()

    def _stop_scan(self):
        """
        This function is used Mp to abort the scan.

        :returns: Returns 1 if the abort event is set, and that causes Mp to
            abort the running scan. Returns 0 otherwise, which doesn't abort
            anything.
        :rtype: int
        """
        return int(self._abort_event.is_set())


class ScanThread(threading.Thread):
    """
    This is a separate Thread that runs the ``Mp``
    scan.
    """

    def __init__(self, command_queue, return_queue, abort_event):
        """
        Initializes the thread.
        """
        threading.Thread.__init__(self)
        self.daemon = True

        self.command_queue = command_queue
        self.return_queue = return_queue
        self._abort_event = abort_event
        self._stop_event = threading.Event()

        mp.set_user_interrupt_function(self._stop_scan)

        self._commands = {'start_mxdb'      : self._start_mxdb,
                        'set_scan_params'   : self._set_scan_params,
                        'scan'              : self._scan,
                        }

    def run(self):
        """
        Runs the thread. It waits for commands to show up in the command_queue,
        and then runs them. It is aborted if the abort_event is set. It is stopped
        when the stop_event is set, and that allows the process to end gracefully.
        """
        while True:
            try:
                cmd, args, kwargs = self.command_queue.get_nowait()
            except queue.Empty:
                cmd = None

            if self._abort_event.is_set():
                self._abort()
                cmd = None

            if self._stop_event.is_set():
                self._abort()
                break

            if cmd is not None:
                try:
                    self._commands[cmd](*args, **kwargs)
                except Exception as e:
                    print(e)

        if self._stop_event.is_set():
            self._stop_event.clear()
        else:
            self._abort()

    def _start_mxdb(self, db_path):
        """
        Starts the MX database

        :param str db_path: The path to the MX database.
        """
        self.db_path = db_path
        self.mx_database = mp.setup_database(self.db_path)
        self.mx_database.set_plot_enable(2)

    def _set_scan_params(self, device, start, stop, step, scalers,
        dwell_time, timer, detector=None, file_name=None, dir_path=None):
        """
        Sets the parameters for the scan.

        :param str device: The MX record name.
        :param float start: The absolute start position of the scan.
        :param float stop: The absolute stop position of the scan.
        :param float step: The step size of the scan.
        :param list scalers: A list of the scalers for the scan.
        :param float dwell_time: The count time at each point in the scan.
        :param str timer: The name of the timer to be used for the scan.
        :param str detector: Currently not used. The name of the detector
            to be used for the scan.
        :param str file_name: The scan name (and output name) for the scan.
            Currently not used.
        :param str dir_path: The directory path where the scan file will be
            saved. Currently not used.
        """
        self.out_path = dir_path
        self.out_name = file_name

        self.device = device
        self.start = start
        self.stop = stop
        self.step = step

        self.scalers = scalers
        self.dwell_time = dwell_time
        self.timer = timer
        self.detector = detector

    def _scan(self):
        """
        Constructs and MX scan record and then carries out the scan. It also
        communicates with the :mod:`ScanPanel` to send the filename for live
        plotting of the scan.
        """
        all_names = [r.name for r in self.mx_database.get_all_records()]

        if self.out_name is not None:
            scan_name = self.out_name
        else:
            scan_name = self.device
            i=1
            while scan_name in all_names:
                if i == 1:
                    scan_name = "{}_{}".format(scan_name, str(i).zfill(2))
                else:
                    scan_name = "{}_{}".format(scan_name[:-2], str(i).zfill(2))
                i=i+1

        description = ('{} scan linear_scan motor_scan "" "" '.format(scan_name))

        num_scans = 1
        num_motors = 1
        num_independent_variables = num_motors

        description = description + ("{} {} {} {} ".format(num_scans,
            num_independent_variables, num_motors, str(self.device)))

        scalers_detector = list(self.scalers)

        if self.detector is not None:
            scalers_detector.append(self.detector['name'])

        description = description + ("{} ".format(len(scalers_detector)))

        for j in range(len(scalers_detector)):
            description = description + ("{} ".format(scalers_detector[j]))

        scan_flags = 0x0
        settling_time = 0.0
        measurement_type = "preset_time"
        measurement_time = self.dwell_time

        description = description + (
                '%x %f %s "%f %s" ' % (scan_flags, settling_time, measurement_type, measurement_time, self.timer))

        if self.out_path is None:
            standard_paths = wx.StandardPaths.Get()
            tmpdir = standard_paths.GetTempDir()
            fname = tempfile.NamedTemporaryFile(dir=tmpdir).name
            fname=os.path.normpath('./{}'.format(os.path.split(fname)[-1]))
        else:
            fname = os.path.join(self.out_path, self.out_name)

        datafile_description = "sff"
        datafile_name = fname
        plot_description = "none"
        plot_arguments = "$f[0]"

        description = description + (
                "%s %s %s %s " % (datafile_description, datafile_name, plot_description, plot_arguments))

        description = description + ("{} {} ".format(self.start, self.step))

        num_measurements = int(abs(math.floor((self.stop - self.start)/self.step)))+1
        description = description + ("{} ".format(num_measurements))

        self.mx_database.create_record_from_description(description)

        scan = self.mx_database.get_record(scan_name)

        scan.finish_record_initialization()

        self.return_queue.put_nowait([datafile_name])

        scan.perform_scan()

        self.return_queue.put_nowait(['stop_live_plotting'])

    def _abort(self):
        """Clears the ``command_queue`` and aborts all current actions."""
        while True:
            try:
                self.command_queue.get_nowait()
            except queue.Empty:
                break

        self._abort_event.clear()

    def stop_thread(self):
        """Stops the thread cleanly."""
        self._stop_event.set()

    def _stop_scan(self):
        """
        This function is used Mp to abort the scan.

        :returns: Returns 1 if the abort event is set, and that causes Mp to
            abort the running scan. Returns 0 otherwise, which doesn't abort
            anything.
        :rtype: int
        """
        return int(self._abort_event.is_set())

class ScanPanel(wx.Panel):
    """
    This creates the scan panel with both scan controls and the live plot.
    """
    def __init__(self, *args, **kwargs):
        """
        Initializes the scan panel. Accepts the usual wx.Panel arguments
        """
        wx.Panel.__init__(self, *args, **kwargs)

        self.scalers = ['Io']
        self.timer = 'timer1'
        self.device_name = 'mtr1'
        self.use_thread = False

        if self.use_thread:
            self.cmd_q = queue.Queue()
            self.return_q = queue.Queue()
            self.abort_event = threading.Event()
            self.scan_proc = ScanThread(self.cmd_q, self.return_q, self.abort_event)
        else:
            self.manager = multiprocessing.Manager()
            self.cmd_q = self.manager.Queue()
            self.return_q = self.manager.Queue()
            self.abort_event = self.manager.Event()
            self.scan_proc = ScanProcess(self.cmd_q, self.return_q, self.abort_event)

        self.scan_proc.start()

        self.scan_timer = wx.Timer()
        self.scan_timer.Bind(wx.EVT_TIMER, self._on_scantimer)

        self.live_plt_evt = threading.Event()

        self.Bind(wx.EVT_CLOSE, self._on_closewindow)

        self.plt_line = None
        self.plt_y = None
        self.plt_x = None

        self._create_layout()

        self._start_scan_mxdb()

    def _create_layout(self):
        dname = wx.StaticText(self, label=self.device_name)
        info_grid = wx.FlexGridSizer(rows=2, cols=2, vgap=5, hgap=5)
        info_grid.Add(wx.StaticText(self, label='Device name:'))
        info_grid.Add(dname)

        info_sizer = wx.StaticBoxSizer(wx.StaticBox(self, label='Info'),
            wx.VERTICAL)
        info_sizer.Add(info_grid, wx.EXPAND)

        self.start = wx.TextCtrl(self, value='', size=(80, -1))
        self.stop = wx.TextCtrl(self, value='', size=(80, -1))
        self.step = wx.TextCtrl(self, value='', size=(80, -1))
        self.count_time = wx.TextCtrl(self, value='0.1')

        type_sizer =wx.BoxSizer(wx.HORIZONTAL)
        type_sizer.Add(wx.StaticText(self, label='Scan type:'))
        type_sizer.Add(wx.StaticText(self,label='Absolute'), border=5, flag=wx.LEFT)

        mv_grid = wx.FlexGridSizer(rows=2, cols=3, vgap=5, hgap=5)
        mv_grid.Add(wx.StaticText(self, label='Start'))
        mv_grid.Add(wx.StaticText(self, label='Stop'))
        mv_grid.Add(wx.StaticText(self, label='Step'))
        mv_grid.Add(self.start)
        mv_grid.Add(self.stop)
        mv_grid.Add(self.step)


        count_grid = wx.FlexGridSizer(rows=4, cols=2, vgap=5, hgap=5)
        count_grid.Add(wx.StaticText(self, label='Count time (s):'))
        count_grid.Add(self.count_time)
        count_grid.AddGrowableCol(1)

        self.start_btn = wx.Button(self, label='Start')
        self.start_btn.Bind(wx.EVT_BUTTON, self._on_start)

        self.stop_btn = wx.Button(self, label='Stop')
        self.stop_btn.Bind(wx.EVT_BUTTON, self._on_stop)
        self.stop_btn.Disable()

        ctrl_btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
        ctrl_btn_sizer.Add(self.start_btn)
        ctrl_btn_sizer.Add(self.stop_btn, border=5, flag=wx.LEFT)

        ctrl_sizer = wx.StaticBoxSizer(wx.StaticBox(self, label='Scan Controls'),
            wx.VERTICAL)
        ctrl_sizer.Add(type_sizer)
        ctrl_sizer.Add(mv_grid, border=5, flag=wx.EXPAND|wx.TOP)
        ctrl_sizer.Add(count_grid, border=5, flag=wx.EXPAND|wx.TOP)
        ctrl_sizer.Add(ctrl_btn_sizer, border=5, flag=wx.ALIGN_CENTER_HORIZONTAL|wx.TOP)


        scan_sizer = wx.BoxSizer(wx.VERTICAL)
        scan_sizer.Add(info_sizer, flag=wx.EXPAND)
        scan_sizer.Add(ctrl_sizer, flag=wx.EXPAND)

        self.fig = matplotlib.figure.Figure()
        self.canvas = FigureCanvasWxAgg(self, -1, self.fig)
        self.canvas.SetBackgroundColour('white')
        self.toolbar = NavigationToolbar2WxAgg(self.canvas)
        self.toolbar.Realize()

        self.plt_gs = gridspec.GridSpec(1, 1)

        self.plot = self.fig.add_subplot(self.plt_gs[0], title='Scan')
        self.plot.set_ylabel('Scaler counts')
        self.plot.set_xlabel('Position')

        self.cid = self.canvas.mpl_connect('draw_event', self._ax_redraw)

        plot_sizer = wx.BoxSizer(wx.VERTICAL)
        plot_sizer.Add(self.canvas, 1, flag=wx.EXPAND)
        plot_sizer.Add(self.toolbar, 0, flag=wx.EXPAND)

        top_sizer = wx.BoxSizer(wx.HORIZONTAL)
        top_sizer.Add(scan_sizer)
        top_sizer.Add(plot_sizer, 1, flag=wx.EXPAND)

        self.SetSizer(top_sizer)


    def _on_start(self, evt):
        """
        Called when the start scan button is pressed. It gets the scan
        parameters and then puts the scan in the ``cmd_q``.
        """
        self.start_btn.Disable()
        self.stop_btn.Enable()
        scan_params = self._get_params()

        if scan_params is not None:
            if scan_params['start'] < scan_params['stop']:
                self.plot.set_xlim(scan_params['start'], scan_params['stop'])
            else:
                self.plot.set_xlim(scan_params['stop'], scan_params['start'])

            self.scan_timer.Start(10)

            self.cmd_q.put_nowait(['set_scan_params', [], scan_params])
            self.cmd_q.put_nowait(['scan', [], {}])

    def _on_stop(self, evt):
        """
        Called when the stop button is pressed. Aborts the scan and live
        plotting
        """
        self.abort_event.set()
        time.sleep(0.5) #Wait for the process to abort before trying to reload the db
        self.return_q.put_nowait(['stop_live_plotting'])

    def _get_params(self):
        """
        Gets the scan parameters from the GUI and returns them.

        :returns: A dictionary of the scan parameters.
        :rtype: dict
        """
        try:
            start = float(self.start.GetValue())
            stop = float(self.stop.GetValue())

            if start < stop:
                step = abs(float(self.step.GetValue()))
            else:
                step = -abs(float(self.step.GetValue()))
            scan_params = {'device'     : self.device_name,
                        'start'         : start,
                        'stop'          : stop,
                        'step'          : step,
                        'scalers'       : self.scalers,
                        'dwell_time'    : float(self.count_time.GetValue()),
                        'timer'         : self.timer,
                        'detector'      : 'None'
                        }
        except ValueError:
            msg = 'All of start, stop, step, and count time must be numbers.'
            wx.MessageBox(msg, "Failed to start scan", wx.OK)
            return None

        if scan_params['detector'] == 'None':
            scan_params['detector'] = None

        return scan_params

    def _start_scan_mxdb(self):
        """Loads the mx database in the scan process"""
        mxdir = get_mxdir()
        database_filename = os.path.join(mxdir, "etc", "mxmotor.dat")
        database_filename = os.path.normpath(database_filename)
        self.cmd_q.put_nowait(['start_mxdb', [database_filename], {}])

    def _on_scantimer(self, evt):
        """
        Called while the scan is running. It starts the live plotting at the
        start of the scan, and stops the live plotting at the end, and enables/disables
        the start/stop buttons as appropriate.
        """
        try:
            scan_return = self.return_q.get_nowait()[0]
        except queue.Empty:
            scan_return = None

        if scan_return is not None and scan_return != 'stop_live_plotting':
            print('starting live plotting')
            self.live_plt_evt.clear()
            self.live_thread = threading.Thread(target=self.live_plot, args=(scan_return,))
            self.live_thread.daemon = True
            self.live_thread.start()
        elif scan_return == 'stop_live_plotting':
            self.scan_timer.Stop()
            self.live_plt_evt.set()
            #This is a hack
            # self.scan_proc.stop_thread()
            # if self.use_thread:
            #     self.scan_proc = ScanThread(self.cmd_q, self.return_q, self.abort_event)
            # else:
            #     self.scan_proc = ScanProcess(self.cmd_q, self.return_q, self.abort_event)
            # self.scan_proc.start()
            # self._start_scan_mxdb()

            self.start_btn.Enable()
            self.stop_btn.Disable()

    def _ax_redraw(self, widget=None):
        """Redraw plots on window resize event."""

        self.background = self.canvas.copy_from_bbox(self.plot.bbox)

        self.update_plot()

    def _safe_draw(self):
        """A safe draw call that doesn 't endlessly recurse."""
        self.canvas.mpl_disconnect(self.cid)
        self.canvas.draw()
        self.cid = self.canvas.mpl_connect('draw_event', self._ax_redraw)

    def update_plot(self):
        """
        Updates the plot.
        """
        get_plt_bkg = False

        if self.plt_line is None:
            if (self.plt_x is not None and self.plt_y is not None and
                len(self.plt_x) == len(self.plt_y)) and len(self.plt_x) > 0:

                self.plt_line, = self.plot.plot(self.plt_x, self.plt_y, 'b-o', animated=True)
                get_plt_bkg = True

        if get_plt_bkg:
            self._safe_draw()
            self.background = self.canvas.copy_from_bbox(self.plot.bbox)

        if self.plt_line is not None:
            self.plt_line.set_xdata(self.plt_x)
            self.plt_line.set_ydata(self.plt_y)

        redraw = False

        if self.plt_line is not None:
            oldx = self.plot.get_xlim()
            oldy = self.plot.get_ylim()

            self.plot.relim()
            self.plot.autoscale_view()

            newx = self.plot.get_xlim()
            newy = self.plot.get_ylim()

            if newx != oldx or newy != oldy:
                redraw = True

        if redraw:
            self.canvas.mpl_disconnect(self.cid)
            self.canvas.draw()
            self.cid = self.canvas.mpl_connect('draw_event', self._ax_redraw)

        if self.plt_line is not None:
            self.canvas.restore_region(self.background)
            self.plot.draw_artist(self.plt_line)

        self.canvas.blit(self.plot.bbox)

    def live_plot(self, filename):
        """
        This does the live plotting. It is intended to be run in its own
        thread. It first clears all of the plot related variables and clears
        the plot. It then enters a loop where it reads from the scan file
        and plots the points as they come in, until the scan ends.

        :param str filename: The filename of the scan file to live plot.
        """
        print('live plot thread started')
        if self.plt_line is not None:
            wx.CallAfter(self.plt_line.remove)
            self.plt_line = None
            wx.Callafter(self.plt_pts.remove)
            self.plt_pts = None

        self.plt_x = []
        self.plt_y = []

        self.scan_header = ''

        wx.CallAfter(self.update_plot) #Is this threadsafe?
        wx.Yield()

        if not os.path.exists(filename):
            time.sleep(0.1)

        with open(filename) as thefile:
            print('opening scan output datafile')
            data = file_follow(thefile, self.live_plt_evt)
            for val in data:
                print('got new scan value: %s' %(val))
                if self.live_plt_evt.is_set():
                    break
                if val.startswith('#'):
                    self.scan_header = self.scan_header + val
                else:
                    x, y = val.strip().split()
                    self.plt_x.append(float(x))
                    self.plt_y.append(float(y))

                    wx.CallAfter(self.update_plot) #Is this threadsafe?
                    wx.Yield()

        os.remove(filename)


    def _on_closewindow(self, event):
        """
        Called when the scan window is closed.
        """
        self.scan_timer.Stop()
        self.scan_proc.stop_thread()

        while self.scan_proc.is_alive():
            time.sleep(.01)

        self.Destroy()


class ScanFrame(wx.Frame):
    """
    A lightweight scan frame that holds the :mod:`ScanPanel`.
    """
    def __init__(self, *args, **kwargs):
        """
        Initializes the scan frame. Takes all the usual wx.Frame arguments.
        """
        wx.Frame.__init__(self, *args, **kwargs)

        self._create_layout()

        self.Fit()

    def _create_layout(self):
        """
        Creates the layout, by calling mod:`ScanPanel`.
        """
        scan_panel = ScanPanel(parent=self)

        top_sizer = wx.BoxSizer(wx.HORIZONTAL)
        top_sizer.Add(scan_panel, 1, wx.EXPAND)

        self.SetSizer(top_sizer)


if __name__ == '__main__':

    app = wx.App()

    frame = ScanFrame(parent=None, title='Test Scan Control')
    frame.Show()
    app.MainLoop()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant