aboutsummaryrefslogtreecommitdiff
path: root/acquisition
diff options
context:
space:
mode:
authorBlaise Thompson <blaise@untzag.com>2018-03-27 16:06:30 -0500
committerBlaise Thompson <blaise@untzag.com>2018-03-27 16:06:30 -0500
commit093ce016b23812d0aaf58c2b05c9d4189791e2a6 (patch)
tree125fb4613ebdb38d63d5f87a6d62eb8063118495 /acquisition
parent8c9fc50d417809d7c5ad1b512f55c83307a83364 (diff)
2018-03-27 16:06
Diffstat (limited to 'acquisition')
-rw-r--r--acquisition/chapter.tex141
-rw-r--r--acquisition/driver.py43
-rw-r--r--acquisition/hardware.py41
-rw-r--r--acquisition/hardware_inheritance16
-rw-r--r--acquisition/parent_hardware.py40
5 files changed, 278 insertions, 3 deletions
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)