From 093ce016b23812d0aaf58c2b05c9d4189791e2a6 Mon Sep 17 00:00:00 2001 From: Blaise Thompson Date: Tue, 27 Mar 2018 16:06:30 -0500 Subject: 2018-03-27 16:06 --- acquisition/chapter.tex | 141 ++++++++++++++++++++++++++++++++++++++- acquisition/driver.py | 43 ++++++++++++ acquisition/hardware.py | 41 ++++++++++++ acquisition/hardware_inheritance | 16 +++++ acquisition/parent_hardware.py | 40 +++++++++++ 5 files changed, 278 insertions(+), 3 deletions(-) create mode 100644 acquisition/driver.py create mode 100644 acquisition/hardware.py create mode 100644 acquisition/hardware_inheritance create mode 100644 acquisition/parent_hardware.py (limited to 'acquisition') diff --git a/acquisition/chapter.tex b/acquisition/chapter.tex index 1525b9e..863bf96 100644 --- a/acquisition/chapter.tex +++ b/acquisition/chapter.tex @@ -56,6 +56,8 @@ Besides the extendable modular pieces, the rest of PyCMDS is a mostly-static cod modules and does the necessary things to handle display of information from, and communication between them. % +\subsection{Multithreading} % -------------------------------------------------------------------- + For the kinds of acquisitions that the Wright Group has done the acquisition software spends the vast majority of its run-time waiting---waiting for user input through mouse clicks or keyboard presses, waiting for hardware to finish moving or for sensors to finish reading and return @@ -72,7 +74,7 @@ acquisition requires. % Threads are powerful because they allow for ``semi-synchronous'' operation. % Imagine PyCMDS is in the middle of a 2D delay-delay scan, and the scan thread has just told each of -the two delay stages to head to their positions. % +the two delay stages to head to their destinations. % PyCMDS must sit in a tight loop to keep track of the position as closely as possible during motor motion. % In a single-threaded configuration, this tight loop would only run for one delay at a time, such @@ -84,12 +86,102 @@ This switching is handled in an OS and hardware specific way---luckily it is all platform-agnostic Qt threads. % Threads are dangerous because it is hard to pass information between them. % -Thus, mutexes... signals and slots... +Without any special protection, two threads have no reason not to simultaneously edit and read the +same location in memory. % +If a delay stage is writing its position to memory as a 64-bit double at the same time as the +acquisition thread reads that memory address, the acquisition thread will read in nonsense (or +worse), it will crash). % +So some strategy is needed to ensure that threads respect each other. % +The Mutex design allows threads to ``lock'' an object such that it cannot be modified by a +different thread. % +This lock is like the ``talking stick'' employed my many early child educators. % +When the talking stick is being used, only the child that holds the stick is allowed to speak. % +The stick must be passed to another child (as directed by the teacher) before they can share their +thought. % +PyCMDS makes heavy use of Mutexes, in particular the \bash{QMutex} class \cite{QMutex}. % + +Mutexes handle basic information transfer (two threads can both safely modify and read a particular +object), but what about sending instructions between threads? % +Here the problem is deciding what happens when multiple instructions are given simultaneously, or +an instruction is given while another instruction is being carried out. % +Again, this is a classic problem in computer science, and the classic answer is the queue. % +Queues are just like lines at the coffee shop---each person (instruction) is served (carried out) +in the order that they joined the line. % +Queues are commonly referred to as FIFO (First In First Out) for this reason. % +PyCMDS uses queues for almost all instructions. % + +Finally, PyCMDS makes extensive use of the ``signals and slots'' construct, which is somewhat +unique (and certainly original) to Qt. % +Signals and slots are powerful because they allow threads without instruction to go completely +silent, making them essentially free in terms of CPU usage. % +Normally, a thread needs to sit in a loop merely listening for instructions. % +Within the Qt framework, a thread can be ``woken'' by a signal without needing that thread to +explicitly ``listen''. % +These concepts fit within the broader umbrella of ``event-driven programming'', a concept that has +been used in many languages and frameworks (notably high level LabVIEW tends to be very +event-driven). % +The Qt signals and slots system massively simplifies programming within PyCMDS. % + +Note that multithreading is very different from multiprocessing. % + +\subsection{High level objects} % ---------------------------------------------------------------- + +PyCMDS is made to be extended and developed by and for immature programmers, so it is crucial to +create something that is less complicated... + +At it's most basic PyCMDS defines the following simple data types (derived from +\python{PyCMDS_object}): +\begin{ditemize} + \item Bool + \item Combo + \item Filepath + \item Number + \item String +\end{ditemize} +These classes do multiple things. % +First, they \emph{are} Mutexes, with thread-safe \python{read} and \python{write} methods. % +Secondly, they support ``implicit'' storage in ini files. % +Third, they know how to participate in the GUI. % +They can display their value, and if modified they will propagate that modification to the internal +threads of outward... +Finally, they have special properties like units and limits etc... Without getting into details, let's investigate the key ``signals and slots'' that hardware and sensors have. % % TODO: elaborate +The following is the top-level hardware class, parent of all hardware and sensors. % + +\begin{figure} + \includepython{"acquisition/parent_hardware.py"} + \caption[Parent to hardware and sensors.]{ + Parent class of all hardware and sensors. % + For brevity, methods \python{close}, \python{update} and \python{wait_until_still} have been + omitted. % + } + \label{aqn:fig:parent_hardware_class} +\end{figure} + +\begin{figure} + \includepython{"acquisition/driver.py"} + \caption[TODO]{ + TODO + } + \label{aqn:fig:driver} +\end{figure} + +\subsection{Graphical user interface} % ---------------------------------------------------------- + +Made up of widgets... + +Table widget... + +Use of qt plots... + +pyqtgraph \cite{pyqtgraph} + +\subsection{Scans} % ----------------------------------------------------------------------------- + The central loop of scans in PyCMDS. % \begin{codefragment}{python, label=aqn:lst:loop_simple} for coordinates in list_of_coordinates: @@ -146,6 +238,45 @@ because indeed that is all that can be generalized. % \section{Hardware} \label{aqn:sec:hardware} % ==================================================== +Hardware are things that 1) have a position, 2) can be set to a destination. % +Typically they also have associated units and limits. % +Each hardware can be thought of as a dimension of the MR-CMDS experiment, and scans include a +specific traversal through this multidimensional space. % + +\subsection{Hardware inheritance} % -------------------------------------------------------------- + +All hardware classes are children of the parent \python{Hardware} class, which is itself subclassed +from the global \python{Hardware} class shown in \autoref{aqn:lst:parent_hardware}. % + +\begin{figure} + \includepython{"acquisition/hardware.py"} + \caption[Parent hardware class.]{ + Parent class of all hardware. % + For brevity, methods \python{close}, \python{get_destination}, \python{get_position}, + \python{is_valid}, \python{on_address_initialized}, \python{poll}, and \python{@property units} + have been omitted. % + } + \label{aqn:fig:hardware_class} +\end{figure} + + +\begin{figure} + \includebash{"acquisition/hardware_inheritance"} + \caption[Hardware inheritance.]{ + } + \label{aqn:fig:hardware_inheritance} +\end{figure} + + + +\subsection{Delays} % ---------------------------------------------------------------------------- + +\subsection{Spectrometers} % --------------------------------------------------------------------- + +\subsection{OPAs} % ------------------------------------------------------------------------------ + +\subsection{Filters} % --------------------------------------------------------------------------- + \section{Sensors (devices)} \label{aqn:sec:sensors} % ============================================ \subsection{Sensors as axes} % ------------------------------------------------------------------- @@ -292,4 +423,8 @@ S_n &=& (1-c)\left(\frac{n}{N}\right)^{\frac{\tau_{\mathrm{step}}}{\tau_{\mathrm \subsection{Enhanced modularity} % --------------------------------------------------------------- -\subsection{wt5 savefile} % ---------------------------------------------------------------------- \ No newline at end of file +\subsection{wt5 savefile} % ---------------------------------------------------------------------- + +\subsection{Hotswappable hardware} % ------------------------------------------------------------- + +\subsection{Better logging and error handling} % ------------------------------------------------- \ No newline at end of file diff --git a/acquisition/driver.py b/acquisition/driver.py new file mode 100644 index 0000000..da1b18a --- /dev/null +++ b/acquisition/driver.py @@ -0,0 +1,43 @@ +class Driver(QtCore.QObject): + update_ui = QtCore.pyqtSignal() + queue_emptied = QtCore.pyqtSignal() + initialized = Bool() + + def check_busy(self): + """ + Handles writing of busy to False. + + Must always write to busy. + """ + if self.is_busy(): + time.sleep(0.01) # don't loop like crazy + self.busy.write(True) + elif self.enqueued.read(): + time.sleep(0.1) # don't loop like crazy + self.busy.write(True) + else: + self.busy.write(False) + self.update_ui.emit() + + @QtCore.pyqtSlot(str, list) + def dequeue(self, method, inputs): + """ + Slot to accept enqueued commands from main thread. + + Method passed as qstring, inputs as list of [args, kwargs]. + + Calls own method with arguments from inputs. + """ + self.update_ui.emit() + method = str(method) # method passed as qstring + args, kwargs = inputs + if g.debug.read(): + print(self.name, ' dequeue:', method, inputs, self.busy.read()) + self.enqueued.pop() + getattr(self, method)(*args, **kwargs) + if not self.enqueued.read(): + self.queue_emptied.emit() + self.check_busy() + + def is_busy(self): + return False diff --git a/acquisition/hardware.py b/acquisition/hardware.py new file mode 100644 index 0000000..2d062e2 --- /dev/null +++ b/acquisition/hardware.py @@ -0,0 +1,41 @@ +class Hardware(pc.Hardware): + + def __init__(self, *args, **kwargs): + pc.Hardware.__init__(self, *args, **kwargs) + self.exposed = self.driver.exposed + for obj in self.exposed: + obj.updated.connect(self.update) + self.recorded = self.driver.recorded + self.offset = self.driver.offset + self.position = self.exposed[0] + self.native_units = self.driver.native_units + self.destination = pc.Number(units=self.native_units, display=True) + self.destination.write(self.position.read(self.native_units), + self.native_units) + self.limits = self.driver.limits + self.driver.initialized_signal.connect(self.on_address_initialized) + hardwares.append(self) + + def set_offset(self, offset, input_units=None): + if input_units is None: + pass + else: + offset = wt.units.converter(offset, input_units, + self.native_units) + # do nothing if new offset is same as current offset + if offset == self.offset.read(self.native_units): + return + self.q.push('set_offset', offset) + + def set_position(self, destination, input_units=None, force_send=False): + if input_units is None: + pass + else: + destination = wt.units.converter(destination, input_units, + self.native_units) + # do nothing if new destination is same as current destination + if destination == self.destination.read(self.native_units): + if not force_send: + return + self.destination.write(destination, self.native_units) + self.q.push('set_position', destination) diff --git a/acquisition/hardware_inheritance b/acquisition/hardware_inheritance new file mode 100644 index 0000000..0a13a1b --- /dev/null +++ b/acquisition/hardware_inheritance @@ -0,0 +1,16 @@ +Hardware +├── Delay +│ ├── Aerotech +│ ├── LTS300 +│ ├── MFA +│ ├── MFA +│ └── PMC +├── Filter +│ └── Homebuilt +├── OPA +│ ├── OPA800/PMC +│ └── TOPAS +│ ├── TOPAS-C +│ └── TOPAS-800 +└── Spectrometer + └── MicroHR \ No newline at end of file diff --git a/acquisition/parent_hardware.py b/acquisition/parent_hardware.py new file mode 100644 index 0000000..8ced565 --- /dev/null +++ b/acquisition/parent_hardware.py @@ -0,0 +1,40 @@ +class Hardware(QtCore.QObject): + update_ui = QtCore.pyqtSignal() + initialized_signal = QtCore.pyqtSignal() + + def __init__(self, driver_class, driver_arguments, gui_class, + name, model, serial=None): + """ + Hardware representation object living in the main thread. + + Parameters + driver_class : Driver class + driver_arguments : dictionary + name : string + model : string + serial : string or None (optional) + """ + QtCore.QObject.__init__(self) + self.name = name + self.model = model + self.serial = serial + # create objects + self.thread = QtCore.QThread() + self.enqueued = Enqueued() + self.busy = Busy() + self.driver = driver_class(self, **driver_arguments) + self.initialized = self.driver.initialized + self.gui = gui_class(self) + self.q = Q(self.enqueued, self.busy, self.driver) + # start thread + self.driver.moveToThread(self.thread) + self.thread.start() + # connect to address object signals + self.driver.update_ui.connect(self.update) + self.busy.update_signal = self.driver.update_ui + # initialize drivers + self.q.push('initialize') + # integrate close into PyCMDS shutdown + self.shutdown_timeout = 30 # seconds + g.shutdown.add_method(self.close) + g.hardware_waits.add(self.wait_until_still) -- cgit v1.2.3