From bc525a1f8190023cac14969fb0af8fc813974cef Mon Sep 17 00:00:00 2001 From: David Meyer Date: Wed, 28 Oct 2020 21:07:55 -0400 Subject: [PATCH 01/28] Boiler plate doc files creation. Outlines basic documentation structure to cover all the devices as well as some higher level stuff. --- docs/source/adding_devices.rst | 2 + docs/source/conf.py | 2 + docs/source/devices.rst | 83 ++++++++++++++++++++ docs/source/devices/IMAQdx.rst | 2 + docs/source/devices/alazartechboard.rst | 2 + docs/source/devices/andorsolis.rst | 2 + docs/source/devices/dummyintermediate.rst | 2 + docs/source/devices/dummypseudoclock.rst | 2 + docs/source/devices/flycapture2.rst | 2 + docs/source/devices/functionrunner.rst | 2 + docs/source/devices/lightcrafterdmd.rst | 2 + docs/source/devices/ni_daqs.rst | 2 + docs/source/devices/novatechDDS9m.rst | 2 + docs/source/devices/opalkellyXEM3001.rst | 2 + docs/source/devices/phasematrixquicksyn.rst | 2 + docs/source/devices/pineblaster.rst | 2 + docs/source/devices/pulseblaster.rst | 2 + docs/source/devices/pulseblaster_no_dds.rst | 2 + docs/source/devices/pylon.rst | 49 ++++++++++++ docs/source/devices/rfblaster.rst | 2 + docs/source/devices/tekscope.rst | 2 + docs/source/devices/testdevice.rst | 2 + docs/source/devices/zaberstagecontroller.rst | 2 + docs/source/ex_conn_tables.rst | 2 + docs/source/index.rst | 6 +- docs/source/introduction.rst | 24 ++++++ docs/source/user_devices.rst | 2 + 27 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 docs/source/adding_devices.rst create mode 100644 docs/source/devices.rst create mode 100644 docs/source/devices/IMAQdx.rst create mode 100644 docs/source/devices/alazartechboard.rst create mode 100644 docs/source/devices/andorsolis.rst create mode 100644 docs/source/devices/dummyintermediate.rst create mode 100644 docs/source/devices/dummypseudoclock.rst create mode 100644 docs/source/devices/flycapture2.rst create mode 100644 docs/source/devices/functionrunner.rst create mode 100644 docs/source/devices/lightcrafterdmd.rst create mode 100644 docs/source/devices/ni_daqs.rst create mode 100644 docs/source/devices/novatechDDS9m.rst create mode 100644 docs/source/devices/opalkellyXEM3001.rst create mode 100644 docs/source/devices/phasematrixquicksyn.rst create mode 100644 docs/source/devices/pineblaster.rst create mode 100644 docs/source/devices/pulseblaster.rst create mode 100644 docs/source/devices/pulseblaster_no_dds.rst create mode 100644 docs/source/devices/pylon.rst create mode 100644 docs/source/devices/rfblaster.rst create mode 100644 docs/source/devices/tekscope.rst create mode 100644 docs/source/devices/testdevice.rst create mode 100644 docs/source/devices/zaberstagecontroller.rst create mode 100644 docs/source/ex_conn_tables.rst create mode 100644 docs/source/introduction.rst create mode 100644 docs/source/user_devices.rst diff --git a/docs/source/adding_devices.rst b/docs/source/adding_devices.rst new file mode 100644 index 00000000..ef009025 --- /dev/null +++ b/docs/source/adding_devices.rst @@ -0,0 +1,2 @@ +How to Add a Device +=================== \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index 3610cd87..e2d4a0f8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -39,6 +39,7 @@ # ones. extensions = [ "sphinx.ext.autodoc", + "sphinx.ext.autosummary", "sphinx.ext.autosectionlabel", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", @@ -49,6 +50,7 @@ ] autodoc_typehints = 'description' +autoclass_content = 'init' # Prefix each autosectionlabel with the name of the document it is in and a colon autosectionlabel_prefix_document = True diff --git a/docs/source/devices.rst b/docs/source/devices.rst new file mode 100644 index 00000000..79a62b87 --- /dev/null +++ b/docs/source/devices.rst @@ -0,0 +1,83 @@ +Devices +========= + +Here is a list of all the currently supported devices. + + +Pseudoclocks +~~~~~~~~~~~~ + +Pseudoclocks provide the timing backbone of the labscript_suite. These devices produce hardware-timed clocklines that trigger other device outputs and acquisitions. Many pseudoclock devices also include other types of outputs, including digital voltage and DDS frequency synthesizers. + +.. toctree:: + :maxdepth: 2 + + devices/pulseblaster + devices/pulseblaster_no_dds + devices/opalkellyXEM3001 + devices/pineblaster + devices/rfblaster + +NI DAQS +~~~~~~~~~~~~ + +The NI_DAQmx device provides a generic interface for National Instruments data acquisition hardware. This includes digital and analog voltage I/O. These input/outputs can be either static or hardware-timed dynamically changing variables. + +.. toctree:: + :maxdepth: 2 + + devices/ni_daqs + +Cameras +~~~~~~~~~~~~ + +The camera devices provide interfaces for using various scientific cameras to acquire hardware-timed images during an experiment. They are organized by the programming API the underlies the communication to the device. The "master" camera class which provides the core functionality and from which the others derive is the IMAQdx class. + +.. toctree:: + :maxdepth: 2 + + devices/IMAQdx + devices/pylon + devices/flycapture2 + devices/andorsolis + + +Frequency Sources +~~~~~~~~~~~~~~~~~ + +These devices cover various frequency sources that provide either hardware-timed frequency, amplitude, or phase updates or static frequency outputs. + +.. toctree:: + :maxdepth: 2 + + devices/novatechDDS9m + devices/phasematrixquicksyn + + +Miscellaneous +~~~~~~~~~~~~~~~ + +These devices cover other types of devices. + +.. toctree:: + :maxdepth: 2 + + devices/alazartechboard + devices/lightcrafterdmd + devices/tekscope + devices/zaberstagecontroller + + +Other +~~~~~~~~~~~~~~ + +These devices provide dummy instruments for prototyping and testing purposes of the rest of the labscript_suite as well as the FunctionRunner device which can run arbitrary code post-shot. + +.. toctree:: + :maxdepth: 2 + + devices/functionrunner + devices/dummypseudoclock + devices/dummyintermediate + devices/testdevice + \ No newline at end of file diff --git a/docs/source/devices/IMAQdx.rst b/docs/source/devices/IMAQdx.rst new file mode 100644 index 00000000..ef1780a2 --- /dev/null +++ b/docs/source/devices/IMAQdx.rst @@ -0,0 +1,2 @@ +IMAQdx Cameras +============== \ No newline at end of file diff --git a/docs/source/devices/alazartechboard.rst b/docs/source/devices/alazartechboard.rst new file mode 100644 index 00000000..78ce8d72 --- /dev/null +++ b/docs/source/devices/alazartechboard.rst @@ -0,0 +1,2 @@ +Alazar Tech Board +================= \ No newline at end of file diff --git a/docs/source/devices/andorsolis.rst b/docs/source/devices/andorsolis.rst new file mode 100644 index 00000000..2027727e --- /dev/null +++ b/docs/source/devices/andorsolis.rst @@ -0,0 +1,2 @@ +Andor Solis Cameras +=================== \ No newline at end of file diff --git a/docs/source/devices/dummyintermediate.rst b/docs/source/devices/dummyintermediate.rst new file mode 100644 index 00000000..64e8e285 --- /dev/null +++ b/docs/source/devices/dummyintermediate.rst @@ -0,0 +1,2 @@ +Dummy Intermediate Device +========================= \ No newline at end of file diff --git a/docs/source/devices/dummypseudoclock.rst b/docs/source/devices/dummypseudoclock.rst new file mode 100644 index 00000000..30e34e2c --- /dev/null +++ b/docs/source/devices/dummypseudoclock.rst @@ -0,0 +1,2 @@ +Dummy Pseudoclock +================= \ No newline at end of file diff --git a/docs/source/devices/flycapture2.rst b/docs/source/devices/flycapture2.rst new file mode 100644 index 00000000..52ce76f7 --- /dev/null +++ b/docs/source/devices/flycapture2.rst @@ -0,0 +1,2 @@ +FlyCapture2 Cameras +=================== \ No newline at end of file diff --git a/docs/source/devices/functionrunner.rst b/docs/source/devices/functionrunner.rst new file mode 100644 index 00000000..b4830b06 --- /dev/null +++ b/docs/source/devices/functionrunner.rst @@ -0,0 +1,2 @@ +Function Runner +=============== \ No newline at end of file diff --git a/docs/source/devices/lightcrafterdmd.rst b/docs/source/devices/lightcrafterdmd.rst new file mode 100644 index 00000000..398862c0 --- /dev/null +++ b/docs/source/devices/lightcrafterdmd.rst @@ -0,0 +1,2 @@ +Light Crafter DMD +================= \ No newline at end of file diff --git a/docs/source/devices/ni_daqs.rst b/docs/source/devices/ni_daqs.rst new file mode 100644 index 00000000..1fc20a0b --- /dev/null +++ b/docs/source/devices/ni_daqs.rst @@ -0,0 +1,2 @@ +NI DAQS +======= \ No newline at end of file diff --git a/docs/source/devices/novatechDDS9m.rst b/docs/source/devices/novatechDDS9m.rst new file mode 100644 index 00000000..688759a1 --- /dev/null +++ b/docs/source/devices/novatechDDS9m.rst @@ -0,0 +1,2 @@ +Novatech DDS 9m +=============== \ No newline at end of file diff --git a/docs/source/devices/opalkellyXEM3001.rst b/docs/source/devices/opalkellyXEM3001.rst new file mode 100644 index 00000000..ac30202a --- /dev/null +++ b/docs/source/devices/opalkellyXEM3001.rst @@ -0,0 +1,2 @@ +Cicero Opal-Kelly XEM3001 +========================= \ No newline at end of file diff --git a/docs/source/devices/phasematrixquicksyn.rst b/docs/source/devices/phasematrixquicksyn.rst new file mode 100644 index 00000000..09619111 --- /dev/null +++ b/docs/source/devices/phasematrixquicksyn.rst @@ -0,0 +1,2 @@ +Phase Matrix Quick Synthesizer +============================== \ No newline at end of file diff --git a/docs/source/devices/pineblaster.rst b/docs/source/devices/pineblaster.rst new file mode 100644 index 00000000..07cf4491 --- /dev/null +++ b/docs/source/devices/pineblaster.rst @@ -0,0 +1,2 @@ +Pineblaster +=========== \ No newline at end of file diff --git a/docs/source/devices/pulseblaster.rst b/docs/source/devices/pulseblaster.rst new file mode 100644 index 00000000..201d5b54 --- /dev/null +++ b/docs/source/devices/pulseblaster.rst @@ -0,0 +1,2 @@ +Pulseblaster +============ \ No newline at end of file diff --git a/docs/source/devices/pulseblaster_no_dds.rst b/docs/source/devices/pulseblaster_no_dds.rst new file mode 100644 index 00000000..173c56cd --- /dev/null +++ b/docs/source/devices/pulseblaster_no_dds.rst @@ -0,0 +1,2 @@ +Pulseblaster (-DDS) +=================== \ No newline at end of file diff --git a/docs/source/devices/pylon.rst b/docs/source/devices/pylon.rst new file mode 100644 index 00000000..53abaaa3 --- /dev/null +++ b/docs/source/devices/pylon.rst @@ -0,0 +1,49 @@ +Pylon Cameras +============= + +Overview +~~~~~~~~ + +This device allows control of Basler scientific cameras via the `Pylon API `_ with the `PyPylon python wrapper `_. In order to use this device, both the Basler Pylon API and the PyPylon wrapper must be installed. + +.. autosummary:: + labscript_devices.PylonCamera.labscript_devices + labscript_devices.PylonCamera.blacs_tabs + labscript_devices.PylonCamera.blacs_workers + labscript_devices.PylonCamera.register_classes + +Installation +~~~~~~~~~~~~ + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.PylonCamera + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.PylonCamera.labscript_devices + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.PylonCamera.blacs_tabs + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.PylonCamera.blacs_workers + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.PylonCamera.register_classes + :members: + :undoc-members: + :show-inheritance: + :private-members: \ No newline at end of file diff --git a/docs/source/devices/rfblaster.rst b/docs/source/devices/rfblaster.rst new file mode 100644 index 00000000..c25fb389 --- /dev/null +++ b/docs/source/devices/rfblaster.rst @@ -0,0 +1,2 @@ +RFblaster +========= diff --git a/docs/source/devices/tekscope.rst b/docs/source/devices/tekscope.rst new file mode 100644 index 00000000..e0204d49 --- /dev/null +++ b/docs/source/devices/tekscope.rst @@ -0,0 +1,2 @@ +Tektronix Oscilloscope +====================== \ No newline at end of file diff --git a/docs/source/devices/testdevice.rst b/docs/source/devices/testdevice.rst new file mode 100644 index 00000000..faae360f --- /dev/null +++ b/docs/source/devices/testdevice.rst @@ -0,0 +1,2 @@ +Test Device +=========== \ No newline at end of file diff --git a/docs/source/devices/zaberstagecontroller.rst b/docs/source/devices/zaberstagecontroller.rst new file mode 100644 index 00000000..e4d08da2 --- /dev/null +++ b/docs/source/devices/zaberstagecontroller.rst @@ -0,0 +1,2 @@ +Zaber Stage Controller +====================== diff --git a/docs/source/ex_conn_tables.rst b/docs/source/ex_conn_tables.rst new file mode 100644 index 00000000..67a3df66 --- /dev/null +++ b/docs/source/ex_conn_tables.rst @@ -0,0 +1,2 @@ +Example Connection Tables +========================= \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index aba2a7a4..f3bfd0b4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,7 +11,11 @@ labscript-devices :hidden: :caption: DOCUMENTATION - + introduction + devices + user_devices + ex_conn_tables + adding_devices .. toctree:: :maxdepth: 2 diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst new file mode 100644 index 00000000..052c4cf9 --- /dev/null +++ b/docs/source/introduction.rst @@ -0,0 +1,24 @@ +Introduction +============ + +The **labscript_devices** module contains the low-level hardware interfacing code that intermediates between the :doc:`labscript ` API (converting **labscript** instructions into hardware instructions) as well as the :doc:`BLACS ` GUI (which communicates directly with the hardware). + +Each "device" is made up of four classes that handle the various tasks. + +* `labscript_device` (derives from :doc:`labscript.Device `) + + - Defines the interface between the **labscript** API and generates hardware instructions that can be saved to the shot h5 file. + +* `BLACS_tab` (derives from :doc:`blacs.device_base_class.DeviceTab `) + + - Defines the graphical tab that is present in the **BLACS** GUI. This tab provides graphical widgets for controlling hardware outputs and visualizing hardware inputs. + +* `BLACS_worker` (derives from :doc:`blacs.tab_base_classes.Worker `) + + - Defines the software control interface to the hardware. The `BLACS_tab` spawns a process that uses this class to send and receive commands with the hardware. + +* `runviewer_parser` + + - Defines a software interface that interprets hardware instructions in a shot h5 file and displays them in the :doc:`runviewer ` GUI. + +The **labscript_suite** provides an extensive :doc:`list of device classes ` for commercially available hardware. Furthermore, it is simple to add local :doc:`user devices ` to control instruments not already within the labscript-suite. \ No newline at end of file diff --git a/docs/source/user_devices.rst b/docs/source/user_devices.rst new file mode 100644 index 00000000..f356b676 --- /dev/null +++ b/docs/source/user_devices.rst @@ -0,0 +1,2 @@ +User Devices +============ \ No newline at end of file From 19e42fd9073b81e4ad3b812f320a9fc462c19922 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Thu, 29 Oct 2020 20:26:09 -0400 Subject: [PATCH 02/28] A few more updates to the Pylon Camera documentation and a start to the IMAQdx documentation. Mostly just to trigger another build on ReadTheDocs. --- docs/source/devices/IMAQdx.rst | 54 +++++++++++++++++++++++++++++- docs/source/devices/pylon.rst | 61 ++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/docs/source/devices/IMAQdx.rst b/docs/source/devices/IMAQdx.rst index ef1780a2..cdfe00e2 100644 --- a/docs/source/devices/IMAQdx.rst +++ b/docs/source/devices/IMAQdx.rst @@ -1,2 +1,54 @@ IMAQdx Cameras -============== \ No newline at end of file +============== + +Overview +~~~~~~~~ + +The "master" camera device from which all others derive. + +.. autosummary:: + labscript_devices.IMAQdxCamera.labscript_devices + labscript_devices.IMAQdxCamera.blacs_tabs + labscript_devices.IMAQdxCamera.blacs_workers + labscript_devices.IMAQdxCamera.register_classes + +Installation +~~~~~~~~~~~~ + + +Usage +~~~~~ + + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.IMAQdxCamera + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.IMAQdxCamera.labscript_devices + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.IMAQdxCamera.blacs_tabs + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.IMAQdxCamera.blacs_workers + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.IMAQdxCamera.register_classes + :members: + :undoc-members: + :show-inheritance: + :private-members: \ No newline at end of file diff --git a/docs/source/devices/pylon.rst b/docs/source/devices/pylon.rst index 53abaaa3..e4061e62 100644 --- a/docs/source/devices/pylon.rst +++ b/docs/source/devices/pylon.rst @@ -15,6 +15,67 @@ This device allows control of Basler scientific cameras via the `Pylon API `_ (after signing up for a free account with Basler). It is advisable to use the Pylon Viewer program that comes with the SDK to test communications with the camera. + +The python wrapper is installed via pip: + +.. code-block:: bash + + pip install -U pypylon + +At present, the wrapper is tested and confirmed compatible with Pylon 5 for USB3 and GigE interface cameras. + +For GigE cameras, ensure that the network interface card (NIC) on the computer with the BLACS controlling the camera has enabled Jumbo Frames. That maximum allowed value (typically 9000) is preferable to avoid dropped frames. + +For USB3 cameras, care should be taken to use a USB3 host that is compatible with the Basler cameras. Basler maintains a list of compatible host controllers. The cameras will work on any USB3 port, but non-compatible hosts will not allow for the faster performance. + +Usage +~~~~~ + +Like the :doc:`IMAQdxCamera ` device, the bulk of camera configuration is performed using a dictionary of kwargs, where the key names and values mirror those provided by the Pylon SDK interface. Which parameters can/need to be set depend on the communication interface. Discovery of what parameters are available can be done in three ways: + +1. Careful reading of the Pylon SDK docs. +2. Mirroring the Pylon Viewer parameter names and values. +3. Connecting to the camera with a minimal configuration, viewing the current parameters dictionary, and copying the relevant values to the connection table (preferred). + +This is an example configuration for a GigE camera. + +.. code-block:: python + + from labscript_devices.PylonCamera.labscript_devices import PylonCamera + + PylonCamera('gigeCamera',parent_device=parent,connection=conn, + serial_number=1234567, # set to the camera serial number + minimum_recovery_time=36e-6, # the minimum exposure time depends on the camera model & configuration + camera_attributs={}, + manual_camera_attributes={}) + + start() + + gigeCamera.expose(t=0.5,'exposure1') + + stop(1) + +This is an example configuration for a USB3 camera. + +.. code-block:: python + + from labscript_devices.PylonCamera.labscript_devices import PylonCamera + + PylonCamera('usb3Camera',parent_device=parent,connection=conn, + serial_number=1234567, + minimum_recovery_time=36e-6, + camera_attributs={}, + manual_camera_attributes={}) + + start() + + usb3Camera.expose(t=0.5,'exposure1') + + stop(1) + + + Detailed Documentation ~~~~~~~~~~~~~~~~~~~~~~ From 79a3e58d0aed0975174ff562a6c7ddeb65449971 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Fri, 30 Oct 2020 16:59:16 -0400 Subject: [PATCH 03/28] Increasing coverage to the rest of the devices, excluding a few remaining pseudoclocks. I have used an alternate file structure for the docs on the DummyIntermediateDevice as an example of another method of documenting. --- docs/source/devices/IMAQdx.rst | 7 -- docs/source/devices/alazartechboard.rst | 22 +++++- docs/source/devices/andorsolis.rst | 43 ++++++++++- docs/source/devices/dummyintermediate.rst | 10 ++- docs/source/devices/dummypseudoclock.rst | 63 +++++++++++++++- docs/source/devices/flycapture2.rst | 77 +++++++++++++++++++- docs/source/devices/functionrunner.rst | 44 ++++++++++- docs/source/devices/lightcrafterdmd.rst | 17 ++++- docs/source/devices/novatechDDS9m.rst | 15 +++- docs/source/devices/phasematrixquicksyn.rst | 17 ++++- docs/source/devices/pulseblaster.rst | 46 +++++++++++- docs/source/devices/pylon.rst | 67 ++++++++++------- docs/source/devices/tekscope.rst | 49 ++++++++++++- docs/source/devices/testdevice.rst | 14 +++- docs/source/devices/zaberstagecontroller.rst | 42 +++++++++++ labscript_devices/DummyIntermediateDevice.py | 43 +++++++++-- 16 files changed, 520 insertions(+), 56 deletions(-) diff --git a/docs/source/devices/IMAQdx.rst b/docs/source/devices/IMAQdx.rst index cdfe00e2..92bcfb47 100644 --- a/docs/source/devices/IMAQdx.rst +++ b/docs/source/devices/IMAQdx.rst @@ -10,7 +10,6 @@ The "master" camera device from which all others derive. labscript_devices.IMAQdxCamera.labscript_devices labscript_devices.IMAQdxCamera.blacs_tabs labscript_devices.IMAQdxCamera.blacs_workers - labscript_devices.IMAQdxCamera.register_classes Installation ~~~~~~~~~~~~ @@ -46,9 +45,3 @@ Detailed Documentation :undoc-members: :show-inheritance: :private-members: - -.. automodule:: labscript_devices.IMAQdxCamera.register_classes - :members: - :undoc-members: - :show-inheritance: - :private-members: \ No newline at end of file diff --git a/docs/source/devices/alazartechboard.rst b/docs/source/devices/alazartechboard.rst index 78ce8d72..a4d715f8 100644 --- a/docs/source/devices/alazartechboard.rst +++ b/docs/source/devices/alazartechboard.rst @@ -1,2 +1,22 @@ Alazar Tech Board -================= \ No newline at end of file +================= + +A labscript device class for data acquisition boards made by Alazar Technologies Inc (ATS). + +Installation +~~~~~~~~~~~~ + +This device requires the atsapi.py wrapper. It should be installed into site-packages or +kept in the local directory. + +It also uses the tqdm progress bar, which is not a standard dependency for the labscript-suite. + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.AlazarTechBoard + :members: + :undoc-members: + :show-inheritance: + :member-order: bysource + :private-members: \ No newline at end of file diff --git a/docs/source/devices/andorsolis.rst b/docs/source/devices/andorsolis.rst index 2027727e..58ae3776 100644 --- a/docs/source/devices/andorsolis.rst +++ b/docs/source/devices/andorsolis.rst @@ -1,2 +1,43 @@ Andor Solis Cameras -=================== \ No newline at end of file +=================== + +A labscript device for controlling Andor scientific cameras via the Andor SDK3 interface. + +Presently, this device is hard-coded for use with the iXon camera. Minor modifications can allow use with other Andor cameras, so long as they are compatible with the Andor SDK3 library. + +.. autosummary:: + labscript_devices.AndorSolis.labscript_devices + labscript_devices.AndorSolis.blacs_tabs + labscript_devices.AndorSolis.blacs_workers + +Installation +~~~~~~~~~~~~ + +The Andor SDK is available from Andor as a paid product (typically purchased with the camera). It must be installed with the SDK directory added to the system path. + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.AndorSolis + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.AndorSolis.labscript_devices + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.AndorSolis.blacs_tabs + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.AndorSolis.blacs_workers + :members: + :undoc-members: + :show-inheritance: + :private-members: \ No newline at end of file diff --git a/docs/source/devices/dummyintermediate.rst b/docs/source/devices/dummyintermediate.rst index 64e8e285..7a654dc7 100644 --- a/docs/source/devices/dummyintermediate.rst +++ b/docs/source/devices/dummyintermediate.rst @@ -1,2 +1,10 @@ Dummy Intermediate Device -========================= \ No newline at end of file +========================= + +.. automodule:: labscript_devices.DummyIntermediateDevice + :members: + :undoc-members: + :show-inheritance: + :member-order: bysource + :private-members: + diff --git a/docs/source/devices/dummypseudoclock.rst b/docs/source/devices/dummypseudoclock.rst index 30e34e2c..5078668b 100644 --- a/docs/source/devices/dummypseudoclock.rst +++ b/docs/source/devices/dummypseudoclock.rst @@ -1,2 +1,63 @@ Dummy Pseudoclock -================= \ No newline at end of file +================= + +This device represents a dummy labscript device for purposes of testing BLACS +and labscript. The device is a PseudoclockDevice, and can be the sole device +in a connection table or experiment. + +.. autosummary:: + labscript_devices.DummyPseudoclock.labscript_devices + labscript_devices.DummyPseudoclock.blacs_tabs + labscript_devices.DummyPseudoclock.blacs_workers + labscript_devices.DummyPseudoclock.runviewer_parsers + +Usage +~~~~~ + +.. code-block:: python + + from labscript import * + + from labscript_devices.DummyPseudoclock.labscript_devices import DummyPseudoclock + from labscript_devices.DummyIntermediateDevice import DummyIntermediateDevice + + DummyPseudoclock(name='dummy_clock',BLACS_connection='dummy') + DummyIntermediateDevice(name='dummy_device',BLACS_connection='dummy2', + parent_device=dummy_clock.clockline) + + start() + stop(1) + + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.DummyPseudoclock + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.DummyPseudoclock.labscript_devices + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.DummyPseudoclock.blacs_tabs + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.DummyPseudoclock.blacs_workers + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.DummyPseudoclock.runviewer_parsers + :members: + :undoc-members: + :show-inheritance: + :private-members: diff --git a/docs/source/devices/flycapture2.rst b/docs/source/devices/flycapture2.rst index 52ce76f7..9dea4323 100644 --- a/docs/source/devices/flycapture2.rst +++ b/docs/source/devices/flycapture2.rst @@ -1,2 +1,77 @@ FlyCapture2 Cameras -=================== \ No newline at end of file +=================== + +This device allows control of FLIR (formerly Point Grey) scientific cameras via the `FlyCapture2 SDK `_ with the now deprecated PyCapture2 wrapper. In order to use this device, both the SDK and the python wrapper must be installed. Note that PyCapture2 only supports up to Python 3.6. + +.. autosummary:: + labscript_devices.FlyCapture2Camera.labscript_devices + labscript_devices.FlyCapture2Camera.blacs_tabs + labscript_devices.FlyCapture2Camera.blacs_workers + +Installation +~~~~~~~~~~~~ + +First ensure that the FlyCapture2 SDK is installed. + +The python wrapper is available via FLIR and is only released for Python up to 3.6. It must be installed separately to pointed to the correct conda environment during install. + + +For GigE cameras, ensure that the network interface card (NIC) on the computer with the BLACS controlling the camera has enabled Jumbo Frames. That maximum allowed value (typically 9000) is preferable to avoid dropped frames. + +Usage +~~~~~ + +Like the :doc:`IMAQdxCamera ` device, the bulk of camera configuration is performed using a dictionary of kwargs, where the key names and values mirror those provided by the FlyCapture2 SDK interface. Which parameters can/need to be set depend on the communication interface. Discovery of what parameters are available can be done in three ways: + +1. Careful reading of the FlyCapture2 SDK docs. +2. Mirroring the FlyCap Viewer parameter names and values. +3. Connecting to the camera with a minimal configuration, viewing the current parameters dictionary, and copying the relevant values to the connection table (preferred). + +Below is a generic configuration. + +.. code-block:: python + + from labscript import * + + from labscript_devices.FlyCapture2Camera.labscript_devices import FlyCapture2Camera + + FlyCapture2Camera('gigeCamera',parent_device=parent,connection=conn, + serial_number=1234567, # set to the camera serial number + minimum_recovery_time=36e-6, # the minimum exposure time depends on the camera model & configuration + camera_attributs={}, + manual_camera_attributes={}) + + start() + + gigeCamera.expose(t=0.5,'exposure1') + + + stop(1) + + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.FlyCapture2Camera + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.FlyCapture2Camera.labscript_devices + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.FlyCapture2Camera.blacs_tabs + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.FlyCapture2Camera.blacs_workers + :members: + :undoc-members: + :show-inheritance: + :private-members: diff --git a/docs/source/devices/functionrunner.rst b/docs/source/devices/functionrunner.rst index b4830b06..43489d91 100644 --- a/docs/source/devices/functionrunner.rst +++ b/docs/source/devices/functionrunner.rst @@ -1,2 +1,44 @@ Function Runner -=============== \ No newline at end of file +=============== + +A labscript device to run custom functions before, after, or during (not yet +implemented) the experiment in software time. + +.. autosummary:: + labscript_devices.FunctionRunner.labscript_devices + labscript_devices.FunctionRunner.blacs_tabs + labscript_devices.FunctionRunner.blacs_workers + labscript_devices.FunctionRunner.utils + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.FunctionRunner + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.FunctionRunner.labscript_devices + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.FunctionRunner.blacs_tabs + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.FunctionRunner.blacs_workers + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.FunctionRunner.utils + :members: + :undoc-members: + :show-inheritance: + :private-members: diff --git a/docs/source/devices/lightcrafterdmd.rst b/docs/source/devices/lightcrafterdmd.rst index 398862c0..633e4e63 100644 --- a/docs/source/devices/lightcrafterdmd.rst +++ b/docs/source/devices/lightcrafterdmd.rst @@ -1,2 +1,17 @@ Light Crafter DMD -================= \ No newline at end of file +================= + +This device allows for control of Light Crafter development module boards. +It is currently hard-coded to work with a DLPC300 with a fixed DLP 0.3 WVGA +resolution. Extenstion to other models involves subclassing and altering +relevant class attributes. + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.LightCrafterDMD + :members: + :undoc-members: + :show-inheritance: + :member-order: bysource + :private-members: \ No newline at end of file diff --git a/docs/source/devices/novatechDDS9m.rst b/docs/source/devices/novatechDDS9m.rst index 688759a1..b0124a47 100644 --- a/docs/source/devices/novatechDDS9m.rst +++ b/docs/source/devices/novatechDDS9m.rst @@ -1,2 +1,15 @@ Novatech DDS 9m -=============== \ No newline at end of file +=============== + +Labscript device for control of the Novatech DDS9m synthesizer. +With minor modifications, it can also control the Novatech 409B DDS. + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.NovaTechDDS9M + :members: + :undoc-members: + :show-inheritance: + :member-order: bysource + :private-members: \ No newline at end of file diff --git a/docs/source/devices/phasematrixquicksyn.rst b/docs/source/devices/phasematrixquicksyn.rst index 09619111..89f712a5 100644 --- a/docs/source/devices/phasematrixquicksyn.rst +++ b/docs/source/devices/phasematrixquicksyn.rst @@ -1,2 +1,15 @@ -Phase Matrix Quick Synthesizer -============================== \ No newline at end of file +QuickSyn FSW-0010 Synthesizer +============================= + +A labscript device that controlls the NI Quicksyn FSW-0010 Microwave Synthesizer +(formerly PhaseMatrix). + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.PhaseMatrixQuickSyn + :members: + :undoc-members: + :show-inheritance: + :member-order: bysource + :private-members: \ No newline at end of file diff --git a/docs/source/devices/pulseblaster.rst b/docs/source/devices/pulseblaster.rst index 201d5b54..a2c7a528 100644 --- a/docs/source/devices/pulseblaster.rst +++ b/docs/source/devices/pulseblaster.rst @@ -1,2 +1,46 @@ Pulseblaster -============ \ No newline at end of file +============ + +This labscript device controls the Spincore PulseblaserDDS-II-300-AWG. The Pulseblaster is a programmable pulse generator that is the typical timing backbone of an experiment (ie it generates the pseudoclock timing pulses that control execution of other devices in the experiment). This labscript device is the master implementation of the various Pulseblaster devices. + +This particular version of Pulseblaster also has DDS synthesizer outputs. + +Installation +~~~~~~~~~~~~ + +Use of the Pulseblaster requires driver installation available from the manufacturer `here `_. The corresponding python wrapper, `spinapi `_ is available via pip. + +.. code-block:: bash + + pip install -U spinapi + +Usage +~~~~~ + +.. code-block:: python + + from labscript import * + + from labscript_devices.PulseBlaster import PulseBlaster + + PulseBlaster(name='pb',board_number=0,programming_scheme='pb_start/BRANCH') + + Clockline(name='pb_clockline_fast', pseudoclock=pb.pseudoclock,connection='flag 0') + Clockline(name='pb_clockline_slow', pseudoclock=pb.pseudoclock,connection='flag 1') + + DigitalOut(name='pb_0',parent_device=pb.direct_outputs,connection='flag 2') + + PulseBlasterDDS(name='pb_dds_0',parent_device=pb.direct_outputs, 'channel 0') + + start() + + stop(1) + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.PulseBlaster + :members: + :undoc-members: + :show-inheritance: + :private-members: \ No newline at end of file diff --git a/docs/source/devices/pylon.rst b/docs/source/devices/pylon.rst index e4061e62..9d57158d 100644 --- a/docs/source/devices/pylon.rst +++ b/docs/source/devices/pylon.rst @@ -10,7 +10,6 @@ This device allows control of Basler scientific cameras via the `Pylon API ` device, the bulk of camera configuration i 2. Mirroring the Pylon Viewer parameter names and values. 3. Connecting to the camera with a minimal configuration, viewing the current parameters dictionary, and copying the relevant values to the connection table (preferred). -This is an example configuration for a GigE camera. +Below are generic configurations for GigE and USB3 based cameras. .. code-block:: python + + from labscript import * from labscript_devices.PylonCamera.labscript_devices import PylonCamera PylonCamera('gigeCamera',parent_device=parent,connection=conn, serial_number=1234567, # set to the camera serial number - minimum_recovery_time=36e-6, # the minimum exposure time depends on the camera model & configuration - camera_attributs={}, - manual_camera_attributes={}) - - start() - - gigeCamera.expose(t=0.5,'exposure1') - - stop(1) - -This is an example configuration for a USB3 camera. - -.. code-block:: python - - from labscript_devices.PylonCamera.labscript_devices import PylonCamera + minimum_recovery_time=20e-3, # the minimum exposure time depends on the camera model & configuration + camera_attributs={ + 'ExposureTimeAbs':1000, #in us + 'ExposureMode':'Timed', + 'ExposureAuto':'Off', + 'GainAuto':'Off', + 'PixelFormat':'Mono12Packed', + 'Gamma':1.0, + 'BlackLevelRaw':0, + 'TriggerSource':'Line 1', + 'TriggerMode':'On' + }, + manual_camera_attributes={ + 'TriggerSource':'Software', + 'TriggerMode':'Off' + }) PylonCamera('usb3Camera',parent_device=parent,connection=conn, - serial_number=1234567, - minimum_recovery_time=36e-6, - camera_attributs={}, - manual_camera_attributes={}) + serial_number=12345678, + minimum_recovery_time=36e-3, + camera_attributs={ + 'ExposureTime':1000, #in us + 'ExposureMode':'Timed', + 'ExposureAuto':'Off', + 'GainAuto':'Off', + 'PixelFormat':'Mono12Packed', + 'Gamma':1.0, + 'BlackLevel':0, + 'TriggerSource':'Line 1', + 'TriggerMode':'On' + }, + manual_camera_attributes={ + 'TriggerSource':'Software', + 'TriggerMode':'Off' + }) start() - usb3Camera.expose(t=0.5,'exposure1') + gigeCamera.expose(t=0.5,'exposure1') + + usb3Camera.expose(t=0.45,'exposure2') stop(1) @@ -102,9 +119,3 @@ Detailed Documentation :undoc-members: :show-inheritance: :private-members: - -.. automodule:: labscript_devices.PylonCamera.register_classes - :members: - :undoc-members: - :show-inheritance: - :private-members: \ No newline at end of file diff --git a/docs/source/devices/tekscope.rst b/docs/source/devices/tekscope.rst index e0204d49..f71cea4f 100644 --- a/docs/source/devices/tekscope.rst +++ b/docs/source/devices/tekscope.rst @@ -1,2 +1,49 @@ Tektronix Oscilloscope -====================== \ No newline at end of file +====================== + +A device for controlling Tektronix oscilloscopes using the standard VISA interface. + +.. autosummary:: + labscript_devices.TekScope.labscript_devices + labscript_devices.TekScope.blacs_tabs + labscript_devices.TekScope.blacs_workers + labscript_devices.TekScope.TekScope + +Installation +~~~~~~~~~~~~ + +This wrapper requires PyVISA and a compatible VISA installation. Free versions are +provided by NI and Keysight (NI preferred if already using NI DAQs). + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.TekScope + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.TekScope.labscript_devices + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.TekScope.blacs_tabs + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.TekScope.blacs_workers + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.TekScope.TekScope + :members: + :undoc-members: + :show-inheritance: + :private-members: \ No newline at end of file diff --git a/docs/source/devices/testdevice.rst b/docs/source/devices/testdevice.rst index faae360f..9a1c1165 100644 --- a/docs/source/devices/testdevice.rst +++ b/docs/source/devices/testdevice.rst @@ -1,2 +1,14 @@ Test Device -=========== \ No newline at end of file +=========== + +A generic test device to aid in testing labscript infrastructure/functionality. + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.test_device + :members: + :undoc-members: + :show-inheritance: + :member-order: bysource + :private-members: \ No newline at end of file diff --git a/docs/source/devices/zaberstagecontroller.rst b/docs/source/devices/zaberstagecontroller.rst index e4d08da2..11e031d4 100644 --- a/docs/source/devices/zaberstagecontroller.rst +++ b/docs/source/devices/zaberstagecontroller.rst @@ -1,2 +1,44 @@ Zaber Stage Controller ====================== + +Device for controlling a Zaber translation stage. + +.. autosummary:: + labscript_devices.ZaberStageController.labscript_devices + labscript_devices.ZaberStageController.blacs_tabs + labscript_devices.ZaberStageController.blacs_workers + labscript_devices.ZaberStageController.utils + + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.ZaberStageController + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.ZaberStageController.labscript_devices + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.ZaberStageController.blacs_tabs + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.ZaberStageController.blacs_workers + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.ZaberStageController.utils + :members: + :undoc-members: + :show-inheritance: + :private-members: diff --git a/labscript_devices/DummyIntermediateDevice.py b/labscript_devices/DummyIntermediateDevice.py index 768fa015..8ba3f046 100644 --- a/labscript_devices/DummyIntermediateDevice.py +++ b/labscript_devices/DummyIntermediateDevice.py @@ -11,16 +11,43 @@ # # ##################################################################### +""" +Overview +~~~~~~~~ -# This file represents a dummy labscript device for purposes of testing BLACS -# and labscript. The device is a Intermediate Device, and can be attached to -# a pseudoclock in labscript in order to test the pseudoclock behaviour -# without needing a real Intermediate Device. -# -# You can attach an arbitrary number of outputs to this device, however we -# currently only support outputs of type AnalogOut and DigitalOut. I would be -# easy to extend this is anyone needed further functionality. +This file represents a dummy labscript device for purposes of testing BLACS +and labscript. The device is a Intermediate Device, and can be attached to +a pseudoclock in labscript in order to test the pseudoclock behaviour +without needing a real Intermediate Device. +You can attach an arbitrary number of outputs to this device, however we +currently only support outputs of type AnalogOut and DigitalOut. I would be +easy to extend this is anyone needed further functionality. + +Usage +~~~~~ + +.. code-block:: python + + from labscript import * + + from labscript_devices.DummyPseudoclock.labscript_devices import DummyPseudoclock + from labscript_devices.DummyIntermediateDevice import DummyIntermediateDevice + + DummyPseudoclock(name='dummy_clock',BLACS_connection='dummy') + DummyIntermediateDevice(name='dummy_device',BLACS_connection='dummy2', + parent_device=dummy_clock.clockline) + + DigitalOut(name='do1',parent_device=dummy_device,connection='dummy_do1') + DigitalOut(name='do2',parent_device=dummy_device,connection='dummy_do2') + + start() + stop(1) + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +""" from labscript_devices import labscript_device, BLACS_tab, BLACS_worker from labscript import IntermediateDevice, DigitalOut, AnalogOut, config From 6ce351c8b045097af883122b00d6da1af4ecd13e Mon Sep 17 00:00:00 2001 From: David Meyer Date: Wed, 4 Nov 2020 17:17:23 -0500 Subject: [PATCH 04/28] Adding boiler plate for rest of Pseudoclocks. --- docs/source/devices/opalkellyXEM3001.rst | 18 ++- docs/source/devices/pineblaster.rst | 13 ++- docs/source/devices/pulseblaster.rst | 4 +- docs/source/devices/pulseblaster_no_dds.rst | 121 +++++++++++++++++++- docs/source/devices/rfblaster.rst | 11 ++ 5 files changed, 162 insertions(+), 5 deletions(-) diff --git a/docs/source/devices/opalkellyXEM3001.rst b/docs/source/devices/opalkellyXEM3001.rst index ac30202a..58798d60 100644 --- a/docs/source/devices/opalkellyXEM3001.rst +++ b/docs/source/devices/opalkellyXEM3001.rst @@ -1,2 +1,18 @@ Cicero Opal-Kelly XEM3001 -========================= \ No newline at end of file +========================= + +A pseudoclocking labscript device based on the OpalKelly XEM3001 integration module, which uses a Xilinx Spartan-3 FPGA. + +Installation +~~~~~~~~~~~~ + +Firmware (.bit) files for the FPGA are available `here `_ and should be placed in the labscript_devices folder along with the `CiceroOpalKellyXEM3001.py` file. The Opal Kelly SDK, which provides the python bindings, is also required. The python bindings will need to either be added to the PATH or manually copied to the site-packages of the virtual environment that BLACS is running in. + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.CiceroOpalKellyXEM3001 + :members: + :undoc-members: + :show-inheritance: + :private-members: \ No newline at end of file diff --git a/docs/source/devices/pineblaster.rst b/docs/source/devices/pineblaster.rst index 07cf4491..af054413 100644 --- a/docs/source/devices/pineblaster.rst +++ b/docs/source/devices/pineblaster.rst @@ -1,2 +1,13 @@ Pineblaster -=========== \ No newline at end of file +=========== + +This labscript device controls the `PineBlaster `_ open-source digital pattern generator based on the Digilent chipKIT Max32 Prototyping platform. + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.PineBlaster + :members: + :undoc-members: + :show-inheritance: + :private-members: \ No newline at end of file diff --git a/docs/source/devices/pulseblaster.rst b/docs/source/devices/pulseblaster.rst index a2c7a528..cbdf1ae7 100644 --- a/docs/source/devices/pulseblaster.rst +++ b/docs/source/devices/pulseblaster.rst @@ -1,9 +1,9 @@ Pulseblaster ============ -This labscript device controls the Spincore PulseblaserDDS-II-300-AWG. The Pulseblaster is a programmable pulse generator that is the typical timing backbone of an experiment (ie it generates the pseudoclock timing pulses that control execution of other devices in the experiment). This labscript device is the master implementation of the various Pulseblaster devices. +This labscript device controls the Spincore PulseblaserDDS-II-300-AWG. The Pulseblaster is a programmable pulse generator that is the typical timing backbone of an experiment (ie it generates the pseudoclock timing pulses that control execution of other devices in the experiment). This labscript device is the master implementation of the various Pulseblaster devices. Other Pulseblaster labscript devices subclass this device and make the relevant changes to hard-coded values. Most importantly, the `core_clock_freq` must be manually set to match that of the Pulseblaster being used in order for the timing of the programmed pulses to be correct (in the `labscript_device` and the `BLACS_worker`). -This particular version of Pulseblaster also has DDS synthesizer outputs. +This particular version of Pulseblaster has a 75 MHz core clock frequency and also has DDS synthesizer outputs. Installation ~~~~~~~~~~~~ diff --git a/docs/source/devices/pulseblaster_no_dds.rst b/docs/source/devices/pulseblaster_no_dds.rst index 173c56cd..e965e216 100644 --- a/docs/source/devices/pulseblaster_no_dds.rst +++ b/docs/source/devices/pulseblaster_no_dds.rst @@ -1,2 +1,121 @@ Pulseblaster (-DDS) -=================== \ No newline at end of file +=================== + +Overview +~~~~~~~~ + +This labscript device controls the Spincore Pulseblasers that do not have DDS outputs. The Pulseblaster is a programmable pulse generator that is the typical timing backbone of an experiment (ie it generates the pseudoclock timing pulses that control execution of other devices in the experiment). This labscript device inherits from the :doc:`Pulseblaster ` device. The primary difference is the removal of code handling DDS outputs. + +The labscript-suite currently supports a number of no-dds variants of the Pulseblaster device, each with different numbers of outputs and clock frequencies: + + * `PulseBlaster_No_DDS`: Has 24 digital outputs and a 100 MHz core clock frequency. + * `PulseBlasterUSB`: Identical to the `PulseBlaster_No_DDS` device + * `PulseBlaster_SP2_24_100_32k`: Has slightly lower `clock_limit` and `clock_resolution` than the standard device. Also supports 32k instructions instead of the standard 4k. + * `PulseBlasterESRPro200`: Has a 200 MHz core clock frequency. + * `PulseBlasterESRPro500`: Has a 500 MHz core clock frequency. + +ESR-Pro PulseBlasters +^^^^^^^^^^^^^^^^^^^^^ + +The timing resolution of a normal PulseBlaster is one clock cycle, the minimum interval is typically limited to 5 clock cycles (or nine in the case of the external memory models like the 32k). +The ESR-Pro series of PulseBlasters have the Short Pulse Feature, which allows for pulse lengths of 1-5 clock periods. This is controlled using the top three bits (21-23) according to the following table. + +.. csv-table:: Short Pulse Control + :header: "SpinAPI Define", "Bits 21-23", "Clock Periods", "Pulse Length (ns) at 500 MHz" + :widths: auto + :align: center + + \- , 000, \- , "All outputs low" + "ONE_PERIOD", 001, 1, 2 + "TWO_PERIOD", 010, 2, 4 + "THREE_PERIOD", 011, 3, 6 + "FOUR_PERIOD", 100, 4, 8 + "FIVE_PERIOD", 101, 5, 10 + "ON", 111, \- , "Short Pulse Disabled" + +Currently, the PulseBlaster labscript device does not use this functionality. However, in order to get any output at all, bits 21-23 must be set high manually. + + +Installation +~~~~~~~~~~~~ + +Use of the Pulseblaster requires driver installation available from the manufacturer `here `_. The corresponding python wrapper, `spinapi `_ is available via pip. + +.. code-block:: bash + + pip install -U spinapi + +Usage +~~~~~ + +.. code-block:: python + + from labscript import * + + from labscript_devices.PulseBlaster import PulseBlaster + + PulseBlaster(name='pb',board_number=0,programming_scheme='pb_start/BRANCH') + + Clockline(name='pb_clockline_fast', pseudoclock=pb.pseudoclock,connection='flag 0') + Clockline(name='pb_clockline_slow', pseudoclock=pb.pseudoclock,connection='flag 1') + + DigitalOut(name='pb_0',parent_device=pb.direct_outputs,connection='flag 2') + + start() + + stop(1) + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. autosummary:: + labscript_devices.PulseBlaster_No_DDS + labscript_devices.PulseBlasterUSB + labscript_devices.PulseBlaster_SP2_24_100_32k + labscript_devices.PulseBlasterESRPro200 + labscript_devices.PulseBlasterESRPro500 + +PulseBlaster_No_DDS +^^^^^^^^^^^^^^^^^^^ + +.. automodule:: labscript_devices.PulseBlaster_No_DDS + :members: + :undoc-members: + :show-inheritance: + :private-members: + +PulseBlasterUSB +^^^^^^^^^^^^^^^ + +.. automodule:: labscript_devices.PulseBlasterUSB + :members: + :undoc-members: + :show-inheritance: + :private-members: + +PulseBlaster_SP2_24_100_32k +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: labscript_devices.PulseBlaster_SP2_24_100_32k + :members: + :undoc-members: + :show-inheritance: + :private-members: + +PulseBlasterESRPro200 +^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: labscript_devices.PulseBlasterESRPro200 + :members: + :undoc-members: + :show-inheritance: + :private-members: + +PulseBlasterESRPro500 +^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: labscript_devices.PulseBlasterESRPro500 + :members: + :undoc-members: + :show-inheritance: + :private-members: \ No newline at end of file diff --git a/docs/source/devices/rfblaster.rst b/docs/source/devices/rfblaster.rst index c25fb389..23132177 100644 --- a/docs/source/devices/rfblaster.rst +++ b/docs/source/devices/rfblaster.rst @@ -1,2 +1,13 @@ RFblaster ========= + +Another pseudoclock-cable labscript device. + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.RFBlaster + :members: + :undoc-members: + :show-inheritance: + :private-members: \ No newline at end of file From 03249952636c02d29823b0a102b23b1b474d78d0 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Fri, 6 Nov 2020 18:29:43 -0500 Subject: [PATCH 05/28] Add docs for the SpinnakerCamera device I somehow overlooked. --- docs/source/devices.rst | 1 + docs/source/devices/flycapture2.rst | 4 +- docs/source/devices/spinnaker.rst | 76 +++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 docs/source/devices/spinnaker.rst diff --git a/docs/source/devices.rst b/docs/source/devices.rst index 79a62b87..e00d3ddd 100644 --- a/docs/source/devices.rst +++ b/docs/source/devices.rst @@ -39,6 +39,7 @@ The camera devices provide interfaces for using various scientific cameras to ac devices/IMAQdx devices/pylon devices/flycapture2 + devices/spinnaker devices/andorsolis diff --git a/docs/source/devices/flycapture2.rst b/docs/source/devices/flycapture2.rst index 9dea4323..3cfb7453 100644 --- a/docs/source/devices/flycapture2.rst +++ b/docs/source/devices/flycapture2.rst @@ -3,6 +3,8 @@ FlyCapture2 Cameras This device allows control of FLIR (formerly Point Grey) scientific cameras via the `FlyCapture2 SDK `_ with the now deprecated PyCapture2 wrapper. In order to use this device, both the SDK and the python wrapper must be installed. Note that PyCapture2 only supports up to Python 3.6. +The new FLIR SDK is supported using the :doc:`SpinnakerCamera labscript device `. + .. autosummary:: labscript_devices.FlyCapture2Camera.labscript_devices labscript_devices.FlyCapture2Camera.blacs_tabs @@ -13,7 +15,7 @@ Installation First ensure that the FlyCapture2 SDK is installed. -The python wrapper is available via FLIR and is only released for Python up to 3.6. It must be installed separately to pointed to the correct conda environment during install. +The python wrapper is available via FLIR and is only released for Python up to 3.6. It must be installed separately, pointed to the correct conda environment during install. For GigE cameras, ensure that the network interface card (NIC) on the computer with the BLACS controlling the camera has enabled Jumbo Frames. That maximum allowed value (typically 9000) is preferable to avoid dropped frames. diff --git a/docs/source/devices/spinnaker.rst b/docs/source/devices/spinnaker.rst new file mode 100644 index 00000000..d6ae8808 --- /dev/null +++ b/docs/source/devices/spinnaker.rst @@ -0,0 +1,76 @@ +Spinnaker Cameras +================= + +This device allows control of FLIR scientific cameras via the `Spinnaker SDK `_ with the PySpin wrapper. In order to use this device, both the SDK and the python wrapper must be installed. + +.. autosummary:: + labscript_devices.SpinnakerCamera.labscript_devices + labscript_devices.SpinnakerCamera.blacs_tabs + labscript_devices.SpinnakerCamera.blacs_workers + +Installation +~~~~~~~~~~~~ + +First ensure that the Spinnaker SDK is installed. + +The python wrapper is available via FLIR. It must be installed separately and pointed to the correct conda environment during install. + +For GigE cameras, ensure that the network interface card (NIC) on the computer with the BLACS controlling the camera has enabled Jumbo Frames. That maximum allowed value (typically 9000) is preferable to avoid dropped frames. + +Usage +~~~~~ + +Like the :doc:`IMAQdxCamera ` device, the bulk of camera configuration is performed using a dictionary of kwargs, where the key names and values mirror those provided by the Spinnaker SDK interface. Which parameters can/need to be set depend on the communication interface. Discovery of what parameters are available can be done in three ways: + +1. Careful reading of the Spinnaker SDK docs. +2. Mirroring the SpinView parameter names and values. +3. Connecting to the camera with a minimal configuration, viewing the current parameters dictionary, and copying the relevant values to the connection table (preferred). + +Below is a generic configuration. + +.. code-block:: python + + from labscript import * + + from labscript_devices.SpinnakerCamera.labscript_devices import SpinnakerCamera + + SpinnakerCamera('gigeCamera',parent_device=parent,connection=conn, + serial_number=1234567, # set to the camera serial number + minimum_recovery_time=36e-6, # the minimum exposure time depends on the camera model & configuration + camera_attributs={}, + manual_camera_attributes={}) + + start() + + gigeCamera.expose(t=0.5,'exposure1') + + + stop(1) + + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.SpinnakerCamera + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.SpinnakerCamera.labscript_devices + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.SpinnakerCamera.blacs_tabs + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.SpinnakerCamera.blacs_workers + :members: + :undoc-members: + :show-inheritance: + :private-members: From 68d6837497b9b99db21487ec76c63cf937ffeee5 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Fri, 6 Nov 2020 21:24:47 -0500 Subject: [PATCH 06/28] Protecting the NI_DAQ.models.get_capabilities script against import. Now when sphinx imports it, it doesn't try to run NI-MAX device discovery. --- .../NI_DAQmx/models/get_capabilities.py | 228 +++++++++--------- 1 file changed, 114 insertions(+), 114 deletions(-) diff --git a/labscript_devices/NI_DAQmx/models/get_capabilities.py b/labscript_devices/NI_DAQmx/models/get_capabilities.py index 471491b0..c05c2808 100644 --- a/labscript_devices/NI_DAQmx/models/get_capabilities.py +++ b/labscript_devices/NI_DAQmx/models/get_capabilities.py @@ -290,124 +290,124 @@ def get_min_semiperiod_measurement(device_name): return dtmin return dtmin - -capabilities = {} -if os.path.exists(CAPABILITIES_FILE): - with open(CAPABILITIES_FILE) as f: +if __name__ == '__main__': + capabilities = {} + if os.path.exists(CAPABILITIES_FILE): + with open(CAPABILITIES_FILE) as f: + try: + capabilities = json.load(f) + except ValueError: + pass + + + models = [] + for name in DAQmxGetSysDevNames().split(', '): + model = DAQmxGetDevProductType(name) + print("found device:", name, model) + if model not in models: + models.append(model) + capabilities[model] = {} try: - capabilities = json.load(f) - except ValueError: - pass - - -models = [] -for name in DAQmxGetSysDevNames().split(', '): - model = DAQmxGetDevProductType(name) - print("found device:", name, model) - if model not in models: - models.append(model) - capabilities[model] = {} - try: - capabilities[model]["supports_buffered_AO"] = DAQmxGetDevAOSampClkSupported( - name - ) - except PyDAQmx.DAQmxFunctions.AttrNotSupportedError: - capabilities[model]["supports_buffered_AO"] = False - try: - capabilities[model]["max_DO_sample_rate"] = DAQmxGetDevDOMaxRate(name) - capabilities[model]["supports_buffered_DO"] = True - except PyDAQmx.DAQmxFunctions.AttrNotSupportedError: - capabilities[model]["max_DO_sample_rate"] = None - capabilities[model]["supports_buffered_DO"] = False - if capabilities[model]["supports_buffered_AO"]: - capabilities[model]["max_AO_sample_rate"] = DAQmxGetDevAOMaxRate(name) - else: - capabilities[model]["max_AO_sample_rate"] = None - - capabilities[model]["num_AO"] = len(DAQmxGetDevAOPhysicalChans(name)) - capabilities[model]["num_AI"] = len(DAQmxGetDevAIPhysicalChans(name)) - if capabilities[model]["num_AI"] > 0: - single_rate = DAQmxGetDevAIMaxSingleChanRate(name) - multi_rate = DAQmxGetDevAIMaxMultiChanRate(name) - else: - single_rate = None - multi_rate = None - capabilities[model]["max_AI_single_chan_rate"] = single_rate - capabilities[model]["max_AI_multi_chan_rate"] = multi_rate - - capabilities[model]["ports"] = {} - ports = DAQmxGetDevDOPorts(name) - chans = DAQmxGetDevDOLines(name) - for port in ports: - if '_' in port: - # Ignore the alternate port names such as 'port0_32' that allow using two or - # more ports together as a single, larger one: - continue - port_info = {} - capabilities[model]["ports"][port] = port_info - port_chans = [chan for chan in chans if chan.split('/')[0] == port] - port_info['num_lines'] = len(port_chans) - if capabilities[model]["supports_buffered_DO"]: - port_info['supports_buffered'] = port_supports_buffered(name, port) + capabilities[model]["supports_buffered_AO"] = DAQmxGetDevAOSampClkSupported( + name + ) + except PyDAQmx.DAQmxFunctions.AttrNotSupportedError: + capabilities[model]["supports_buffered_AO"] = False + try: + capabilities[model]["max_DO_sample_rate"] = DAQmxGetDevDOMaxRate(name) + capabilities[model]["supports_buffered_DO"] = True + except PyDAQmx.DAQmxFunctions.AttrNotSupportedError: + capabilities[model]["max_DO_sample_rate"] = None + capabilities[model]["supports_buffered_DO"] = False + if capabilities[model]["supports_buffered_AO"]: + capabilities[model]["max_AO_sample_rate"] = DAQmxGetDevAOMaxRate(name) else: - port_info['supports_buffered'] = False + capabilities[model]["max_AO_sample_rate"] = None - capabilities[model]["num_CI"] = len(DAQmxGetDevCIPhysicalChans(name)) - supports_semiperiod = supports_semiperiod_measurement(name) - capabilities[model]["supports_semiperiod_measurement"] = supports_semiperiod - if capabilities[model]["num_CI"] > 0 and supports_semiperiod: - min_semiperiod_measurement = get_min_semiperiod_measurement(name) - else: - min_semiperiod_measurement = None - capabilities[model]["min_semiperiod_measurement"] = min_semiperiod_measurement - - if capabilities[model]['num_AO'] > 0: - AO_ranges = [] - raw_limits = DAQmxGetDevAOVoltageRngs(name) - for i in range(0, len(raw_limits), 2): - Vmin, Vmax = raw_limits[i], raw_limits[i + 1] - AO_ranges.append([Vmin, Vmax]) - # Find range with the largest maximum voltage and use that: - Vmin, Vmax = max(AO_ranges, key=lambda range: range[1]) - # Confirm that no other range has a voltage lower than Vmin, - # since if it does, this violates our assumptions and things might not - # be as simple as having a single range: - assert min(AO_ranges)[0] >= Vmin - capabilities[model]["AO_range"] = [Vmin, Vmax] - else: - capabilities[model]["AO_range"] = None - - if capabilities[model]['num_AI'] > 0: - AI_ranges = [] - raw_limits = DAQmxGetDevAIVoltageRngs(name) - for i in range(0, len(raw_limits), 2): - Vmin, Vmax = raw_limits[i], raw_limits[i + 1] - AI_ranges.append([Vmin, Vmax]) - # Restrict to the ranges allowed for non-differential input: - AI_ranges = supported_AI_ranges_for_non_differential_input(name, AI_ranges) - # Find range with the largest maximum voltage and use that: - Vmin, Vmax = max(AI_ranges, key=lambda range: range[1]) - # Confirm that no other range has a voltage lower than Vmin, - # since if it does, this violates our assumptions and things might not - # be as simple as having a single range: - assert min(AI_ranges)[0] >= Vmin - capabilities[model]["AI_range"] = [Vmin, Vmax] - else: - capabilities[model]["AI_range"] = None + capabilities[model]["num_AO"] = len(DAQmxGetDevAOPhysicalChans(name)) + capabilities[model]["num_AI"] = len(DAQmxGetDevAIPhysicalChans(name)) + if capabilities[model]["num_AI"] > 0: + single_rate = DAQmxGetDevAIMaxSingleChanRate(name) + multi_rate = DAQmxGetDevAIMaxMultiChanRate(name) + else: + single_rate = None + multi_rate = None + capabilities[model]["max_AI_single_chan_rate"] = single_rate + capabilities[model]["max_AI_multi_chan_rate"] = multi_rate + + capabilities[model]["ports"] = {} + ports = DAQmxGetDevDOPorts(name) + chans = DAQmxGetDevDOLines(name) + for port in ports: + if '_' in port: + # Ignore the alternate port names such as 'port0_32' that allow using two or + # more ports together as a single, larger one: + continue + port_info = {} + capabilities[model]["ports"][port] = port_info + port_chans = [chan for chan in chans if chan.split('/')[0] == port] + port_info['num_lines'] = len(port_chans) + if capabilities[model]["supports_buffered_DO"]: + port_info['supports_buffered'] = port_supports_buffered(name, port) + else: + port_info['supports_buffered'] = False + + capabilities[model]["num_CI"] = len(DAQmxGetDevCIPhysicalChans(name)) + supports_semiperiod = supports_semiperiod_measurement(name) + capabilities[model]["supports_semiperiod_measurement"] = supports_semiperiod + if capabilities[model]["num_CI"] > 0 and supports_semiperiod: + min_semiperiod_measurement = get_min_semiperiod_measurement(name) + else: + min_semiperiod_measurement = None + capabilities[model]["min_semiperiod_measurement"] = min_semiperiod_measurement + + if capabilities[model]['num_AO'] > 0: + AO_ranges = [] + raw_limits = DAQmxGetDevAOVoltageRngs(name) + for i in range(0, len(raw_limits), 2): + Vmin, Vmax = raw_limits[i], raw_limits[i + 1] + AO_ranges.append([Vmin, Vmax]) + # Find range with the largest maximum voltage and use that: + Vmin, Vmax = max(AO_ranges, key=lambda range: range[1]) + # Confirm that no other range has a voltage lower than Vmin, + # since if it does, this violates our assumptions and things might not + # be as simple as having a single range: + assert min(AO_ranges)[0] >= Vmin + capabilities[model]["AO_range"] = [Vmin, Vmax] + else: + capabilities[model]["AO_range"] = None + + if capabilities[model]['num_AI'] > 0: + AI_ranges = [] + raw_limits = DAQmxGetDevAIVoltageRngs(name) + for i in range(0, len(raw_limits), 2): + Vmin, Vmax = raw_limits[i], raw_limits[i + 1] + AI_ranges.append([Vmin, Vmax]) + # Restrict to the ranges allowed for non-differential input: + AI_ranges = supported_AI_ranges_for_non_differential_input(name, AI_ranges) + # Find range with the largest maximum voltage and use that: + Vmin, Vmax = max(AI_ranges, key=lambda range: range[1]) + # Confirm that no other range has a voltage lower than Vmin, + # since if it does, this violates our assumptions and things might not + # be as simple as having a single range: + assert min(AI_ranges)[0] >= Vmin + capabilities[model]["AI_range"] = [Vmin, Vmax] + else: + capabilities[model]["AI_range"] = None - if capabilities[model]["num_AI"] > 0: - capabilities[model]["AI_start_delay"] = AI_start_delay(name) - else: - capabilities[model]["AI_start_delay"] = None + if capabilities[model]["num_AI"] > 0: + capabilities[model]["AI_start_delay"] = AI_start_delay(name) + else: + capabilities[model]["AI_start_delay"] = None -with open(CAPABILITIES_FILE, 'w', newline='\n') as f: - data = json.dumps(capabilities, sort_keys=True, indent=4) - f.write(data) + with open(CAPABILITIES_FILE, 'w', newline='\n') as f: + data = json.dumps(capabilities, sort_keys=True, indent=4) + f.write(data) -print("added/updated capabilities for %d models" % len(models)) -print("Total models with known capabilities: %d" % len(capabilities)) -for model in capabilities: - if model not in models: - print(model, 'capabilities not updated') -print("run generate_subclasses.py to make labscript devices for these models") + print("added/updated capabilities for %d models" % len(models)) + print("Total models with known capabilities: %d" % len(capabilities)) + for model in capabilities: + if model not in models: + print(model, 'capabilities not updated') + print("run generate_subclasses.py to make labscript devices for these models") From b5071bd3e423ff5a9e865fc6ac8defcdf8c01ce6 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Fri, 6 Nov 2020 21:35:41 -0500 Subject: [PATCH 07/28] Add NI-DAQmx documentation. This employs apidoc to auto-generate a custom templated summary of all the auto-generated sub-class models. It modifies conf.py to auto-run apidoc to create the template. Therefore new sub-classes that are added will automatically have the appropriate documentation generated. Note: apidoc creates a large number of cruft docfiles for all modules in labscript_devices using this specific custom template. This is because I haven't figured out how to get the autodoc directives to work without full specification of the module from the top level. This could be worked around by the apidoc exclude option, but it only uses very basic fnmatch syntax that can't seem to be fudged to only select a single submodule. --- .gitignore | 1 + docs/source/_templates/models/package.rst_t | 47 +++++++ docs/source/conf.py | 17 ++- docs/source/devices/ni_daq_models.rst | 4 + docs/source/devices/ni_daqs.rst | 141 +++++++++++++++++++- 5 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 docs/source/_templates/models/package.rst_t create mode 100644 docs/source/devices/ni_daq_models.rst diff --git a/.gitignore b/.gitignore index 85a021d2..8fc6bd7d 100644 --- a/.gitignore +++ b/.gitignore @@ -159,3 +159,4 @@ conda_packages/ docs/html/ docs/source/_build/ docs/source/components.rst +docs/source/devices/_apidoc diff --git a/docs/source/_templates/models/package.rst_t b/docs/source/_templates/models/package.rst_t new file mode 100644 index 00000000..ea933851 --- /dev/null +++ b/docs/source/_templates/models/package.rst_t @@ -0,0 +1,47 @@ +{%- macro automodule(modname, options) -%} +.. automodule:: {{ modname }} +{%- for option in options %} + :{{ option }}: +{%- endfor %} +{%- endmacro %} + +{%- macro toctree(docnames) -%} +.. toctree:: + :maxdepth: {{ maxdepth }} +{% for docname in docnames %} + {{ docname }} +{%- endfor %} +{%- endmacro %} + +{%- macro autosummary(submodules) -%} +.. autosummary:: +{% for submodule in submodules %} + {{submodule}} +{%- endfor %} +{%- endmacro %} + +{%- macro autosum(pkgname) -%} +.. autosummary:: + + {{pkgname}} +{%- endmacro %} + +{%- if is_namespace %} +{{- [pkgname, "namespace"] | join(" ") | e | heading }} +{% else %} +{# {{- [pkgname, ""] | join(" ") | e | heading }} #} +{% endif %} + +{%- if submodules %} +{{ autosummary(submodules) }} +{% if separatemodules %} +{{ toctree(submodules) }} +{% else %} +{%- for submodule in submodules %} +{% if show_headings %} +{{- [submodule, ""] | join(" ") | e | heading(2) }} +{% endif %} +{{ automodule(submodule, automodule_options) }} +{% endfor %} +{%- endif %} +{%- endif %} diff --git a/docs/source/conf.py b/docs/source/conf.py index e2d4a0f8..916e9df3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -50,7 +50,7 @@ ] autodoc_typehints = 'description' -autoclass_content = 'init' +autoclass_content = 'both' # options: 'both', 'class', 'init' # Prefix each autosectionlabel with the name of the document it is in and a colon autosectionlabel_prefix_document = True @@ -225,3 +225,18 @@ def setup(app): img_path=img_path ) ) + + # hook to run apidoc before building + app.connect('builder-inited',run_apidoc) + +def run_apidoc(_): + """Runs apidoc with our desired parameters to generate the NI_DAQmx models docs. + + Also manually strips out some irrelevant stuff.""" + from sphinx.ext.apidoc import main + daq_models_path = os.path.join(os.path.abspath('..'),'labscript_devices') + out_path = os.path.join(os.path.dirname(Path(__file__)),'devices','_apidoc') + templates_path = os.path.join(os.path.dirname(Path(__file__)),'_templates') + main(['-TMf','-s','inc', + '-t', templates_path, + '-o',out_path,daq_models_path]) diff --git a/docs/source/devices/ni_daq_models.rst b/docs/source/devices/ni_daq_models.rst new file mode 100644 index 00000000..3f7e32d7 --- /dev/null +++ b/docs/source/devices/ni_daq_models.rst @@ -0,0 +1,4 @@ +Sub-Classed NI DAQ Models +========================= + +.. include:: _apidoc/labscript_devices.NI_DAQmx.models.inc diff --git a/docs/source/devices/ni_daqs.rst b/docs/source/devices/ni_daqs.rst index 1fc20a0b..d6a633a5 100644 --- a/docs/source/devices/ni_daqs.rst +++ b/docs/source/devices/ni_daqs.rst @@ -1,2 +1,139 @@ -NI DAQS -======= \ No newline at end of file +NI DAQs +======= + +Overview +~~~~~~~~ + +This labscript device is a master device that can control a wide range of NI Multi-function data acquistion devices. + +Installation +~~~~~~~~~~~~ + +This labscript device requires an installation of the NI-DAQmx module, available for free from `NI `_. + +The python bindings are provided by the PyDAQmx package, available through pip. + + +Adding a Device +~~~~~~~~~~~~~~~ + +While the `NI_DAQmx` device can be used directly by manually specifying the many necessary parameters, it is preferable to add the device via an appropriate subclass. This process is greatly simplified by using the `get_capabilities.py` script. + +To add support for a DAQmx device that is not yet supported, run `get_capabilities.py` on +a computer with the device in question connected (or with a simulated device of the +correct model configured in NI-MAX). This will introspect the capabilities of the device +and add those details to capabilities.json. To generate labscript device classes for all +devices whose capabilities are known, run `generate_classes.py`. Subclasses of NI_DAQmx +will be made in the `models` subfolder, and they can then be imported into labscript code with: + +..code-block:: python + + from labscript_devices.NI_DAQmx.labscript_devices import NI_PCIe_6363 + +or similar. The class naming is based on the model name by prepending "NI\_" and +replacing the hyphen with an underscore, i.e. 'PCIe-6363' -> NI_PCIe_6363. + +Generating device classes requires the Python code-formatting library 'black', which can +be installed via pip (Python 3.6+ only). If you don't want to install this library, the +generation code will still work, it just won't be formatted well. + +The current list of pre-subclassed devices is: + +.. toctree:: + :maxdepth: 2 + + ni_daq_models + + +Usage +~~~~~ + +NI Multifunction DAQs generally provide hardware channels for the :ref:`StaticAnalogOut `, :ref:`StaticDigitalOut `, :ref:`AnalogOut `, :ref:`DigitalOut `, and :ref:`AnalogIn ` labscript quantities for use in experiments. Exact numbers of channels, performance, and configuration depend on the model of DAQ used. + +.. code-block:: python + + from labscript import * + + from labscript_devices.DummyPseudoclock.labscript_devices import DummyPseudoclock + from labscript_devices.NI_DAQmx.models.NI_USB_6343 import NI_USB_6343 + + DummyPseudoclock('dummy_clock',BLACS_connection='dummy') + + NI_USB_6343(name='daq',parent_device=dummy_clock.clockline, + MAX_name='ni_usb_6343', + clock_terminal='/ni_usb_6343/PFI0', + acquisition_rate=100e3) + + AnalogIn('daq_ai0',daq,'ai0') + AnalogIn('daq_ai1',daq,'ai1') + + AnalogOut('daq_ao0',daq,'ao0') + AnalogIn('daq_ai1',daq,'ai1') + +NI DAQs are also used within labscript to provide a :ref:`WaitMonitor `. When configured, the `WaitMonitor` allows for arbitrary-length pauses in experiment execution, waiting for some trigger to restart. The monitor provides a measurement of the duration of the wait for use in interpreting the resulting data from the experiment. + +Configuration uses three digital I/O connections on the DAQ: + +* The parent_connection which sends pulses at the beginning of the experiment, the start of the wait, and the end of the wait. +* The acquisition_connection which must be wired to a counter and measures the time between the pulses of the parent connection. +* The timeout_connection which can send a restart pulse if the wait times out. + +An example configuration of a `WaitMonitor` using a NI DAQ is shown here + +.. code-block:: python + + # A wait monitor for AC-line triggering + # This requires custom hardware + WaitMonitor(name='wait_monitor',parent_device=daq,connection='port0/line0', + acquisition_device=daq, acquisition_connection='ctr0', + timeout_device=daq, timeout_connection='PFI1') + # Necessary to ensure even number of digital out lines in shot + DigitalOut('daq_do1',daq,'port0/line1') + +Note that the counter connection is specified using the logical label `'ctr0'`. On many NI DAQs, the physical connection to this counter is PFI9. The physical wiring for this configuration would have port0/line0 wired directly to PFI9, which PFI1 being sent to the master pseudoclock retriggering system in case of timeout. If timeouts are not expect/represent experiment failure, this physical connection can be omitted. + + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.NI_DAQmx + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.NI_DAQmx.labscript_devices + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.NI_DAQmx.blacs_tabs + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.NI_DAQmx.blacs_workers + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.NI_DAQmx.runviewer_parsers + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.NI_DAQmx.daqmx_utils + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.NI_DAQmx.utils + :members: + :undoc-members: + :show-inheritance: + :private-members: From 82157780cde5f11e874f8fd3d0e8cff249050792 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Fri, 6 Nov 2020 21:36:59 -0500 Subject: [PATCH 08/28] Fix some pathing issues on the last NI-DAQmx commit. --- docs/source/conf.py | 4 ++-- docs/source/devices/ni_daq_models.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 916e9df3..7194aa4f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -235,8 +235,8 @@ def run_apidoc(_): Also manually strips out some irrelevant stuff.""" from sphinx.ext.apidoc import main daq_models_path = os.path.join(os.path.abspath('..'),'labscript_devices') - out_path = os.path.join(os.path.dirname(Path(__file__)),'devices','_apidoc') - templates_path = os.path.join(os.path.dirname(Path(__file__)),'_templates') + out_path = os.path.join(os.path.dirname(Path(__file__)),'devices','_apidoc','models') + templates_path = os.path.join(os.path.dirname(Path(__file__)),'_templates','models') main(['-TMf','-s','inc', '-t', templates_path, '-o',out_path,daq_models_path]) diff --git a/docs/source/devices/ni_daq_models.rst b/docs/source/devices/ni_daq_models.rst index 3f7e32d7..511e1c15 100644 --- a/docs/source/devices/ni_daq_models.rst +++ b/docs/source/devices/ni_daq_models.rst @@ -1,4 +1,4 @@ Sub-Classed NI DAQ Models ========================= -.. include:: _apidoc/labscript_devices.NI_DAQmx.models.inc +.. include:: _apidoc/models/labscript_devices.NI_DAQmx.models.inc From e597a0ace1f44a8f7fb2aff33e2c3da02a106671 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Fri, 6 Nov 2020 21:37:45 -0500 Subject: [PATCH 09/28] Updating the NI_DAQmx model template and auto-generated files to produce slightly more helpful documentation. --- labscript_devices/NI_DAQmx/models/NI_PCI_6251.py | 2 ++ labscript_devices/NI_DAQmx/models/NI_PCI_6534.py | 2 ++ labscript_devices/NI_DAQmx/models/NI_PCI_6713.py | 2 ++ labscript_devices/NI_DAQmx/models/NI_PCI_6733.py | 2 ++ labscript_devices/NI_DAQmx/models/NI_PCI_DIO_32HS.py | 2 ++ labscript_devices/NI_DAQmx/models/NI_PCIe_6363.py | 2 ++ labscript_devices/NI_DAQmx/models/NI_PCIe_6738.py | 2 ++ labscript_devices/NI_DAQmx/models/NI_PXI_6733.py | 2 ++ labscript_devices/NI_DAQmx/models/NI_PXIe_6361.py | 2 ++ labscript_devices/NI_DAQmx/models/NI_PXIe_6535.py | 2 ++ labscript_devices/NI_DAQmx/models/NI_PXIe_6738.py | 2 ++ labscript_devices/NI_DAQmx/models/NI_USB_6008.py | 2 ++ labscript_devices/NI_DAQmx/models/NI_USB_6229.py | 2 ++ labscript_devices/NI_DAQmx/models/NI_USB_6343.py | 2 ++ labscript_devices/NI_DAQmx/models/_subclass_template.py | 2 ++ 15 files changed, 30 insertions(+) diff --git a/labscript_devices/NI_DAQmx/models/NI_PCI_6251.py b/labscript_devices/NI_DAQmx/models/NI_PCI_6251.py index 54f2b139..e7587717 100644 --- a/labscript_devices/NI_DAQmx/models/NI_PCI_6251.py +++ b/labscript_devices/NI_DAQmx/models/NI_PCI_6251.py @@ -22,6 +22,7 @@ from labscript_devices.NI_DAQmx.labscript_devices import NI_DAQmx +#: CAPABILITIES = { 'AI_range': [-10.0, 10.0], 'AI_start_delay': 2.5e-07, @@ -49,6 +50,7 @@ class NI_PCI_6251(NI_DAQmx): description = 'NI-PCI-6251' def __init__(self, *args, **kwargs): + """Class for NI-PCI-6251""" # Any provided kwargs take precedent over capabilities combined_kwargs = CAPABILITIES.copy() combined_kwargs.update(kwargs) diff --git a/labscript_devices/NI_DAQmx/models/NI_PCI_6534.py b/labscript_devices/NI_DAQmx/models/NI_PCI_6534.py index 2cc9bd43..c0b7f9f0 100644 --- a/labscript_devices/NI_DAQmx/models/NI_PCI_6534.py +++ b/labscript_devices/NI_DAQmx/models/NI_PCI_6534.py @@ -22,6 +22,7 @@ from labscript_devices.NI_DAQmx.labscript_devices import NI_DAQmx +#: CAPABILITIES = { 'AI_range': None, 'AI_start_delay': None, @@ -52,6 +53,7 @@ class NI_PCI_6534(NI_DAQmx): description = 'NI-PCI-6534' def __init__(self, *args, **kwargs): + """Class for NI-PCI-6534""" # Any provided kwargs take precedent over capabilities combined_kwargs = CAPABILITIES.copy() combined_kwargs.update(kwargs) diff --git a/labscript_devices/NI_DAQmx/models/NI_PCI_6713.py b/labscript_devices/NI_DAQmx/models/NI_PCI_6713.py index 4e6341e6..99361845 100644 --- a/labscript_devices/NI_DAQmx/models/NI_PCI_6713.py +++ b/labscript_devices/NI_DAQmx/models/NI_PCI_6713.py @@ -22,6 +22,7 @@ from labscript_devices.NI_DAQmx.labscript_devices import NI_DAQmx +#: CAPABILITIES = { 'AI_range': None, 'AI_start_delay': None, @@ -45,6 +46,7 @@ class NI_PCI_6713(NI_DAQmx): description = 'NI-PCI-6713' def __init__(self, *args, **kwargs): + """Class for NI-PCI-6713""" # Any provided kwargs take precedent over capabilities combined_kwargs = CAPABILITIES.copy() combined_kwargs.update(kwargs) diff --git a/labscript_devices/NI_DAQmx/models/NI_PCI_6733.py b/labscript_devices/NI_DAQmx/models/NI_PCI_6733.py index c373fe67..234e3f09 100644 --- a/labscript_devices/NI_DAQmx/models/NI_PCI_6733.py +++ b/labscript_devices/NI_DAQmx/models/NI_PCI_6733.py @@ -22,6 +22,7 @@ from labscript_devices.NI_DAQmx.labscript_devices import NI_DAQmx +#: CAPABILITIES = { 'AI_range': None, 'AI_start_delay': None, @@ -45,6 +46,7 @@ class NI_PCI_6733(NI_DAQmx): description = 'NI-PCI-6733' def __init__(self, *args, **kwargs): + """Class for NI-PCI-6733""" # Any provided kwargs take precedent over capabilities combined_kwargs = CAPABILITIES.copy() combined_kwargs.update(kwargs) diff --git a/labscript_devices/NI_DAQmx/models/NI_PCI_DIO_32HS.py b/labscript_devices/NI_DAQmx/models/NI_PCI_DIO_32HS.py index 47b72ac0..6f297f36 100644 --- a/labscript_devices/NI_DAQmx/models/NI_PCI_DIO_32HS.py +++ b/labscript_devices/NI_DAQmx/models/NI_PCI_DIO_32HS.py @@ -22,6 +22,7 @@ from labscript_devices.NI_DAQmx.labscript_devices import NI_DAQmx +#: CAPABILITIES = { 'AI_range': None, 'AI_start_delay': None, @@ -52,6 +53,7 @@ class NI_PCI_DIO_32HS(NI_DAQmx): description = 'NI-PCI-DIO-32HS' def __init__(self, *args, **kwargs): + """Class for NI-PCI-DIO-32HS""" # Any provided kwargs take precedent over capabilities combined_kwargs = CAPABILITIES.copy() combined_kwargs.update(kwargs) diff --git a/labscript_devices/NI_DAQmx/models/NI_PCIe_6363.py b/labscript_devices/NI_DAQmx/models/NI_PCIe_6363.py index 7dc4711c..8155ce02 100644 --- a/labscript_devices/NI_DAQmx/models/NI_PCIe_6363.py +++ b/labscript_devices/NI_DAQmx/models/NI_PCIe_6363.py @@ -22,6 +22,7 @@ from labscript_devices.NI_DAQmx.labscript_devices import NI_DAQmx +#: CAPABILITIES = { 'AI_range': [-10.0, 10.0], 'AI_start_delay': 7e-08, @@ -49,6 +50,7 @@ class NI_PCIe_6363(NI_DAQmx): description = 'NI-PCIe-6363' def __init__(self, *args, **kwargs): + """Class for NI-PCIe-6363""" # Any provided kwargs take precedent over capabilities combined_kwargs = CAPABILITIES.copy() combined_kwargs.update(kwargs) diff --git a/labscript_devices/NI_DAQmx/models/NI_PCIe_6738.py b/labscript_devices/NI_DAQmx/models/NI_PCIe_6738.py index f0176ea2..fb592d21 100644 --- a/labscript_devices/NI_DAQmx/models/NI_PCIe_6738.py +++ b/labscript_devices/NI_DAQmx/models/NI_PCIe_6738.py @@ -22,6 +22,7 @@ from labscript_devices.NI_DAQmx.labscript_devices import NI_DAQmx +#: CAPABILITIES = { 'AI_range': None, 'AI_start_delay': None, @@ -48,6 +49,7 @@ class NI_PCIe_6738(NI_DAQmx): description = 'NI-PCIe-6738' def __init__(self, *args, **kwargs): + """Class for NI-PCIe-6738""" # Any provided kwargs take precedent over capabilities combined_kwargs = CAPABILITIES.copy() combined_kwargs.update(kwargs) diff --git a/labscript_devices/NI_DAQmx/models/NI_PXI_6733.py b/labscript_devices/NI_DAQmx/models/NI_PXI_6733.py index 1f3181c3..74ff7215 100644 --- a/labscript_devices/NI_DAQmx/models/NI_PXI_6733.py +++ b/labscript_devices/NI_DAQmx/models/NI_PXI_6733.py @@ -22,6 +22,7 @@ from labscript_devices.NI_DAQmx.labscript_devices import NI_DAQmx +#: CAPABILITIES = { 'AI_range': None, 'AI_start_delay': None, @@ -45,6 +46,7 @@ class NI_PXI_6733(NI_DAQmx): description = 'NI-PXI-6733' def __init__(self, *args, **kwargs): + """Class for NI-PXI-6733""" # Any provided kwargs take precedent over capabilities combined_kwargs = CAPABILITIES.copy() combined_kwargs.update(kwargs) diff --git a/labscript_devices/NI_DAQmx/models/NI_PXIe_6361.py b/labscript_devices/NI_DAQmx/models/NI_PXIe_6361.py index 7b103032..7b88180a 100644 --- a/labscript_devices/NI_DAQmx/models/NI_PXIe_6361.py +++ b/labscript_devices/NI_DAQmx/models/NI_PXIe_6361.py @@ -22,6 +22,7 @@ from labscript_devices.NI_DAQmx.labscript_devices import NI_DAQmx +#: CAPABILITIES = { 'AI_range': [-10.0, 10.0], 'AI_start_delay': 7e-08, @@ -49,6 +50,7 @@ class NI_PXIe_6361(NI_DAQmx): description = 'NI-PXIe-6361' def __init__(self, *args, **kwargs): + """Class for NI-PXIe-6361""" # Any provided kwargs take precedent over capabilities combined_kwargs = CAPABILITIES.copy() combined_kwargs.update(kwargs) diff --git a/labscript_devices/NI_DAQmx/models/NI_PXIe_6535.py b/labscript_devices/NI_DAQmx/models/NI_PXIe_6535.py index d6c83d0f..a71a4322 100644 --- a/labscript_devices/NI_DAQmx/models/NI_PXIe_6535.py +++ b/labscript_devices/NI_DAQmx/models/NI_PXIe_6535.py @@ -22,6 +22,7 @@ from labscript_devices.NI_DAQmx.labscript_devices import NI_DAQmx +#: CAPABILITIES = { 'AI_range': None, 'AI_start_delay': None, @@ -51,6 +52,7 @@ class NI_PXIe_6535(NI_DAQmx): description = 'NI-PXIe-6535' def __init__(self, *args, **kwargs): + """Class for NI-PXIe-6535""" # Any provided kwargs take precedent over capabilities combined_kwargs = CAPABILITIES.copy() combined_kwargs.update(kwargs) diff --git a/labscript_devices/NI_DAQmx/models/NI_PXIe_6738.py b/labscript_devices/NI_DAQmx/models/NI_PXIe_6738.py index 0d6ae352..10b93ec5 100644 --- a/labscript_devices/NI_DAQmx/models/NI_PXIe_6738.py +++ b/labscript_devices/NI_DAQmx/models/NI_PXIe_6738.py @@ -22,6 +22,7 @@ from labscript_devices.NI_DAQmx.labscript_devices import NI_DAQmx +#: CAPABILITIES = { 'AI_range': None, 'AI_start_delay': None, @@ -48,6 +49,7 @@ class NI_PXIe_6738(NI_DAQmx): description = 'NI-PXIe-6738' def __init__(self, *args, **kwargs): + """Class for NI-PXIe-6738""" # Any provided kwargs take precedent over capabilities combined_kwargs = CAPABILITIES.copy() combined_kwargs.update(kwargs) diff --git a/labscript_devices/NI_DAQmx/models/NI_USB_6008.py b/labscript_devices/NI_DAQmx/models/NI_USB_6008.py index dae0bd4a..4a0b4714 100644 --- a/labscript_devices/NI_DAQmx/models/NI_USB_6008.py +++ b/labscript_devices/NI_DAQmx/models/NI_USB_6008.py @@ -22,6 +22,7 @@ from labscript_devices.NI_DAQmx.labscript_devices import NI_DAQmx +#: CAPABILITIES = { 'AI_range': [-10.0, 10.0], 'AI_start_delay': 8.333333333333334e-08, @@ -48,6 +49,7 @@ class NI_USB_6008(NI_DAQmx): description = 'NI-USB-6008' def __init__(self, *args, **kwargs): + """Class for NI-USB-6008""" # Any provided kwargs take precedent over capabilities combined_kwargs = CAPABILITIES.copy() combined_kwargs.update(kwargs) diff --git a/labscript_devices/NI_DAQmx/models/NI_USB_6229.py b/labscript_devices/NI_DAQmx/models/NI_USB_6229.py index 6c758e56..b17bf1b5 100644 --- a/labscript_devices/NI_DAQmx/models/NI_USB_6229.py +++ b/labscript_devices/NI_DAQmx/models/NI_USB_6229.py @@ -22,6 +22,7 @@ from labscript_devices.NI_DAQmx.labscript_devices import NI_DAQmx +#: CAPABILITIES = { 'AI_range': [-10.0, 10.0], 'AI_start_delay': 2.5e-07, @@ -49,6 +50,7 @@ class NI_USB_6229(NI_DAQmx): description = 'NI-USB-6229' def __init__(self, *args, **kwargs): + """Class for NI-USB-6229""" # Any provided kwargs take precedent over capabilities combined_kwargs = CAPABILITIES.copy() combined_kwargs.update(kwargs) diff --git a/labscript_devices/NI_DAQmx/models/NI_USB_6343.py b/labscript_devices/NI_DAQmx/models/NI_USB_6343.py index 3c00aa6b..1d38e471 100644 --- a/labscript_devices/NI_DAQmx/models/NI_USB_6343.py +++ b/labscript_devices/NI_DAQmx/models/NI_USB_6343.py @@ -22,6 +22,7 @@ from labscript_devices.NI_DAQmx.labscript_devices import NI_DAQmx +#: CAPABILITIES = { 'AI_range': [-10.0, 10.0], 'AI_start_delay': 7e-08, @@ -49,6 +50,7 @@ class NI_USB_6343(NI_DAQmx): description = 'NI-USB-6343' def __init__(self, *args, **kwargs): + """Class for NI-USB-6343""" # Any provided kwargs take precedent over capabilities combined_kwargs = CAPABILITIES.copy() combined_kwargs.update(kwargs) diff --git a/labscript_devices/NI_DAQmx/models/_subclass_template.py b/labscript_devices/NI_DAQmx/models/_subclass_template.py index 2edace73..dae85c7a 100644 --- a/labscript_devices/NI_DAQmx/models/_subclass_template.py +++ b/labscript_devices/NI_DAQmx/models/_subclass_template.py @@ -15,6 +15,7 @@ from labscript_devices.NI_DAQmx.labscript_devices import NI_DAQmx +#: CAPABILITIES = ${CAPABILITIES} @@ -22,6 +23,7 @@ class ${CLASS_NAME}(NI_DAQmx): description = '${MODEL_NAME}' def __init__(self, *args, **kwargs): + """Class for ${MODEL_NAME}""" # Any provided kwargs take precedent over capabilities combined_kwargs = CAPABILITIES.copy() combined_kwargs.update(kwargs) From 7b0688f117680497225230286ab8cae1dff6d52d Mon Sep 17 00:00:00 2001 From: David Meyer Date: Fri, 13 Nov 2020 16:06:56 -0500 Subject: [PATCH 10/28] Increasing doc details for flycapture2 and pylon cameras. --- docs/source/devices/flycapture2.rst | 43 +++++++++++++++++++++++++++-- docs/source/devices/ni_daqs.rst | 2 +- docs/source/devices/pylon.rst | 18 +++++++++++- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/docs/source/devices/flycapture2.rst b/docs/source/devices/flycapture2.rst index 3cfb7453..1ae73faf 100644 --- a/docs/source/devices/flycapture2.rst +++ b/docs/source/devices/flycapture2.rst @@ -29,7 +29,9 @@ Like the :doc:`IMAQdxCamera ` device, the bulk of camera configuration i 2. Mirroring the FlyCap Viewer parameter names and values. 3. Connecting to the camera with a minimal configuration, viewing the current parameters dictionary, and copying the relevant values to the connection table (preferred). -Below is a generic configuration. +The python structure for setting these values differs somewhat from other camera devices in labscript, taking the form of nested dictionaries. This structure most closely matches the structure of the FlyCapture2 SDK in that each camera property has multiple sub-elements that control the feature. In this implementation, the standard camera properties are set using keys with ALL CAPS. The control of the Trigger Mode and Image Mode properties is handled separately, using a slightly different nesting structure than the other properties. + +Below is a generic configuration for a Point Grey Blackfly PGE-23S6M-C device. .. code-block:: python @@ -40,8 +42,43 @@ Below is a generic configuration. FlyCapture2Camera('gigeCamera',parent_device=parent,connection=conn, serial_number=1234567, # set to the camera serial number minimum_recovery_time=36e-6, # the minimum exposure time depends on the camera model & configuration - camera_attributs={}, - manual_camera_attributes={}) + camera_attributs={ + 'GAMMA':{ + 'onOff':False, + 'absControl':True, + 'absValue':1}, + 'AUTO_EXPOSURE':{ + 'onOff':True, + 'absControl':True, + 'autoManualMode':False, + 'absValue':0}, + 'GAIN':{ + 'autoManualMode':False, + 'absControl':True, + 'absValue':0}, + 'SHARPNESS':{ + 'onOff':False, + 'autoManualMode':False, + 'absValue':1024}, + 'FRAME_RATE':{ + 'autoManualMode':False, + 'absControl':True}, + 'SHUTTER':{ + 'autoManualMode':False, + 'absValue':0}, + 'TriggerMode':{ + 'polarity':1, + 'source':0, + 'mode':1, + 'onOff':True}, + 'ImageMode':{ + 'width':1920, + 'height':1200, + 'offsetX':0, + 'offsetY':0, + 'pixelFormat':'MONO16'} + }, + manual_camera_attributes={'TriggerMode':{'onOff':False}}) start() diff --git a/docs/source/devices/ni_daqs.rst b/docs/source/devices/ni_daqs.rst index d6a633a5..e363c1eb 100644 --- a/docs/source/devices/ni_daqs.rst +++ b/docs/source/devices/ni_daqs.rst @@ -90,7 +90,7 @@ An example configuration of a `WaitMonitor` using a NI DAQ is shown here # Necessary to ensure even number of digital out lines in shot DigitalOut('daq_do1',daq,'port0/line1') -Note that the counter connection is specified using the logical label `'ctr0'`. On many NI DAQs, the physical connection to this counter is PFI9. The physical wiring for this configuration would have port0/line0 wired directly to PFI9, which PFI1 being sent to the master pseudoclock retriggering system in case of timeout. If timeouts are not expect/represent experiment failure, this physical connection can be omitted. +Note that the counter connection is specified using the logical label `'ctr0'`. On many NI DAQs, the physical connection to this counter is PFI9. The physical wiring for this configuration would have port0/line0 wired directly to PFI9, with PFI1 being sent to the master pseudoclock retriggering system in case of timeout. If timeouts are not expected/represent experiment failure, this physical connection can be omitted. Detailed Documentation diff --git a/docs/source/devices/pylon.rst b/docs/source/devices/pylon.rst index 9d57158d..43ab0632 100644 --- a/docs/source/devices/pylon.rst +++ b/docs/source/devices/pylon.rst @@ -76,7 +76,8 @@ Below are generic configurations for GigE and USB3 based cameras. 'Gamma':1.0, 'BlackLevel':0, 'TriggerSource':'Line 1', - 'TriggerMode':'On' + 'TriggerMode':'On', + 'ShutterMode':'Global' }, manual_camera_attributes={ 'TriggerSource':'Software', @@ -91,6 +92,21 @@ Below are generic configurations for GigE and USB3 based cameras. stop(1) +Utilities +~~~~~~~~~ + +The Pylon labscript device includes a script in the `testing` subfolder that can automatically determine the full-frame sensor readout time and maximum possible framerate. This tool helps in correctly determining the appropriate `minimum_recovery_time` to set for each device. The minimum recovery time is a function of the model used, the communication bus used, and minor details of the setup (such as host controller firmwares, cable lengths, host computer workload, etc). As a result, live testing of the device is often needed to accurately determine the actual recovery time needed between shots. + +The script is run from within the testing folder using + +.. code-block:: python + + python ExposureTiming.py [camera_sn] +with `[camera_sn]` being the serial number of the camera to connect to and test. + +The script reports the minimum recovery time between two shots of 1 ms exposure each, without the use of overlapped exposure mode. Editing the script to include your typical experiment parameters will help in more accurately determining your minimum recovery time. Typically, the minimum recovery time should be slightly longer than the reported sensor readout time. + +Note that in overlapped exposure mode, a second exposure is begun before the first exposure has finished reading out and *must* end after the readout of the first exposure frame is complete. This allows for a series of two exposures with shorter delay between them, at the expense of limitations on the length of the second exposure. The script will also report the minimum time between the end of one exposure and the beginning of the second (nominally `readout_time - exposure_time`). Note that this feature is automatically handled at the Pylon API level; this labscript device is not actively aware of it. As a result, incorrect uses of overlapped mode will not be caught at compile time, but rather during the shot as hardware errors. Detailed Documentation From 30b983bdfcc3db274761c150416f037ba6dcb761 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Fri, 13 Nov 2020 18:40:33 -0500 Subject: [PATCH 11/28] Adding some basic high-level documentation. This is largely from my own experience, and could probably benefit from leveraging Chris and Phil's thesis instead. --- docs/source/adding_devices.rst | 59 +++++++++++++- docs/source/ex_conn_tables.rst | 136 ++++++++++++++++++++++++++++++++- docs/source/index.rst | 3 + docs/source/user_devices.rst | 16 +++- 4 files changed, 211 insertions(+), 3 deletions(-) diff --git a/docs/source/adding_devices.rst b/docs/source/adding_devices.rst index ef009025..f56fae8c 100644 --- a/docs/source/adding_devices.rst +++ b/docs/source/adding_devices.rst @@ -1,2 +1,59 @@ How to Add a Device -=================== \ No newline at end of file +=================== + +Adding a **labscript-device** involves implementing interfaces for your hardware to different protions of the **labscript-suite**. Namely, you must provide + +* A `labscript_device` that takes **labscript** high-level commands and turns them into instructions that are saved to the shot h5 file. +* A `BLACS_worker` that handles communication with the hardware, in particular interpreting the instructions from the shot h5 file into the necessary hardware commands to configure the device. +* A `BLACS_tab` which provides a graphical interface to control the instrument via **BLACS** + +Though not strictly required, you should also consider providing a `runviewer_parser`, which can read the h5 instructions and produce the appropriate shot timings that would occur. This interface is only used in **runviewer**. + +General Strategy +~~~~~~~~~~~~~~~~ + +As a general rule, it is best to model new hardware implementations off of a currently implemented device that has similar functionality. If the functionality is similar enough, it may even be possible to simply sub-class the currently implemented device, which is likely preferrable. + +Barring the above simple solution, one must work from scratch. It is best to begin by determining the **labscript** device class to inherit from: `Psuedoclock`, `Device`, `IntermediateDevice`. The first is for implementing Psuedoclock devices, the second is for generic devices that are not hardware timed by a pseudoclock, and the last is for hardware timed device that are connected to another device controlled via labscript. + +The `labscript_device` implements general configuration parameters, many of which are passed to the `BLACS_worker`. It also implements the `generate_code` method which converts **labscript** high-level instructions and saves them to the h5 file. + +The `BLACS_tab` defines the GUI widgets that control the device. This typically takes the form of using standard widgets provided by **labscript** for controlling **labscript** output primitives (ie `AnalogOut`, `DigitalOut`,`DDS`, etc). This configuration is done in the `initialiseGUI` method. This also links directly to the appropriate BLACS workers. + +The `BLACS_worker` handles communication with the hardware itself and often represents the bulk of the work required to implement a new labscript device. In general, it should provide five different methods: + +* `init`: This method initialised communications with the device. Not to be confused with the standard python class `__init__` method. +* `program_manual`: This method allows for user control of the device via the `BLACS_tab`, setting outputs to the values set in the `BLACS_tab` widgets. +* `check_remote_values`: This method reads the current settings of the device, updating the `BLACS_tab` widgets to reflect these values. +* `transition_to_buffered`: This method transitions the device to buffered shot mode, reading the shot h5 file and taking the saved instructions from `labscript_device.generate_code` and sending the appropriate commands to the hardware. +* `transition_to_manual`: This method transitions the device from buffered to manual mode. It does any necessary configuration to take the device out of buffered mode and is used to read an measurements and save them to the shot h5 file as results. + +The `runviewer_parser` takes shot h5 files, reads the saved instructions, and allows you to view them in **runviewer** in order to visualise experiment timing. + +Code Organization +~~~~~~~~~~~~~~~~~ + +There are currently two supported file organization styles for a labscript-device. + +The old style has the `labscript_device`, `BLACS_tab`, `BLACS_worker`, and `runviewer_parser` all in the same file, which typically has the same name as the `labscript_device` class name. + +The new style allows for arbitrary code organization, but typically has a folder named after the `labscript_device` with each device component in a different file (ie `labscript_devices.py`, `BLACS_workers.py`, etc). With this style, the folder requires an `__init__.py` file (which can be empty) as well as a `register_classes.py` file. This file imports :ref:`` via + +.. code-block:: python + + from labscript_devices import register_classes + +This function informs **labscript** where to find the necessary classes during import. An example for the `NI_DAQmx` device is + +.. code-block:: python + + register_classes( + 'NI_DAQmx', + BLACS_tab='labscript_devices.NI_DAQmx.blacs_tabs.NI_DAQmxTab', + runviewer_parser='labscript_devices.NI_DAQmx.runviewer_parsers.NI_DAQmxParser', + ) + +Contributions to **labscript-devices** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you decide to implement a labscript-device for controlling new hardware, we highly encourage you to consider making a pull-request to the **labscript-devices** repository in order to add your work to the **labscript-suite**. Increasing the list of supported devices is an important way for the **labscript-suite** to continue to grow, allowing new users to more quickly get up and running with hardware they may already have. \ No newline at end of file diff --git a/docs/source/ex_conn_tables.rst b/docs/source/ex_conn_tables.rst index 67a3df66..d6004a04 100644 --- a/docs/source/ex_conn_tables.rst +++ b/docs/source/ex_conn_tables.rst @@ -1,2 +1,136 @@ Example Connection Tables -========================= \ No newline at end of file +========================= + +An example connection table for the experiment described in [1]_. This connection table makes extensive use of `user_devices`, by name of `naqslab_devices`. + +.. code-block:: python + + from labscript import * + from naqslab_devices.PulseBlasterESRPro300.labscript_device import PulseBlasterESRPro300 + from naqslab_devices.NovaTechDDS.labscript_device import NovaTech409B, NovaTech409B_AC + from labscript_devices.NI_DAQmx.models.NI_USB_6343 import NI_USB_6343 + from naqslab_devices.SignalGenerator.Models import RS_SMA100B, SRS_SG386 + from naqslab_devices import ScopeChannel, StaticFreqAmp + from naqslab_devices.KeysightXSeries.labscript_device import KeysightXScope + #from labscript_devices.PylonCamera.labscript_devices import PylonCamera + from naqslab_devices.KeysightDCSupply.labscript_device import KeysightDCSupply + from naqslab_devices.SR865.labscript_device import SR865 + + PulseBlasterESRPro300(name='pulseblaster_0', board_number=0, programming_scheme='pb_start/BRANCH') + ClockLine(name='pulseblaster_0_clockline_fast', pseudoclock=pulseblaster_0.pseudoclock, connection='flag 0') + ClockLine(name='pulseblaster_0_clockline_slow', pseudoclock=pulseblaster_0.pseudoclock, connection='flag 1') + + NI_USB_6343(name='ni_6343', parent_device=pulseblaster_0_clockline_fast, + clock_terminal='/ni_usb_6343/PFI0', + MAX_name='ni_usb_6343', + acquisition_rate = 243e3, # 500 kS/s max aggregate) + stop_order = -1) #as clocking device, ensure it transitions first + + NovaTech409B(name='novatech_static', com_port="com4", baud_rate = 115200, + phase_mode='aligned',ext_clk=True, clk_freq=100, clk_mult=5) + NovaTech409B_AC(name='novatech', parent_device=pulseblaster_0_clockline_slow, + com_port="com3", update_mode='asynchronous', phase_mode='aligned', + baud_rate = 115200, ext_clk=True, clk_freq=100, clk_mult=5) + + # using NI-MAX alias instead of full VISA name + RS_SMA100B(name='SMA100B', VISA_name='SMA100B') + RS_SMA100B(name='SMA100B2', VISA_name='SMA100B-2') + SRS_SG386(name='SG386', VISA_name='SG386-6181I', output='RF', mod_type='Sweep') + + # call the scope, use NI-MAX alias instead of full name + KeysightXScope(name='Scope',VISA_name='DSOX3024T', + trigger_device=pulseblaster_0.direct_outputs,trigger_connection='flag 3', + num_AI=4,DI=False) + ScopeChannel('Heterodyne',Scope,'Channel 1') + #ScopeChannel('Absorption',Scope,'Channel 2') + #ScopeChannel('Modulation',Scope,'Channel 4') + + # DC Supplies + KeysightDCSupply(name='DCSupply',VISA_name='E3640A', + range='HIGH',volt_limits=(0,20),current_limits=(0,1)) + StaticAnalogOut('DCBias_Gnd',DCSupply,'channel 0') + KeysightDCSupply(name='DCSupply2',VISA_name='E3644A', + range='HIGH',volt_limits=(0,20),current_limits=(0,1)) + StaticAnalogOut('DCBias_Sig',DCSupply2,'channel 0') + + # Lock-In Amplifier + SR865(name='LockIn',VISA_name='SR865') + + # Define Cameras + # note that Basler cameras can overlap frames if + # second exposure does not end before frame transfer of first finishes + + ''' + PylonCamera('CCD_2',parent_device=pulseblaster_0.direct_outputs,connection='flag 6', + serial_number=21646179, + mock=False, + camera_attributes={'ExposureTime':9000, + 'ExposureMode':'Timed', + 'Gain':0.0, + 'ExposureAuto':'Off', + 'GainAuto':'Off', + 'PixelFormat':'Mono12', + 'Gamma':1.0, + 'BlackLevel':0, + 'TriggerSource':'Line1', + 'ShutterMode':'Global', + 'TriggerMode':'On'}, + manual_mode_camera_attributes={'TriggerSource':'Software', + 'TriggerMode':'Off'}) + ''' + # Define the Wait Monitor for the AC-Line Triggering + # note that connections used here cannot be used elsewhere + # 'connection' needs to be physically connected to 'acquisition_connection' + # for M-Series DAQs, ctr0 gate is on PFI9 + WaitMonitor(name='wait_monitor', parent_device=ni_6343, + connection='port0/line0', acquisition_device=ni_6343, + acquisition_connection='ctr0', timeout_device=ni_6343, + timeout_connection='PFI1') + + DigitalOut( 'AC_trigger_arm', pulseblaster_0.direct_outputs, 'flag 2') + + # define the PB digital outputs + DigitalOut( 'probe_AOM_enable', pulseblaster_0.direct_outputs, 'flag 4') + DigitalOut( 'LO_AOM_enable', pulseblaster_0.direct_outputs, 'flag 5') + + # short pulse control channels + DigitalOut( 'bit21', pulseblaster_0.direct_outputs, 'flag 21') + DigitalOut( 'bit22', pulseblaster_0.direct_outputs, 'flag 22') + DigitalOut( 'bit23', pulseblaster_0.direct_outputs, 'flag 23') + + AnalogOut( 'ProbeAmpLock', ni_6343, 'ao0') + AnalogOut( 'LOAmpLock', ni_6343, 'ao1') + AnalogOut( 'blueSweep', ni_6343, 'ao2') + AnalogOut( 'MW_Phase', ni_6343, 'ao3') + + AnalogIn( 'Homodyne', ni_6343, 'ai0') + AnalogIn( 'AI1', ni_6343, 'ai1') + AnalogIn( 'LockInX', ni_6343, 'ai2') + AnalogIn( 'LockInY', ni_6343, 'ai3') + + # this dummy line necessary to balance the digital out for the wait monitor + DigitalOut( 'P0_1', ni_6343, 'port0/line1') + + StaticDDS( 'Probe_EOM', novatech_static, 'channel 0') + StaticDDS( 'Probe_AOM', novatech_static, 'channel 1') + StaticDDS( 'LO_AOM', novatech_static, 'channel 2') + StaticDDS( 'LO', novatech_static, 'channel 3') + + DDS( 'Probe_BN', novatech, 'channel 0') + DDS( 'dds1', novatech, 'channel 1') + StaticDDS( 'SAS_Mod', novatech, 'channel 2') + StaticDDS( 'SAS_LO', novatech, 'channel 3') + + StaticFreqAmp( 'uWaves', SMA100B, 'channel 0', freq_limits=(8e-6,20), amp_limits=(-145,35)) + StaticFreqAmp( 'uWavesLO', SMA100B2, 'channel 0', freq_limits=(8e-6,20), amp_limits=(-145,35)) + StaticFreqAmp( 'blueEOM', SG386, 'channel 0', freq_limits=(1,6.075e3), amp_limits=(-110,16.5)) + + start() + + stop(1) + +References +~~~~~~~~~~ + +.. [1] D. H. Meyer, Z. A. Castillo, K. C. Cox, and P. D. Kunz, J. Phys B, **53** 034001 (2020) + https://iopscience.iop.org/article/10.1088/1361-6455/ab6051 \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index f3bfd0b4..c2e62eaa 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -6,6 +6,9 @@ labscript-devices ================= +This portion of the **labscript-suite** contains the plugin architecture for controlling experimental hardware. +In particular, this code provides the interface between **labscript** high-level instructions and hardware-specific instructions, the communication interface to send those instructions to the hardware, and the **BLACS** instrument control interface. + .. toctree:: :maxdepth: 2 :hidden: diff --git a/docs/source/user_devices.rst b/docs/source/user_devices.rst index f356b676..db2fb1a6 100644 --- a/docs/source/user_devices.rst +++ b/docs/source/user_devices.rst @@ -1,2 +1,16 @@ User Devices -============ \ No newline at end of file +============ + +Adding custom devices for use in the labscript-suite can be done using the `user_devices` mechanism. This mechanism provides a simple way to add support for a new device without directly interacting with the **labscript-devices** repository. This is particularly useful when using standard installations of labscript, using code that is proprietary in nature, or code that, while functional, is not mature enough for widespread dissemination. + +This is done by adding the **labscript-device** code into the `userlib/user_devices` folder. Using the custom device in a **labscript** connection table is then done by: + +.. code-block:: python + + from user_devices.MyCustomUserDevice.labscript_devices import MyCustomUserDevice + +This import statement assumes your custom device follows the new device structure organization. + +Note that both the `userlib` path and the `user_devices` folder name can be custom configured in the `labconfig.ini` file. The `user_devices` folder must be in the `userlib` path. If a different `user_devices` folder name is used, the import uses that folder name in place of `user_devices` in the above import statement. + +Note that we highly encourage everyone that adds support for new hardware to consider making a pull request to **labscript-devices** so that it may be added to the mainline and more easily used by other groups. \ No newline at end of file From 5567a9dae4144b644cf093cf8123dc98da759a8f Mon Sep 17 00:00:00 2001 From: David Meyer Date: Tue, 29 Dec 2020 12:13:39 -0500 Subject: [PATCH 12/28] Updated doc string in conf.py --- docs/source/conf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7194aa4f..72e7a4bc 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -231,8 +231,7 @@ def setup(app): def run_apidoc(_): """Runs apidoc with our desired parameters to generate the NI_DAQmx models docs. - - Also manually strips out some irrelevant stuff.""" + """ from sphinx.ext.apidoc import main daq_models_path = os.path.join(os.path.abspath('..'),'labscript_devices') out_path = os.path.join(os.path.dirname(Path(__file__)),'devices','_apidoc','models') From 75bcb30bfd5fc9ce58e01a5757f3cccb4040d55c Mon Sep 17 00:00:00 2001 From: David Meyer Date: Tue, 29 Dec 2020 12:13:54 -0500 Subject: [PATCH 13/28] Updated usage documentation for Spinnaker cameras. --- docs/source/devices/spinnaker.rst | 38 +++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/docs/source/devices/spinnaker.rst b/docs/source/devices/spinnaker.rst index d6ae8808..e231cc95 100644 --- a/docs/source/devices/spinnaker.rst +++ b/docs/source/devices/spinnaker.rst @@ -15,7 +15,7 @@ First ensure that the Spinnaker SDK is installed. The python wrapper is available via FLIR. It must be installed separately and pointed to the correct conda environment during install. -For GigE cameras, ensure that the network interface card (NIC) on the computer with the BLACS controlling the camera has enabled Jumbo Frames. That maximum allowed value (typically 9000) is preferable to avoid dropped frames. +For GigE cameras, ensure that the network interface card (NIC) on the computer with the BLACS controlling the camera has enabled Jumbo Frames. The maximum allowed value (typically 9000) is preferable to avoid dropped frames. Usage ~~~~~ @@ -34,15 +34,45 @@ Below is a generic configuration. from labscript_devices.SpinnakerCamera.labscript_devices import SpinnakerCamera + CCT_global_camera_attributes = { + 'AnalogControl::GainAuto': 'Off', + 'AnalogControl::Gain': 0.0, + 'AnalogControl::BlackLevelEnabled': True, + 'AnalogControl::BlackLevel': 0.0, + 'AnalogControl::GammaEnabled': False, + 'AnalogControl::SharpnessEnabled': False, + 'ImageFormatControl::Width': 1008, + 'ImageFormatControl::Height': 800, + 'ImageFormatControl::OffsetX': 200, + 'ImageFormatControl::OffsetY': 224, + 'ImageFormatControl::PixelFormat': 'Mono16', + 'ImageFormatControl::VideoMode': 'Mode0', + 'AcquisitionControl::TriggerMode': 'Off', + 'AcquisitionControl::TriggerSource': 'Line0', + 'AcquisitionControl::TriggerSelector': 'ExposureActive', + 'AcquisitionControl::TriggerActivation': 'FallingEdge', + } + CCT_manual_mode_attributes = { + 'AcquisitionControl::TriggerMode': 'Off', + 'AcquisitionControl::ExposureMode': 'Timed', + } + CCT_buffered_mode_attributes = { + 'AcquisitionControl::TriggerMode': 'On', + 'AcquisitionControl::ExposureMode': 'TriggerWidth', + } + SpinnakerCamera('gigeCamera',parent_device=parent,connection=conn, serial_number=1234567, # set to the camera serial number minimum_recovery_time=36e-6, # the minimum exposure time depends on the camera model & configuration - camera_attributs={}, - manual_camera_attributes={}) + trigger_edge_type='falling', + camera_attributs={**CCT_global_camera_attributes, + **CCT_buffered_mode_attributes}, + manual_camera_attributes={**CCT_global_camera_attributes, + **CCT_manual_mode_attributes}) start() - gigeCamera.expose(t=0.5,'exposure1') + gigeCamera.expose(t=0.5,'exposure1',trigger_duration=0.25) stop(1) From c7a4f58038b25879d1d902c8fc3cfb610c43d194 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Thu, 27 May 2021 10:56:18 -0400 Subject: [PATCH 14/28] Fix apidoc auto-run code. Apparently RTD builds are from a different directory than the makefile. Need to change the relative path specification to properly find the module code. --- docs/source/conf.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 72e7a4bc..7ba48f1a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -227,15 +227,23 @@ def setup(app): ) # hook to run apidoc before building - app.connect('builder-inited',run_apidoc) + app.connect('builder-inited', run_apidoc) + def run_apidoc(_): """Runs apidoc with our desired parameters to generate the NI_DAQmx models docs. """ from sphinx.ext.apidoc import main - daq_models_path = os.path.join(os.path.abspath('..'),'labscript_devices') - out_path = os.path.join(os.path.dirname(Path(__file__)),'devices','_apidoc','models') - templates_path = os.path.join(os.path.dirname(Path(__file__)),'_templates','models') - main(['-TMf','-s','inc', + if os.environ.get('READTHEDOCS'): + rel_path = '../..' + else: + rel_path = '..' + daq_models_path = os.path.join(os.path.abspath(rel_path), + 'labscript_devices') + out_path = os.path.join(os.path.dirname(Path(__file__)), + 'devices', '_apidoc', 'models') + templates_path = os.path.join(os.path.dirname(Path(__file__)), + '_templates', 'models') + main(['-TMf', '-s', 'inc', '-t', templates_path, - '-o',out_path,daq_models_path]) + '-o', out_path, daq_models_path]) From 45a54ed3cf315b40d0e4adc2a3e0642e47f894be Mon Sep 17 00:00:00 2001 From: David Meyer Date: Thu, 27 May 2021 15:43:39 -0400 Subject: [PATCH 15/28] Adding unlisted dependencies so doc builds can successfully import more components. --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index 6ee0f61e..0fdb833f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,12 +26,15 @@ packages = find: python_requires = >=3.6 install_requires = blacs>=3.0.0 + runmanager>=3.0.0 importlib_metadata labscript>=3.0.0 labscript_utils>=3.0.0 numpy>=1.15.1 pillow + tqdm PyDAQmx + PyVISA PyNIVision pyserial qtutils>=2.2.3 From b8e792306bed91f71be781bbe2239ae0b88f10d1 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Thu, 27 May 2021 15:44:58 -0400 Subject: [PATCH 16/28] Sphinx version bump to match necessary bumps in other labscript docs. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 0fdb833f..d5df1b29 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,7 +44,7 @@ install_requires = [options.extras_require] docs = PyQt5 - Sphinx==3.0.1 + Sphinx==3.2.1 sphinx-rtd-theme==0.4.3 recommonmark==0.6.0 m2r==0.2.1 From a78ed95f5382b3ec05acbc8bb04cac5cbbca02d5 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Thu, 27 May 2021 15:49:08 -0400 Subject: [PATCH 17/28] Modify Spinnaker Camera so proprietary PySpin import does not happen on module load. Fixes docs-build failure and allows use of the Mock Camera for those without PySpin installed. --- labscript_devices/SpinnakerCamera/blacs_workers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/labscript_devices/SpinnakerCamera/blacs_workers.py b/labscript_devices/SpinnakerCamera/blacs_workers.py index 025bf767..b5cbc9c0 100644 --- a/labscript_devices/SpinnakerCamera/blacs_workers.py +++ b/labscript_devices/SpinnakerCamera/blacs_workers.py @@ -18,7 +18,6 @@ import numpy as np from labscript_utils import dedent from enum import IntEnum -import PySpin from time import sleep, perf_counter from labscript_devices.IMAQdxCamera.blacs_workers import IMAQdxCameraWorker @@ -28,6 +27,9 @@ def __init__(self, serial_number): """Initialize Spinnaker API camera. Serial number should be of string(?) type.""" + global PySpin + import PySpin + self.system = PySpin.System.GetInstance() ver = self.system.GetLibraryVersion() From 82871bc23bec6e34a3124b1cf8fb55c97a807786 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Thu, 27 May 2021 15:54:51 -0400 Subject: [PATCH 18/28] Attempting the easy mocking provided within sphinx-autodoc to mock all PyDAQmx imports. --- docs/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7ba48f1a..2627abd5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -51,6 +51,7 @@ autodoc_typehints = 'description' autoclass_content = 'both' # options: 'both', 'class', 'init' +autodoc_mock_imports = ['PyDAQmx'] # Prefix each autosectionlabel with the name of the document it is in and a colon autosectionlabel_prefix_document = True From 3a2087ffb37e9f82cb63d83bd43452484b9c2db8 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Mon, 31 May 2021 13:45:52 -0400 Subject: [PATCH 19/28] Basic high-level docs for the new PrawnBlaster device. --- docs/source/devices.rst | 1 + docs/source/devices/prawnblaster.rst | 87 ++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 docs/source/devices/prawnblaster.rst diff --git a/docs/source/devices.rst b/docs/source/devices.rst index e00d3ddd..2cadc4fe 100644 --- a/docs/source/devices.rst +++ b/docs/source/devices.rst @@ -16,6 +16,7 @@ Pseudoclocks provide the timing backbone of the labscript_suite. These devices p devices/pulseblaster_no_dds devices/opalkellyXEM3001 devices/pineblaster + devices/prawnblaster devices/rfblaster NI DAQS diff --git a/docs/source/devices/prawnblaster.rst b/docs/source/devices/prawnblaster.rst new file mode 100644 index 00000000..17895517 --- /dev/null +++ b/docs/source/devices/prawnblaster.rst @@ -0,0 +1,87 @@ +PrawnBlaster +============ + +This labscript device controls the `PrawnBlaster `_ open-source digital pattern generator based on the `Raspberry Pi Pico `_ platform. + +Specifications +~~~~~~~~~~~~~~ + +The PrawnBlaster takes advantage of the specs of the Pico to provide the following: + +* Configurable as 1, 2, 3, or 4 trulty independent pseudoclocks. + + - Each clock has its own independent instruction set and synchronization between clocks is not required. + - Assuming the default internal clock of 100 MHz, each clock has: + + - Minimum pulse half-period of 50 ns + - Maximum pulse half-period of 42.9 s + - Half-period resolution of 10 ns + +* 30,000 instructions (each with up to 2^32 repetitions) distributed evenly among the configured pseudoclocks; 30,000, 15,000, 10,000, and 7,500 for 1, 2, 3, 4 pseudoclocks respectively. +* Support for external hardware triggers (external trigger common to all pseudoclocks) + + - Up to 100 retriggers (labscript-suite waits) per pseudoclock + - Each wait can support a timeout of up to 42.9 s + - Each wait is internally monitored for its duration (resolution of +/-10 ns) + +* Can be referenced to an external LVCMOS clock +* Internal clock can be set up to 133 MHz (timing specs scale accordingly) + +Installation +~~~~~~~~~~~~ + +In order to turn the standard Pico into a PrawnBlaster, you need to load the custom firmware available in the `Github repo `_ onto the board. The simplest way to do this is by holding the reset button on the board while plugging the USB into a computer. This will bring up a mounted folder that you copy-paste the firmware to. Once copied, the board will reset and be ready to go. + +Note that this device communicates using a virtual COM port. The number is assigned by the controlling computer and will need to be determined in order for BLACS to connect to the PrawnBlaster. + +Usage +~~~~~ + +The default pinout for the PrawnBlaster is as follows: + +* Pseudoclock 0 output: GPIO 9 +* Pseudoclock 1 output: GPIO 11 +* Pseudoclock 2 output: GPIO 13 +* Pseudoclock 3 output: GPIO 15 +* External Triggeer input: GPIO 0 +* External Clocl input: GPIO 20 + +Note that signal cable grounds should be connected to the digital grounds of the Pico for proper operation. + +The PrawnBlaster provides up to four independent clocklines. +They can be accessed either by `name.clocklines[int]` +or directly by their auto-generated labscript names `name_clock_line_int`. + +An example connection table that use the PrawnBlaster: + +.. code-block:: python + + from labscript import * + + from labscript_devices.PrawnBlaster.labscript_devices import PrawnBlaster + from labscript_devices.NI_DAQmx.models.NI_USB_6363 import NI_USB_6363 + + PrawnBlaster(name='prawn', com_port='COM6', num_pseudoclocks=1) + + NI_USB_6363(name='daq', MAX_name='Dev1', + parent_device=prawn.clocklines[0], clock_terminal='/Dev1/PFI0', + acquisition_rate=100e3) + + AnalogOut('ao0', daq, 'ao0') + AnalogOut('ao1', daq, 'ao1') + + if __name__ == '__main__': + + start(0) + + stop(1) + + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.PrawnBlaster + :members: + :undoc-members: + :show-inheritance: + :private-members: \ No newline at end of file From 69a7a473a9c4430173301c81779c37af9c1b8557 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Mon, 31 May 2021 13:55:22 -0400 Subject: [PATCH 20/28] Update sphinx_rtd_theme pin to 0.5.2 and fix PrawnBlaster nested listing. --- docs/source/devices/prawnblaster.rst | 16 ++++++++-------- setup.cfg | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/source/devices/prawnblaster.rst b/docs/source/devices/prawnblaster.rst index 17895517..655fc08f 100644 --- a/docs/source/devices/prawnblaster.rst +++ b/docs/source/devices/prawnblaster.rst @@ -10,19 +10,19 @@ The PrawnBlaster takes advantage of the specs of the Pico to provide the followi * Configurable as 1, 2, 3, or 4 trulty independent pseudoclocks. - - Each clock has its own independent instruction set and synchronization between clocks is not required. - - Assuming the default internal clock of 100 MHz, each clock has: + - Each clock has its own independent instruction set and synchronization between clocks is not required. + - Assuming the default internal clock of 100 MHz, each clock has: - - Minimum pulse half-period of 50 ns - - Maximum pulse half-period of 42.9 s - - Half-period resolution of 10 ns + - Minimum pulse half-period of 50 ns + - Maximum pulse half-period of 42.9 s + - Half-period resolution of 10 ns * 30,000 instructions (each with up to 2^32 repetitions) distributed evenly among the configured pseudoclocks; 30,000, 15,000, 10,000, and 7,500 for 1, 2, 3, 4 pseudoclocks respectively. * Support for external hardware triggers (external trigger common to all pseudoclocks) - - Up to 100 retriggers (labscript-suite waits) per pseudoclock - - Each wait can support a timeout of up to 42.9 s - - Each wait is internally monitored for its duration (resolution of +/-10 ns) + - Up to 100 retriggers (labscript-suite waits) per pseudoclock + - Each wait can support a timeout of up to 42.9 s + - Each wait is internally monitored for its duration (resolution of +/-10 ns) * Can be referenced to an external LVCMOS clock * Internal clock can be set up to 133 MHz (timing specs scale accordingly) diff --git a/setup.cfg b/setup.cfg index d5df1b29..3909ba4f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,6 +45,6 @@ install_requires = docs = PyQt5 Sphinx==3.2.1 - sphinx-rtd-theme==0.4.3 + sphinx-rtd-theme==0.5.2 recommonmark==0.6.0 m2r==0.2.1 From ab577ba501f01dd566d76b51e0f6a880c7670650 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Mon, 31 May 2021 14:14:47 -0400 Subject: [PATCH 21/28] Yet another tweak of the Prawnblaster bullet list. --- docs/source/devices/prawnblaster.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/devices/prawnblaster.rst b/docs/source/devices/prawnblaster.rst index 655fc08f..6db1d71a 100644 --- a/docs/source/devices/prawnblaster.rst +++ b/docs/source/devices/prawnblaster.rst @@ -13,9 +13,9 @@ The PrawnBlaster takes advantage of the specs of the Pico to provide the followi - Each clock has its own independent instruction set and synchronization between clocks is not required. - Assuming the default internal clock of 100 MHz, each clock has: - - Minimum pulse half-period of 50 ns - - Maximum pulse half-period of 42.9 s - - Half-period resolution of 10 ns + - Minimum pulse half-period of 50 ns + - Maximum pulse half-period of 42.9 s + - Half-period resolution of 10 ns * 30,000 instructions (each with up to 2^32 repetitions) distributed evenly among the configured pseudoclocks; 30,000, 15,000, 10,000, and 7,500 for 1, 2, 3, 4 pseudoclocks respectively. * Support for external hardware triggers (external trigger common to all pseudoclocks) From 40b9a0c3a47405726399bb82d1655d1a78359f04 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Mon, 31 May 2021 14:24:26 -0400 Subject: [PATCH 22/28] Fix warning in pylon.rst --- docs/source/devices/pylon.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/devices/pylon.rst b/docs/source/devices/pylon.rst index 43ab0632..53ac8ac7 100644 --- a/docs/source/devices/pylon.rst +++ b/docs/source/devices/pylon.rst @@ -102,6 +102,7 @@ The script is run from within the testing folder using .. code-block:: python python ExposureTiming.py [camera_sn] + with `[camera_sn]` being the serial number of the camera to connect to and test. The script reports the minimum recovery time between two shots of 1 ms exposure each, without the use of overlapped exposure mode. Editing the script to include your typical experiment parameters will help in more accurately determining your minimum recovery time. Typically, the minimum recovery time should be slightly longer than the reported sensor readout time. From 17c507f6dd6b4349dc22d7487c73969f57d56124 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Mon, 31 May 2021 14:24:54 -0400 Subject: [PATCH 23/28] Actually get automodule to document the PrawnBlaster code. --- docs/source/devices/prawnblaster.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/source/devices/prawnblaster.rst b/docs/source/devices/prawnblaster.rst index 6db1d71a..ac002b18 100644 --- a/docs/source/devices/prawnblaster.rst +++ b/docs/source/devices/prawnblaster.rst @@ -81,6 +81,30 @@ Detailed Documentation ~~~~~~~~~~~~~~~~~~~~~~ .. automodule:: labscript_devices.PrawnBlaster + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.PrawnBlaster.labscript_devices + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.PrawnBlaster.blacs_tabs + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.PrawnBlaster.blacs_workers + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.PrawnBlaster.runviewer_parsers :members: :undoc-members: :show-inheritance: From 436f3302daf02d4b041d6caf3a99e2f32377abd1 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Tue, 1 Jun 2021 12:20:58 -0400 Subject: [PATCH 24/28] Add docstring-level documentation for the new PrawnBlaster pseudoclock device. --- docs/source/devices/prawnblaster.rst | 6 +- labscript_devices/PrawnBlaster/blacs_tabs.py | 39 +++++++- .../PrawnBlaster/blacs_workers.py | 97 +++++++++++++++++++ .../PrawnBlaster/labscript_devices.py | 96 +++++++++++++++++- .../PrawnBlaster/runviewer_parsers.py | 18 ++++ 5 files changed, 245 insertions(+), 11 deletions(-) diff --git a/docs/source/devices/prawnblaster.rst b/docs/source/devices/prawnblaster.rst index ac002b18..4f382fc5 100644 --- a/docs/source/devices/prawnblaster.rst +++ b/docs/source/devices/prawnblaster.rst @@ -8,7 +8,7 @@ Specifications The PrawnBlaster takes advantage of the specs of the Pico to provide the following: -* Configurable as 1, 2, 3, or 4 trulty independent pseudoclocks. +* Configurable as 1, 2, 3, or 4 truly independent pseudoclocks. - Each clock has its own independent instruction set and synchronization between clocks is not required. - Assuming the default internal clock of 100 MHz, each clock has: @@ -44,7 +44,7 @@ The default pinout for the PrawnBlaster is as follows: * Pseudoclock 2 output: GPIO 13 * Pseudoclock 3 output: GPIO 15 * External Triggeer input: GPIO 0 -* External Clocl input: GPIO 20 +* External Clock input: GPIO 20 Note that signal cable grounds should be connected to the digital grounds of the Pico for proper operation. @@ -52,7 +52,7 @@ The PrawnBlaster provides up to four independent clocklines. They can be accessed either by `name.clocklines[int]` or directly by their auto-generated labscript names `name_clock_line_int`. -An example connection table that use the PrawnBlaster: +An example connection table that uses the PrawnBlaster: .. code-block:: python diff --git a/labscript_devices/PrawnBlaster/blacs_tabs.py b/labscript_devices/PrawnBlaster/blacs_tabs.py index 25ab73d0..9cc6fd57 100644 --- a/labscript_devices/PrawnBlaster/blacs_tabs.py +++ b/labscript_devices/PrawnBlaster/blacs_tabs.py @@ -24,7 +24,14 @@ class PrawnBlasterTab(DeviceTab): + """BLACS Tab for the PrawnBlaster Device.""" + def initialise_GUI(self): + """Initialises the Tab GUI. + + This method is called automatically by BLACS. + """ + self.connection_table_properties = ( self.settings["connection_table"].find_by_name(self.device_name).properties ) @@ -53,6 +60,16 @@ def initialise_GUI(self): self.statemachine_timeout_add(2000, self.status_monitor) def get_child_from_connection_table(self, parent_device_name, port): + """Finds the attached ClockLines. + + Args: + parent_device_name (str): name of parent_device + port (str): port of parent_device + + Returns: + :class:`~labscript.ClockLine`: PrawnBlaster interal Clocklines + """ + # Pass down channel name search to the pseudoclocks (so we can find the # clocklines) if parent_device_name == self.device_name: @@ -69,6 +86,11 @@ def get_child_from_connection_table(self, parent_device_name, port): return None def initialise_workers(self): + """Initialises the PrawnBlaster Workers. + + This method is called automatically by BLACS. + """ + # Find the COM port to be used com_port = str( self.settings["connection_table"] @@ -97,9 +119,18 @@ def initialise_workers(self): True, ) def status_monitor(self, notify_queue=None): - # When called with a queue, this function writes to the queue - # when the pulseblaster is waiting. This indicates the end of - # an experimental run. + """Gets the status of the PrawnBlaster from the worker. + + When called with a queue, this function writes to the queue + when the PrawnBlaster is waiting. This indicates the end of + an experimental run. + + Args: + notify_queue (:class:`~queue.Queue`): Queue to notify when + the experiment is done. + + """ + status, clock_status, waits_pending = yield ( self.queue_work(self.primary_worker, "check_status") ) @@ -121,6 +152,8 @@ def status_monitor(self, notify_queue=None): @define_state(MODE_BUFFERED, True) def start_run(self, notify_queue): + """When used as the primary Pseudoclock, this starts the run.""" + self.statemachine_timeout_remove(self.status_monitor) yield (self.queue_work(self.primary_worker, "start_run")) self.status_monitor() diff --git a/labscript_devices/PrawnBlaster/blacs_workers.py b/labscript_devices/PrawnBlaster/blacs_workers.py index 6c0bc957..08e5b47d 100644 --- a/labscript_devices/PrawnBlaster/blacs_workers.py +++ b/labscript_devices/PrawnBlaster/blacs_workers.py @@ -19,7 +19,19 @@ class PrawnBlasterWorker(Worker): + """The primary worker for the PrawnBlaster. + + This worker handles configuration and communication + with the hardware. + """ + def init(self): + """Initialises the hardware communication. + + This function is automatically called by BLACS + and configures hardware communication with the device. + """ + # fmt: off global h5py; import labscript_utils.h5_lock, h5py global serial; import serial @@ -58,6 +70,34 @@ def init(self): assert self.prawnblaster.readline().decode() == "ok\r\n" def check_status(self): + """Checks the operational status of the PrawnBlaster. + + This is automatically called by BLACS to update the status + of the PrawnBlaster. It also reads the lengths of any + accumulated waits during a shot. + + Returns: + (int, int, bool): Tuple containing: + + - **run_status** (int): Possible values are: + + * 0 : manual-mode + * 1 : transitioning to buffered execution + * 2 : buffered execution + * 3 : abort requested + * 4 : currently aborting buffered execution + * 5 : last buffered execution aborted + * 6 : transitioning to manual mode + + - **clock_status** (int): Possible values are: + + * 0 : internal clock + * 1 : external clock + + - **waits_pending** (bool): Indicates if all expected waits have + not been read out yet. + """ + if ( self.started and self.wait_table is not None @@ -130,6 +170,15 @@ def check_status(self): return run_status, clock_status, waits_pending def read_status(self): + """Reads the status of the PrawnBlaster. + + Returns: + (int, int): Tuple containing + + - **run-status** (int): Run status code + - **clock-status** (int): Clock status code + """ + self.prawnblaster.write(b"status\r\n") response = self.prawnblaster.readline().decode() match = re.match(r"run-status:(\d) clock-status:(\d)(\r\n)?", response) @@ -145,6 +194,16 @@ def read_status(self): ) def program_manual(self, values): + """Manually sets the state of output pins for the pseudoclocks. + + Args: + values (dict): Dictionary of pseudoclock: value pairs to set. + + Returns: + dict: `values` from arguments on successful programming + reflecting current output state. + """ + for channel, value in values.items(): pin = int(channel.split()[1]) pseudoclock = self.out_pins.index(pin) @@ -158,6 +217,19 @@ def program_manual(self, values): return values def transition_to_buffered(self, device_name, h5file, initial_values, fresh): + """Configures the PrawnBlaster for buffered execution. + + Args: + device_name (str): labscript name of PrawnBlaster + h5file (str): path to shot file to be run + initial_values (dict): Dictionary of output states at start of shot + fresh (bool): When `True`, clear the local :py:attr:`smart_cache`, forcing + a complete reprogramming of the output table. + + Returns: + dict: Dictionary of the expected final output states. + """ + if fresh: self.smart_cache = {} @@ -253,6 +325,9 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): return final def start_run(self): + """When used as the primary pseudoclock, starts execution + in software time to engage the shot.""" + # Start in software: self.logger.info("sending start") self.prawnblaster.write(b"start\r\n") @@ -263,6 +338,9 @@ def start_run(self): self.started = True def wait_for_trigger(self): + """When used as a secondary pseudoclock, sets the PrawnBlaster + to wait for an initial hardware trigger to begin execution.""" + # Set to wait for trigger: self.logger.info("sending hwstart") self.prawnblaster.write(b"hwstart\r\n") @@ -287,6 +365,13 @@ def wait_for_trigger(self): self.started = True def transition_to_manual(self): + """Transition the PrawnBlaster back to manual mode from buffered execution at + the end of a shot. + + Returns: + bool: `True` if transition to manual is successful. + """ + if self.wait_table is not None: with h5py.File(self.h5_file, "a") as hdf5_file: # Work out how long the waits were, save em, post an event saying so @@ -328,9 +413,16 @@ def transition_to_manual(self): return True def shutdown(self): + """Cleanly shuts down the connection to the PrawnBlaster hardware.""" + self.prawnblaster.close() def abort_buffered(self): + """Aborts a currently running buffered execution. + + Returns: + bool: `True` is abort is successful. + """ if not self.is_master_pseudoclock: # Only need to send abort signal if we have told the PrawnBlaster to wait # for a hardware trigger. Otherwise it's just been programmed with @@ -343,4 +435,9 @@ def abort_buffered(self): return True def abort_transition_to_buffered(self): + """Aborts a transition to buffered. + + Calls :py:meth:`abort_buffered`. + """ + return self.abort_buffered() diff --git a/labscript_devices/PrawnBlaster/labscript_devices.py b/labscript_devices/PrawnBlaster/labscript_devices.py index eb05b79c..5d2f21e3 100644 --- a/labscript_devices/PrawnBlaster/labscript_devices.py +++ b/labscript_devices/PrawnBlaster/labscript_devices.py @@ -28,11 +28,27 @@ class _PrawnBlasterPseudoclock(Pseudoclock): + """Customized Clockline for use with the PrawnBlaster. + + This Pseudoclock retains information about which hardware clock + it is associated with, and ensures only one clockline per + pseudoclock. + """ def __init__(self, i, *args, **kwargs): + """ + Args: + i (int): Specifies which hardware pseudoclock this device + is associated with. + """ super().__init__(*args, **kwargs) self.i = i def add_device(self, device): + """ + Args: + device (:class:`~labscript.ClockLine`): Clockline to attach to the + pseudoclock. + """ if isinstance(device, ClockLine): # only allow one child if self.child_devices: @@ -51,6 +67,8 @@ def add_device(self, device): # since everything is handled internally in this device # class _PrawnBlasterDummyPseudoclock(Pseudoclock): + """Dummy Pseudoclock labscript device used internally to allow + :class:`~labscript.WaitMonitor` to work internally to the PrawnBlaster.""" def add_device(self, device): if isinstance(device, _PrawnBlasterDummyClockLine): if self.child_devices: @@ -69,6 +87,8 @@ def generate_code(self, *args, **kwargs): class _PrawnBlasterDummyClockLine(ClockLine): + """Dummy Clockline labscript device used internally to allow + :class:`~labscript.WaitMonitor` to work internally to the PrawnBlaster.""" def add_device(self, device): if isinstance(device, _PrawnBlasterDummyIntermediateDevice): if self.child_devices: @@ -87,6 +107,9 @@ def generate_code(self, *args, **kwargs): class _PrawnBlasterDummyIntermediateDevice(IntermediateDevice): + """Dummy intermediate labscript device used internally to attach + :class:`~labscript.WaitMonitor` objects to the PrawnBlaster.""" + def add_device(self, device): if isinstance(device, WaitMonitor): IntermediateDevice.add_device(self, device) @@ -104,18 +127,26 @@ def generate_code(self, *args, **kwargs): class PrawnBlaster(PseudoclockDevice): description = "PrawnBlaster" clock_limit = 1 / 100e-9 + """Maximum allowable clock rate.""" clock_resolution = 20e-9 - # There appears to be ~50ns buffer on input and then we know there is 80ns between - # trigger detection and first output pulse + """Minimum resolvable time for a clock tick.""" input_response_time = 50e-9 + """Time necessary for hardware to respond to a hardware trigger. + Empirically determined to be a ~50 ns buffer on the input. + """ trigger_delay = input_response_time + 80e-9 - # Overestimate that covers indefinite waits (which labscript does not yet support) + """Processing time delay after trigger is detected. Due to firmware, there is an + 80 ns delay between trigger detection and first output pulse.""" trigger_minimum_duration = 160e-9 - # There are 4 ASM instructions between end of pulse and being ready to detect - # a retrigger + """Minimum required width of hardware trigger. An overestimate that covers + currently unsupported indefinite waits.""" wait_delay = 40e-9 + """Minimum required length of a wait before retrigger can be detected. + Corresponds to 4 instructions.""" allowed_children = [_PrawnBlasterPseudoclock, _PrawnBlasterDummyPseudoclock] max_instructions = 30000 + """Maximum numaber of instructions per pseudoclock. Max is 30,000 for a single + pseudoclock.""" @set_passed_properties( property_names={ @@ -151,6 +182,38 @@ def __init__( external_clock_pin=None, use_wait_monitor=True, ): + """PrawnBlaster Pseudoclock labscript device. + + This labscript device creates Pseudoclocks based on the PrawnBlaster, + a Raspberry Pi Pico with custom firmware. + + Args: + name (str): python variable name to assign to the PrawnBlaster + com_port (str): COM port assigned to the PrawnBlaster by the OS. Takes + the form of `'COMd'`, where `d` is an integer. + num_pseudoclocks (int): Number of pseudoclocks to create. Ranges from 1-4. + trigger_device (:class:`~labscript.IntermediateDevice`, optional): Device + that will send the hardware start trigger when using the PrawnBlaster + as a secondary Pseudoclock. + trigger_connection (str, optional): Which output of the `trigger_device` + is connected to the PrawnBlaster hardware trigger input. + out_pins (list, optional): What outpins to use for the pseudoclock outputs. + Must have length of at least `num_pseudoclocks`. Defaults to `[9,11,13,15]` + in_pins (list, optional): What inpins to use for the pseudoclock hardware + triggering. Must have length of at least `num_pseudoclocks`. + Defaults to `[0,0,0,0]` + clock_frequency (float, optional): Frequency of clock. Standard range + accepts up to 133 MHz. An experimental overclocked firmware is + available that allows higher frequencies. + external_clock_pin (int, optional): If not `None` (the default), + the PrawnBlaster uses an external clock on the provided pin. Valid + options are `20` and `22`. The external frequency must be defined + using `clock_frequency`. + use_wait_monitor (bool, optional): Configure the PrawnBlaster to + perform its own wait monitoring. + + """ + # Check number of pseudoclocks is within range if num_pseudoclocks < 1 or num_pseudoclocks > 4: raise LabscriptError( @@ -249,13 +312,28 @@ def internal_wait_monitor_outputs(self): @property def pseudoclocks(self): + """Returns a list of the automatically generated + :class:`_PrawnBlasterPseudoclock` objects.""" + return copy.copy(self._pseudoclocks) @property def clocklines(self): + """Returns a list of the automatically generated + :class:`~labscript.ClockLine` objects.""" + return copy.copy(self._clocklines) def add_device(self, device): + """Adds child devices. + + This is automatically called by the labscript compiler. + + Args: + device (:class:`_PrawnBlasterPseudoclock` or :class:`_PrawnBlasterDummyPseudoclock`): + Instance to attach to the device. Only the allowed children can be attached. + """ + if len(self.child_devices) < ( self.num_pseudoclocks + self.use_wait_monitor ) and isinstance( @@ -274,6 +352,14 @@ def add_device(self, device): ) def generate_code(self, hdf5_file): + """Generates the hardware instructions for the pseudoclocks. + + This is automatically called by the labscript compiler. + + Args: + hdf5_file (:class:`h5py.File`): h5py file object for shot + """ + PseudoclockDevice.generate_code(self, hdf5_file) group = self.init_device_group(hdf5_file) diff --git a/labscript_devices/PrawnBlaster/runviewer_parsers.py b/labscript_devices/PrawnBlaster/runviewer_parsers.py index 42f9e552..c5ad6c22 100644 --- a/labscript_devices/PrawnBlaster/runviewer_parsers.py +++ b/labscript_devices/PrawnBlaster/runviewer_parsers.py @@ -19,12 +19,30 @@ class PrawnBlasterParser(object): + """Runviewer parser for the PrawnBlaster Pseudoclocks.""" def __init__(self, path, device): + """ + Args: + path (str): path to h5 shot file + device (str): labscript name of PrawnBlaster device + """ self.path = path self.name = device.name self.device = device def get_traces(self, add_trace, clock=None): + """Reads the shot file and extracts hardware instructions to produce + runviewer traces. + + Args: + add_trace (func): function handle that adds traces to runviewer + clock (tuple, optional): clock times from timing device, if not + the primary pseudoclock + + Returns: + dict: Dictionary of clocklines and triggers derived from instructions + """ + if clock is not None: times, clock_value = clock[0], clock[1] clock_indices = np.where((clock_value[1:] - clock_value[:-1]) == 1)[0] + 1 From 34751cead9fce6285d304590798c37843ba999b4 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Tue, 1 Jun 2021 15:05:30 -0400 Subject: [PATCH 25/28] Add PyQT5 intersphinx inventory. --- docs/source/pyqt5-modified-objects.inv | Bin 0 -> 64143 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/source/pyqt5-modified-objects.inv diff --git a/docs/source/pyqt5-modified-objects.inv b/docs/source/pyqt5-modified-objects.inv new file mode 100644 index 0000000000000000000000000000000000000000..418cb89648e921f7eebe61d58f5fc2dac9e2df02 GIT binary patch literal 64143 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 J@&80DL4C2Cbmjm6 literal 0 HcmV?d00001 From 20c2b6d86456404c59ccd5f3a94366ad18e1d29a Mon Sep 17 00:00:00 2001 From: David Meyer Date: Mon, 12 Jul 2021 16:40:17 -0400 Subject: [PATCH 26/28] Improve NI-DAQmx docs a little. --- docs/source/devices/ni_daqs.rst | 11 +- .../NI_DAQmx/labscript_devices.py | 66 ++++++++- .../NI_DAQmx/models/generate_subclasses.py | 22 ++- .../NI_DAQmx/models/get_capabilities.py | 125 ++++++++++++++++-- 4 files changed, 204 insertions(+), 20 deletions(-) diff --git a/docs/source/devices/ni_daqs.rst b/docs/source/devices/ni_daqs.rst index e363c1eb..0353e05c 100644 --- a/docs/source/devices/ni_daqs.rst +++ b/docs/source/devices/ni_daqs.rst @@ -17,16 +17,19 @@ The python bindings are provided by the PyDAQmx package, available through pip. Adding a Device ~~~~~~~~~~~~~~~ -While the `NI_DAQmx` device can be used directly by manually specifying the many necessary parameters, it is preferable to add the device via an appropriate subclass. This process is greatly simplified by using the `get_capabilities.py` script. +While the `NI_DAQmx` device can be used directly by manually specifying the many necessary parameters, +it is preferable to add the device via an appropriate subclass. +This process is greatly simplified by using the :mod:`get_capabilities.py ` script +followed by the :mod:`generate_subclasses.py ` script. To add support for a DAQmx device that is not yet supported, run `get_capabilities.py` on a computer with the device in question connected (or with a simulated device of the correct model configured in NI-MAX). This will introspect the capabilities of the device -and add those details to capabilities.json. To generate labscript device classes for all -devices whose capabilities are known, run `generate_classes.py`. Subclasses of NI_DAQmx +and add those details to `capabilities.json`. To generate labscript device classes for all +devices whose capabilities are known, run `generate_subclasses.py`. Subclasses of NI_DAQmx will be made in the `models` subfolder, and they can then be imported into labscript code with: -..code-block:: python +.. code-block:: python from labscript_devices.NI_DAQmx.labscript_devices import NI_PCIe_6363 diff --git a/labscript_devices/NI_DAQmx/labscript_devices.py b/labscript_devices/NI_DAQmx/labscript_devices.py index 476dc776..dd5107d4 100644 --- a/labscript_devices/NI_DAQmx/labscript_devices.py +++ b/labscript_devices/NI_DAQmx/labscript_devices.py @@ -106,7 +106,52 @@ def __init__( supports_semiperiod_measurement=False, **kwargs ): - """Generic class for NI_DAQmx devices.""" + """Generic class for NI_DAQmx devices. + + Generally over-ridden by device-specific subclasses that contain + the introspected default values. + + Args: + name (str): name to assign to the created labscript device + parent_device (clockline): Parent clockline device that will + clock the outputs of this device + clock_terminal (str): What input on the DAQ is used for the clockline + MAX_name (str): NI-MAX device name + static_AO (int, optional): Number of static analog output channels. + static_DO (int, optional): Number of static digital output channels. + clock_mirror_terminal (str, optional): Channel string of digital output + that mirrors the input clock. Useful for daisy-chaning DAQs on the same + clockline. + acquisiton_rate (float, optional): Default sample rate of inputs. + AI_range (iterable, optional): A `[Vmin, Vmax]` pair that sets the analog + input voltage range for all analog inputs. + AI_start_delay (float, optional): Time in seconds between start of an + analog input task starting and the first sample. + AO_range (iterable, optional): A `[Vmin, Vmax]` pair that sets the analog + output voltage range for all analog outputs. + max_AI_multi_chan_rate (float, optional): Max supported analog input + sampling rate when using multiple channels. + max_AI_single_chan_rate (float, optional): Max supported analog input + sampling rate when only using a single channel. + max_AO_sample_rate (float, optional): Max supported analog output + sample rate. + max_DO_sample_rate (float, optional): Max supported digital output + sample rate. + min_sermiperiod_measurement (float, optional): Minimum measurable time + for a semiperiod measurement. + num_AI (int, optional): Number of analog inputs channels. + num_AO (int, optional): Number of analog output channels. + num_CI (int, optional): Number of counter input channels. + ports (dict, optional): Dictionarly of DIO ports, which number of lines + and whether port supports buffered output. + supports_buffered_AO (bool, optional): True if analog outputs support + buffered output + supports_buffered_DO (bool, optional): True if digital outputs support + buffered output + supports_semiperiod_measurement (bool, optional): True if deviec supports + semi-period measurements + + """ # Default static output setting based on whether the device supports buffered # output: @@ -169,8 +214,8 @@ def __init__( self.wait_monitor_minimum_pulse_width = self.min_semiperiod_measurement - # Set allowed children based on capabilities: self.allowed_children = [] + '''Sets the allowed children types based on the capabilites.''' if self.num_AI > 0: self.allowed_children += [AnalogIn] if self.num_AO > 0 and static_AO: @@ -198,7 +243,13 @@ def __init__( IntermediateDevice.__init__(self, name, parent_device, **kwargs) def add_device(self, device): - """Error checking for adding a child device""" + """Error checking for adding a child device. + + Args: + device (labscript device): Child labscript device to + attach to this device. Only types of devices in :obj:`allowed_children` + can be attached. + """ # Verify static/dynamic outputs compatible with configuration: if isinstance(device, StaticAnalogOut) and not self.static_AO: msg = """Cannot add StaticAnalogOut to NI_DAQmx device configured for @@ -293,6 +344,7 @@ def _check_bounds(self, analogs): np.clip(output.raw_output, vmin, vmax, out=output.raw_output) def _check_AI_not_too_fast(self, AI_table): + """Check that analog input acquisition rates do not exceed maximums.""" if AI_table is None: return n = len(set(AI_table['connection'])) @@ -442,6 +494,14 @@ def _check_wait_monitor_timeout_device_config(self): raise RuntimeError(dedent(msg)) def generate_code(self, hdf5_file): + """Generates the hardware code from the script and saves it to the + shot h5 file. + + This is called automatically when a shot is compiled. + + Args: + hdf5_file (str): Path to shot's hdf5 file to save the instructions to. + """ IntermediateDevice.generate_code(self, hdf5_file) analogs = {} digitals = {} diff --git a/labscript_devices/NI_DAQmx/models/generate_subclasses.py b/labscript_devices/NI_DAQmx/models/generate_subclasses.py index c062def9..0a95ab9b 100644 --- a/labscript_devices/NI_DAQmx/models/generate_subclasses.py +++ b/labscript_devices/NI_DAQmx/models/generate_subclasses.py @@ -10,6 +10,16 @@ # file in the root of the project for the full license. # # # ##################################################################### +"""Reads the capabilities file and generates labscript devices +for each known model of DAQ. + +Called from the command line via + +.. code-block:: shell + + python generate_subclasses.py + +""" import os import warnings import json @@ -23,7 +33,12 @@ def reformat_files(filepaths): - """Apply black formatter to a list of source files""" + """Apply `black `_ + formatter to a list of source files. + + Args: + filepaths (list): List of python source files to format. + """ try: import black except ImportError: @@ -42,6 +57,11 @@ def reformat_files(filepaths): def main(): + """Called when the script is run. + + Will attempt to reformat the generated files using + :func:`reformat_files`. + """ capabilities = {} if os.path.exists(CAPABILITIES_FILE): with open(CAPABILITIES_FILE) as f: diff --git a/labscript_devices/NI_DAQmx/models/get_capabilities.py b/labscript_devices/NI_DAQmx/models/get_capabilities.py index c05c2808..acda3512 100644 --- a/labscript_devices/NI_DAQmx/models/get_capabilities.py +++ b/labscript_devices/NI_DAQmx/models/get_capabilities.py @@ -10,6 +10,20 @@ # file in the root of the project for the full license. # # # ##################################################################### +"""This is a script to update `model_capabilities.json` with the capabilities of all +NI-DAQmx devices currently connected to this computer. + +Run this script to add support for a new model of NI-DAQmx device. +Note that this will work with a simulated device configured through NI-MAX as well, +so support can be added without actually having the physical device. + +Called from the command line via + +.. code-block:: shell + + python get_capabilities.py + +""" import numpy as np import os @@ -24,14 +38,15 @@ CAPABILITIES_FILE = os.path.join(THIS_FOLDER, 'capabilities.json') -"""This is a script to update model_capabilities.json with the capabilities of all -NI-DAQmx devices currently connected to this computer. Run this script to add support -for a new model of NI-DAQmx device. Note that this will work with a simulated device -configured through NI-MAX as well, so support can be added without actually having the -physical device""" +def string_prop(func): + """String property wrapper. + Args: + func (function): PyDAQmx library function that returns a string. -def string_prop(func): + Returns: + function: The wrapped function. + """ def wrapped(name=None): BUFSIZE = 4096 result = ctypes.create_string_buffer(BUFSIZE) @@ -45,6 +60,14 @@ def wrapped(name=None): def bool_prop(func): + """Bool property wrapper. + + Args: + func (function): PyDAQmx library function that returns a boolean. + + Returns: + function: The wrapped function. + """ def wrapped(name): result = bool32() func(name, byref(result)) @@ -54,6 +77,14 @@ def wrapped(name): def int32_prop(func): + """Int32 property wrapper. + + Args: + func (function): PyDAQmx library function that returns a int32. + + Returns: + function: The wrapped function. + """ def wrapped(name): result = int32() func(name, byref(result)) @@ -63,6 +94,14 @@ def wrapped(name): def float64_prop(func): + """Float property wrapper. + + Args: + func (function): PyDAQmx library function that returns a float64. + + Returns: + function: The wrapped function. + """ def wrapped(name): result = float64() func(name, byref(result)) @@ -72,6 +111,15 @@ def wrapped(name): def float64_array_prop(func): + """Array of floats property wrapper. + + Args: + func (function): PyDAQmx library function that returns an array of + float64s. + + Returns: + function: The wrapped function. + """ def wrapped(name): import warnings @@ -91,8 +139,15 @@ def wrapped(name): def chans(func): - """string_prop but splitting the return value into separate channels and stripping - the device name from them""" + """string_prop but splitting the return value into separate channels + and stripping the device name from them + + Args: + func (function): PyDAQmx library function that returns channel string. + + Returns: + function: The wrapped function. + """ wrapped1 = string_prop(func) def wrapped2(name): @@ -126,6 +181,16 @@ def wrapped2(name): def port_supports_buffered(device_name, port, clock_terminal=None): + """Empirically determines if the digital port supports buffered output. + + Args: + device_name (str): NI-MAX device name + port (int): Which port to intro-spect + clock_terminal (str, optional): String that specifies the clock terminal. + + Returns: + bool: True if `port` supports buffered output. + """ all_terminals = DAQmxGetDevTerminals(device_name) if clock_terminal is None: clock_terminal = all_terminals[0] @@ -170,6 +235,15 @@ def port_supports_buffered(device_name, port, clock_terminal=None): def AI_start_delay(device_name): + """Empirically determines the analog inputs' start delay. + + Args: + device_name (str): NI-MAX device name + + Returns: + float: Analog input start delay in seconds. `None` if + analog inputs not supported. + """ if 'PFI0' not in DAQmxGetDevTerminals(device_name): return None task = Task() @@ -202,9 +276,19 @@ def AI_start_delay(device_name): def supported_AI_ranges_for_non_differential_input(device_name, AI_ranges): - """Try AI ranges to see which are actually allowed for non-differential input, since + """Empirically determine the analog input voltage ranges for non-differential inputs. + + Tries AI ranges to see which are actually allowed for non-differential input, since the largest range may only be available for differential input, which we don't - attempt to support (though we could with a little effort)""" + attempt to support (though we could with a little effort). + + Args: + device_name (str): NI-MAX device name + AI_ranges (list): list of `[Vmin, Vmax]` pairs to check compatibility. + + Returns: + list: List of lists with the supported voltage ranges. + """ chan = device_name + '/ai0' supported_ranges = [] for Vmin, Vmax in AI_ranges: @@ -227,6 +311,14 @@ def supported_AI_ranges_for_non_differential_input(device_name, AI_ranges): def supports_semiperiod_measurement(device_name): + """Empirically determines if the DAQ supports semiperiod measurement. + + Args: + device_name (str): NI-MAX device name. + + Returns: + bool: True if semi-period measurements are supported by the device. + """ import warnings with warnings.catch_warnings(): @@ -242,7 +334,9 @@ def supports_semiperiod_measurement(device_name): def get_min_semiperiod_measurement(device_name): - """Depending on the timebase used, counter inputs can measure time intervals of + """Determines the minimum semi-period measurement time supported by the device. + + Depending on the timebase used, counter inputs can measure time intervals of various ranges. As a default, we pick a largish range - the one with the fastest timebase still capable of measuring 100 seconds, or the largest time interval if it is less than 100 seconds, and we save the smallest interval measurable with this @@ -258,7 +352,14 @@ def get_min_semiperiod_measurement(device_name): possibility of timing out. For now (in the wait monitor worker class) we pessimistically add one second to the expected longest measurement to account for software delays. These decisions can be revisited if there is a need, do not - hesitate to file an issue on bitbucket regarding this if it affects you.""" + hesitate to file an issue on bitbucket regarding this if it affects you. + + Args: + device_name (str): NI-MAX device name + + Returns: + float: Minimum measurement time. + """ CI_chans = DAQmxGetDevCIPhysicalChans(device_name) CI_chan = device_name + '/' + CI_chans[0] # Make a task with a semiperiod measurement From c5541318b4e63a8d549d4966ebb60f8df4cbaec9 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Thu, 15 Jul 2021 10:23:25 -0400 Subject: [PATCH 27/28] Add docstring coverage to build. --- docs/source/conf.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 2627abd5..8f86f899 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -229,6 +229,9 @@ def setup(app): # hook to run apidoc before building app.connect('builder-inited', run_apidoc) + # hooks to test docstring coverage + app.connect('autodoc-process-docstring', doc_coverage) + app.connect('build-finished', doc_report) def run_apidoc(_): @@ -248,3 +251,39 @@ def run_apidoc(_): main(['-TMf', '-s', 'inc', '-t', templates_path, '-o', out_path, daq_models_path]) + + +members_to_watch = ['module', 'class', 'function', 'exception', 'method', 'attribute'] +doc_count = 0 +undoc_count = 0 +undoc_objects = [] +undoc_print_objects = False + + +def doc_coverage(app, what, name, obj, options, lines): + global doc_count + global undoc_count + global undoc_objects + + if (what in members_to_watch and len(lines) == 0): + # blank docstring detected + undoc_count += 1 + undoc_objects.append(name) + else: + doc_count += 1 + + +def doc_report(app, exception): + global doc_count + global undoc_count + global undoc_objects + # print out report of documentation coverage + total_docs = undoc_count + doc_count + if total_docs != 0: + print(f'\nAPI Doc coverage of {doc_count/total_docs:.1%}') + if undoc_print_objects or os.environ.get('READTHEDOCS'): + print('\nItems lacking documentation') + print('===========================') + print(*undoc_objects, sep='\n') + else: + print('No docs counted, run \'make clean\' then rebuild to get the count.') \ No newline at end of file From d9514f0d96e8725612ec3a34165d3d4b21e38a79 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Thu, 15 Jul 2021 10:24:20 -0400 Subject: [PATCH 28/28] Update sphinx version. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 3909ba4f..c018fdae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,7 +44,7 @@ install_requires = [options.extras_require] docs = PyQt5 - Sphinx==3.2.1 + Sphinx==3.5.3 sphinx-rtd-theme==0.5.2 recommonmark==0.6.0 m2r==0.2.1